Merge pull request #169 from mamoe/long-message

Support long message
This commit is contained in:
Him188 2020-03-29 02:49:35 +08:00 committed by GitHub
commit 6b332a7328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 912 additions and 183 deletions

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * 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 android.annotation.SuppressLint
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI

View File

@ -33,18 +33,24 @@ import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.contact.MemberInfoImpl import net.mamoe.mirai.qqandroid.contact.MemberInfoImpl
import net.mamoe.mirai.qqandroid.contact.QQImpl import net.mamoe.mirai.qqandroid.contact.QQImpl
import net.mamoe.mirai.qqandroid.contact.checkIsGroupImpl 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.OnlineFriendImageImpl
import net.mamoe.mirai.qqandroid.message.OnlineGroupImageImpl import net.mamoe.mirai.qqandroid.message.OnlineGroupImageImpl
import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.PbMessageSvc import net.mamoe.mirai.qqandroid.network.protocol.data.proto.LongMsg
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.*
import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList 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.*
import net.mamoe.mirai.utils.io.encodeToString import net.mamoe.mirai.utils.io.encodeToString
import net.mamoe.mirai.utils.io.toReadPacket
import kotlin.collections.asSequence import kotlin.collections.asSequence
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.absoluteValue
import kotlin.random.Random
@OptIn(MiraiInternalAPI::class) @OptIn(MiraiInternalAPI::class)
internal expect class QQAndroidBot constructor( internal expect class QQAndroidBot constructor(
@ -360,6 +366,97 @@ internal abstract class QQAndroidBotBase constructor(
return json.parse(GroupActiveData.serializer(), rep) 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<MultiMsg.ApplyUp.Response>()
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) { override suspend fun queryImageUrl(image: Image): String = when (image) {
is OnlineFriendImageImpl -> image.originUrl is OnlineFriendImageImpl -> image.originUrl
is OnlineGroupImageImpl -> image.originUrl is OnlineGroupImageImpl -> image.originUrl

View File

@ -65,6 +65,8 @@ internal class GroupImpl(
companion object; companion object;
override val bot: QQAndroidBot by bot.unsafeWeakRef() override val bot: QQAndroidBot by bot.unsafeWeakRef()
@OptIn(LowLevelAPI::class)
val uin: Long = groupInfo.uin val uin: Long = groupInfo.uin
override lateinit var owner: Member override lateinit var owner: Member
@ -288,9 +290,13 @@ internal class GroupImpl(
source = it source = it
source.startWaitingSequenceId(this) source.startWaitingSequenceId(this)
}.sendAndExpect() }.sendAndExpect()
check( if (response is MessageSvc.PbSendMsg.Response.Failed) {
response is MessageSvc.PbSendMsg.Response.SUCCESS when (response.resultType) {
) { "send message failed: $response" } 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) return MessageReceipt(source, this, botAsMember)
@ -349,7 +355,7 @@ internal class GroupImpl(
imageInput = image.input, imageInput = image.input,
inputSize = image.inputSize.toInt(), inputSize = image.inputSize.toInt(),
fileMd5 = image.md5, fileMd5 = image.md5,
uKey = response.uKey, ticket = response.uKey,
commandId = 2 commandId = 2
) )
} ?: error("timeout uploading image: ${image.filename}") } ?: error("timeout uploading image: ${image.filename}")

View File

@ -18,10 +18,8 @@ import kotlinx.serialization.builtins.SetSerializer
import kotlinx.serialization.internal.* import kotlinx.serialization.internal.*
import kotlinx.serialization.modules.EmptyModule import kotlinx.serialization.modules.EmptyModule
import kotlinx.serialization.modules.SerialModule import kotlinx.serialization.modules.SerialModule
import kotlinx.serialization.protobuf.ProtoId
import net.mamoe.mirai.qqandroid.io.JceStruct import net.mamoe.mirai.qqandroid.io.JceStruct
import net.mamoe.mirai.qqandroid.io.ProtoBuf 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.BYTE
import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.DOUBLE import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.DOUBLE
import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.FLOAT 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.Jce.Companion.ZERO_TYPE
import net.mamoe.mirai.qqandroid.io.serialization.jce.JceHead import net.mamoe.mirai.qqandroid.io.serialization.jce.JceHead
import net.mamoe.mirai.qqandroid.io.serialization.jce.JceId 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 import net.mamoe.mirai.utils.io.toReadPacket
@PublishedApi @PublishedApi

View File

@ -11,7 +11,7 @@ package net.mamoe.mirai.qqandroid.io.serialization.jce
import kotlinx.io.core.* import kotlinx.io.core.*
import net.mamoe.mirai.qqandroid.io.serialization.JceCharset import net.mamoe.mirai.qqandroid.io.serialization.JceCharset
import net.mamoe.mirai.utils.io.readString import net.mamoe.mirai.qqandroid.utils.io.readString
/** /**

View File

@ -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.MiraiInternalAPI
import net.mamoe.mirai.utils.firstValue import net.mamoe.mirai.utils.firstValue
import net.mamoe.mirai.utils.io.read 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 net.mamoe.mirai.utils.io.toReadPacket
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName

View File

@ -254,7 +254,7 @@ internal class MessageSourceFromSendGroup(
override val groupId: Long, override val groupId: Long,
override val originalMessage: MessageChain override val originalMessage: MessageChain
) : MessageSourceFromSend() { ) : MessageSourceFromSend() {
private lateinit var sequenceIdDeferred: Deferred<Int> internal lateinit var sequenceIdDeferred: Deferred<Int>
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override val id: Long override val id: Long

View File

@ -6,6 +6,7 @@
* *
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file: OptIn(MiraiExperimentalAPI::class, MiraiInternalAPI::class, LowLevelAPI::class, ExperimentalUnsignedTypes::class)
package net.mamoe.mirai.qqandroid.message 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) @OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class)
internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgBody.Elem> { internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgBody.Elem> {
val elements = mutableListOf<ImMsgBody.Elem>() val elements = mutableListOf<ImMsgBody.Elem>()
@ -231,33 +234,54 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
} }
} }
var longTextResId: String? = null
fun transformOneMessage(it: Message) { fun transformOneMessage(it: Message) {
if (it is RichMessage) {
val content = MiraiPlatformUtils.zip(it.content.toByteArray())
when (it) {
is LongMessage -> {
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) { when (it) {
is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue))) is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue)))
is At -> { is At -> {
elements.add(ImMsgBody.Elem(text = it.toJceData())) elements.add(ImMsgBody.Elem(text = it.toJceData()))
elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " "))) 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 OfflineGroupImage -> elements.add(ImMsgBody.Elem(customFace = it.toJceData()))
is OnlineGroupImageImpl -> elements.add(ImMsgBody.Elem(customFace = it.delegate)) is OnlineGroupImageImpl -> elements.add(ImMsgBody.Elem(customFace = it.delegate))
is OnlineFriendImageImpl -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate)) is OnlineFriendImageImpl -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate))
@ -267,16 +291,18 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
is QuoteReplyToSend -> { is QuoteReplyToSend -> {
if (forGroup) { if (forGroup) {
check(it is QuoteReplyToSend.ToGroup) { 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) { if (it.sender is Member) {
transformOneMessage(it.createAt()) transformOneMessage(it.createAt())
} }
transformOneMessage(" ".toMessage()) transformOneMessage(PlainText(" "))
} }
} }
is QuoteReply, is QuoteReply, // already transformed above
is MessageSource -> { is MessageSource, // mirai only
is RichMessage, // already transformed above
-> {
} }
else -> error("unsupported message type: ${it::class.simpleName}") else -> error("unsupported message type: ${it::class.simpleName}")
@ -284,10 +310,24 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
} }
this.forEach(::transformOneMessage) this.forEach(::transformOneMessage)
if (this.any<RichMessage>()) { 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<RichMessage>() -> {
// 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 // 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()))) 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()))) }
else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes())))
}
return elements return elements
} }
@ -358,7 +398,7 @@ internal fun MsgComm.Msg.toMessageChain(): MessageChain {
return buildMessageChain(elements.size + 1) { return buildMessageChain(elements.size + 1) {
+MessageSourceFromMsg(delegate = this@toMessageChain) +MessageSourceFromMsg(delegate = this@toMessageChain)
elements.joinToMessageChain(this) elements.joinToMessageChain(this)
}.removeAtIfHasQuoteReply() }.cleanupRubbishMessageElements()
} }
// These two functions are not identical, dont combine. // These two functions are not identical, dont combine.
@ -369,11 +409,40 @@ internal fun ImMsgBody.SourceMsg.toMessageChain(): MessageChain {
return buildMessageChain(elements.size + 1) { return buildMessageChain(elements.size + 1) {
+MessageSourceFromServer(delegate = this@toMessageChain) +MessageSourceFromServer(delegate = this@toMessageChain)
elements.joinToMessageChain(this) 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 <reified R> 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<QuoteReply>()) { if (this.any<QuoteReply>()) {
var removed = false var removed = false
@ -387,9 +456,6 @@ private fun MessageChain.removeAtIfHasQuoteReply(): MessageChain =
}.asMessageChain() }.asMessageChain()
} else this*/ } else this*/
@OptIn(
MiraiInternalAPI::class, ExperimentalUnsignedTypes::class, MiraiDebugAPI::class, LowLevelAPI::class
)
internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilder) { internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilder) {
this.forEach { this.forEach {
when { when {
@ -425,6 +491,12 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilde
when (it.richMsg.serviceId) { when (it.richMsg.serviceId) {
1 -> message.add(JsonMessage(content)) 1 -> message.add(JsonMessage(content))
60 -> message.add(XmlMessage(content)) 60 -> message.add(XmlMessage(content))
35 -> message.add(
LongMessage(
content,
this.firstIsInstance<ImMsgBody.GeneralFlags>().longTextResid
)
)
else -> { else -> {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
MiraiLogger.debug { MiraiLogger.debug {

View File

@ -39,8 +39,8 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.ByteArrayPool import net.mamoe.mirai.utils.io.ByteArrayPool
import net.mamoe.mirai.utils.io.PlatformSocket import net.mamoe.mirai.utils.io.PlatformSocket
import net.mamoe.mirai.utils.io.readPacketExact import net.mamoe.mirai.qqandroid.utils.io.readPacketExact
import net.mamoe.mirai.utils.io.useBytes import net.mamoe.mirai.qqandroid.utils.io.useBytes
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.jvm.Volatile import kotlin.jvm.Volatile
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@ -581,7 +581,6 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" } check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" }
suspend fun doSendAndReceive(handler: PacketListener, data: Any, length: Int): E { suspend fun doSendAndReceive(handler: PacketListener, data: Any, length: Int): E {
val result = async {
withTimeoutOrNull(3000) { withTimeoutOrNull(3000) {
withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) { withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) {
PacketLogger.debug { "Channel sending: $commandName" } PacketLogger.debug { "Channel sending: $commandName" }
@ -592,21 +591,15 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
} }
PacketLogger.debug { "Channel send done: $commandName" } PacketLogger.debug { "Channel send done: $commandName" }
} }
} ?: return@async "timeout sending packet $commandName" } ?: throw TimeoutException("timeout sending packet $commandName")
logger.verbose("Send done: $commandName") logger.verbose("Send done: $commandName")
withTimeoutOrNull(timeoutMillis) { @Suppress("UNCHECKED_CAST")
return withTimeoutOrNull(timeoutMillis) {
handler.await() handler.await()
// 不要 `withTimeout`. timeout 的报错会不正常. // 不要 `withTimeout`. timeout 的报错会不正常.
} ?: return@async "timeout receiving response of $commandName" } as E? ?: throw TimeoutException("timeout receiving response of $commandName")
}
@Suppress("UNCHECKED_CAST")
when (val value = result.await()) {
is String -> throw TimeoutException(value)
else -> return value as E
}
} }
if (retry == 0) { if (retry == 0) {

View File

@ -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.PacketLogger
import net.mamoe.mirai.qqandroid.network.protocol.packet.Tlv import net.mamoe.mirai.qqandroid.network.protocol.packet.Tlv
import net.mamoe.mirai.qqandroid.utils.NetworkType 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.*
import net.mamoe.mirai.utils.cryptor.ECDH import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
import net.mamoe.mirai.utils.cryptor.TEA
import net.mamoe.mirai.utils.io.* import net.mamoe.mirai.utils.io.*
/* /*
@ -101,7 +101,7 @@ internal open class QQAndroidClient(
var openAppId: Long = 715019303L var openAppId: Long = 715019303L
val apkVersionName: ByteArray get() = "8.2.7".toByteArray() 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) private val messageSequenceId: AtomicInt = atomic(22911)
internal fun atomicNextMessageSequenceId(): Int = messageSequenceId.getAndAdd(2) internal fun atomicNextMessageSequenceId(): Int = messageSequenceId.getAndAdd(2)
@ -115,6 +115,9 @@ internal open class QQAndroidClient(
private val highwayDataTransSequenceIdForFriend: AtomicInt = atomic(43973) private val highwayDataTransSequenceIdForFriend: AtomicInt = atomic(43973)
internal fun nextHighwayDataTransSequenceIdForFriend(): Int = highwayDataTransSequenceIdForFriend.getAndAdd(2) internal fun nextHighwayDataTransSequenceIdForFriend(): Int = highwayDataTransSequenceIdForFriend.getAndAdd(2)
private val highwayDataTransSequenceIdForApplyUp: AtomicInt = atomic(77918)
internal fun nextHighwayDataTransSequenceIdForApplyUp(): Int = highwayDataTransSequenceIdForApplyUp.getAndAdd(2)
val appClientVersion: Int = 0 val appClientVersion: Int = 0
var networkType: NetworkType = NetworkType.WIFI var networkType: NetworkType = NetworkType.WIFI

View File

@ -33,7 +33,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.copyAndClose import net.mamoe.mirai.utils.copyAndClose
import net.mamoe.mirai.utils.io.ByteArrayPool import net.mamoe.mirai.utils.io.ByteArrayPool
import net.mamoe.mirai.utils.io.PlatformSocket 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 import kotlinx.serialization.InternalSerializationApi
@OptIn(MiraiInternalAPI::class, InternalSerializationApi::class) @OptIn(MiraiInternalAPI::class, InternalSerializationApi::class)
@ -101,7 +101,7 @@ internal object HighwayHelper {
client: QQAndroidClient, client: QQAndroidClient,
serverIp: String, serverIp: String,
serverPort: Int, serverPort: Int,
uKey: ByteArray, ticket: ByteArray,
imageInput: Any, imageInput: Any,
inputSize: Int, inputSize: Int,
fileMd5: ByteArray, 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(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(fileMd5.size == 16) { "bad md5. Required size=16, got ${fileMd5.size}" }
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(commandId == 2 || commandId == 1) { "bad commandId. Must be 1 or 2" } // require(commandId == 2 || commandId == 1) { "bad commandId. Must be 1 or 2" }
val socket = PlatformSocket() val socket = PlatformSocket()
socket.connect(serverIp, serverPort) socket.connect(serverIp, serverPort)
@ -119,7 +119,7 @@ internal object HighwayHelper {
client = client, client = client,
command = "PicUp.DataUp", command = "PicUp.DataUp",
commandId = commandId, commandId = commandId,
uKey = uKey, ticket = ticket,
data = imageInput, data = imageInput,
dataSize = inputSize, dataSize = inputSize,
fileMd5 = fileMd5 fileMd5 = fileMd5

View File

@ -36,7 +36,7 @@ internal fun createImageDataPacketSequence( // RequestDataTrans
dataFlag: Int = 4096, dataFlag: Int = 4096,
commandId: Int, commandId: Int,
localId: Int = 2052, localId: Int = 2052,
uKey: ByteArray, ticket: ByteArray,
data: Any, data: Any,
dataSize: Int, dataSize: Int,
@ -45,7 +45,7 @@ internal fun createImageDataPacketSequence( // RequestDataTrans
): Flow<ByteReadPacket> { ): Flow<ByteReadPacket> {
ByteArrayPool.checkBufferSize(sizePerPacket) ByteArrayPool.checkBufferSize(sizePerPacket)
require(data is Input || data is InputStream || data is ByteReadChannel) { "unsupported data: ${data::class.simpleName}" } 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}" } require(data !is ByteReadPacket || data.remaining.toInt() == dataSize) { "bad input. given dataSize=$dataSize, but actual readRemaining=${(data as ByteReadPacket).remaining}" }
val flow = when (data) { val flow = when (data) {
@ -64,8 +64,12 @@ internal fun createImageDataPacketSequence( // RequestDataTrans
version = 1, version = 1,
uin = client.uin.toString(), uin = client.uin.toString(),
command = command, command = command,
seq = if (commandId == 2) client.nextHighwayDataTransSequenceIdForGroup() seq = when (commandId) {
else client.nextHighwayDataTransSequenceIdForFriend(), 2 -> client.nextHighwayDataTransSequenceIdForGroup()
1 -> client.nextHighwayDataTransSequenceIdForFriend()
27 -> client.nextHighwayDataTransSequenceIdForApplyUp()
else -> error("illegal commandId: $commandId")
},
retryTimes = 0, retryTimes = 0,
appid = appId, appid = appId,
dataflag = dataFlag, dataflag = dataFlag,
@ -77,7 +81,7 @@ internal fun createImageDataPacketSequence( // RequestDataTrans
datalength = chunkedInput.bufferSize, datalength = chunkedInput.bufferSize,
dataoffset = offset, dataoffset = offset,
filesize = dataSize.toLong(), filesize = dataSize.toLong(),
serviceticket = uKey, serviceticket = ticket,
md5 = MiraiPlatformUtils.md5(chunkedInput.buffer, 0, chunkedInput.bufferSize), md5 = MiraiPlatformUtils.md5(chunkedInput.buffer, 0, chunkedInput.bufferSize),
fileMd5 = fileMd5, fileMd5 = fileMd5,
flag = 0, flag = 0,

View File

@ -17,7 +17,7 @@ import net.mamoe.mirai.qqandroid.io.ProtoBuf
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
@Serializable @Serializable
class BdhExtinfo : ProtoBuf { internal class BdhExtinfo : ProtoBuf {
@Serializable @Serializable
class CommFileExtReq( class CommFileExtReq(
@ProtoId(1) val actionType: Int = 0, @ProtoId(1) val actionType: Int = 0,
@ -140,7 +140,7 @@ class BdhExtinfo : ProtoBuf {
} }
@Serializable @Serializable
class CSDataHighwayHead : ProtoBuf { internal class CSDataHighwayHead : ProtoBuf {
@Serializable @Serializable
class C2CCommonExtendinfo( class C2CCommonExtendinfo(
@ProtoId(1) val infoId: Int = 0, @ProtoId(1) val infoId: Int = 0,
@ -283,7 +283,7 @@ class CSDataHighwayHead : ProtoBuf {
} }
@Serializable @Serializable
class HwConfigPersistentPB : ProtoBuf { internal class HwConfigPersistentPB : ProtoBuf {
@Serializable @Serializable
class HwConfigItemPB( class HwConfigItemPB(
@ProtoId(1) val ingKey: String = "", @ProtoId(1) val ingKey: String = "",
@ -315,7 +315,7 @@ class HwConfigPersistentPB : ProtoBuf {
} }
@Serializable @Serializable
class HwSessionInfoPersistentPB : ProtoBuf { internal class HwSessionInfoPersistentPB : ProtoBuf {
@Serializable @Serializable
class HwSessionInfoPB( class HwSessionInfoPB(
@ProtoId(1) val httpconnSigSession: ByteArray = EMPTY_BYTE_ARRAY, @ProtoId(1) val httpconnSigSession: ByteArray = EMPTY_BYTE_ARRAY,
@ -324,7 +324,7 @@ class HwSessionInfoPersistentPB : ProtoBuf {
} }
@Serializable @Serializable
class Subcmd0x501 : ProtoBuf { internal class Subcmd0x501 : ProtoBuf {
@Serializable @Serializable
class ReqBody( class ReqBody(
@ProtoId(1281) val msgSubcmd0x501ReqBody: SubCmd0x501ReqBody? = null @ProtoId(1281) val msgSubcmd0x501ReqBody: SubCmd0x501ReqBody? = null

View File

@ -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<LongMsg.MsgUpReq>? = null,
@ProtoId(5) val msgDownReq: List<LongMsg.MsgDownReq>? = null,
@ProtoId(6) val msgDelReq: List<LongMsg.MsgDeleteReq>? = null,
@ProtoId(10) val agentType: Int = 0
) : ProtoBuf
@Serializable
class RspBody(
@ProtoId(1) val subcmd: Int = 0,
@ProtoId(2) val msgUpRsp: List<LongMsg.MsgUpRsp>? = null,
@ProtoId(3) val msgDownRsp: List<LongMsg.MsgDownRsp>? = null,
@ProtoId(4) val msgDelRsp: List<LongMsg.MsgDeleteRsp>? = null
) : ProtoBuf
}

View File

@ -467,7 +467,7 @@ internal class ImMsgBody : ProtoBuf {
@ProtoId(4) val rpId: ByteArray = EMPTY_BYTE_ARRAY, @ProtoId(4) val rpId: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoId(5) val prpFold: Int = 0, @ProtoId(5) val prpFold: Int = 0,
@ProtoId(6) val longTextFlag: 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(8) val groupType: Int = 0,
@ProtoId(9) val toUinFlag: Int = 0, @ProtoId(9) val toUinFlag: Int = 0,
@ProtoId(10) val glamourLevel: Int = 0, @ProtoId(10) val glamourLevel: Int = 0,

View File

@ -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<MsgComm.Msg>? = null
) : ProtoBuf
@Serializable
class PbMultiMsgTransmit(
@ProtoId(1) val msg: List<MsgComm.Msg>? = null,
@ProtoId(2) val pbItemList: List<MsgTransmit.PbMultiMsgItem>? = null
) : ProtoBuf
}

View File

@ -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<Int>? = null,
@ProtoId(5) val uint32DownPort: List<Int>? = null,
@ProtoId(6) val msgResid: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoId(7) val msgExternInfo: MultiMsg.ExternMsg? = null,
@ProtoId(8) val bytesDownIpV6: List<ByteArray>? = null,
@ProtoId(9) val uint32DownV6Port: List<Int>? = 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<Int>? = null,
@ProtoId(5) val uint32UpPort: List<Int>? = 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<ByteArray>? = null,
@ProtoId(13) val uint32UpV6Port: List<Int>? = 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<MultiMsg.MultiMsgApplyUpReq>? = null,
@ProtoId(7) val multimsgApplydownReq: List<MultiMsg.MultiMsgApplyDownReq>? = 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<MultiMsg.MultiMsgApplyUpRsp>? = null,
@ProtoId(3) val multimsgApplydownRsp: List<MultiMsg.MultiMsgApplyDownRsp>? = null
) : ProtoBuf
}

View File

@ -14,10 +14,10 @@ import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.buildPacket import kotlinx.io.core.buildPacket
import kotlinx.io.core.writeFully import kotlinx.io.core.writeFully
import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.utils.cryptor.ECDH import net.mamoe.mirai.qqandroid.utils.cryptor.ECDH
import net.mamoe.mirai.utils.cryptor.ECDHKeyPair import net.mamoe.mirai.qqandroid.utils.cryptor.ECDHKeyPair
import net.mamoe.mirai.utils.io.encryptAndWrite import net.mamoe.mirai.qqandroid.utils.io.encryptAndWrite
import net.mamoe.mirai.utils.io.writeShortLVByteArray import net.mamoe.mirai.qqandroid.utils.io.writeShortLVByteArray
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
internal interface EncryptMethod { internal interface EncryptMethod {

View File

@ -16,9 +16,9 @@ import kotlinx.io.core.buildPacket
import kotlinx.io.core.writeFully import kotlinx.io.core.writeFully
import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.io.encryptAndWrite import net.mamoe.mirai.qqandroid.utils.io.encryptAndWrite
import net.mamoe.mirai.utils.io.writeHex import net.mamoe.mirai.qqandroid.utils.io.writeHex
import net.mamoe.mirai.utils.io.writeIntLVPacket import net.mamoe.mirai.qqandroid.utils.io.writeIntLVPacket
internal class OutgoingPacket constructor( internal class OutgoingPacket constructor(
name: String?, name: String?,

View File

@ -11,9 +11,10 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet
import kotlinx.io.core.* import kotlinx.io.core.*
import kotlinx.io.pool.useInstance import kotlinx.io.pool.useInstance
import net.mamoe.mirai.qqandroid.network.Packet
import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.Event
import net.mamoe.mirai.qqandroid.QQAndroidBot 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.PbMessageSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore 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.StatSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.qqandroid.network.readUShortLVByteArray 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.*
import net.mamoe.mirai.utils.cryptor.TEA import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
import net.mamoe.mirai.utils.cryptor.adjustToPublicKey import net.mamoe.mirai.qqandroid.utils.cryptor.adjustToPublicKey
import net.mamoe.mirai.utils.io.* import net.mamoe.mirai.utils.io.*
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
@ -144,7 +149,8 @@ internal object KnownPacketFactories {
TroopManagement.EditGroupNametag, TroopManagement.EditGroupNametag,
TroopManagement.Kick, TroopManagement.Kick,
Heartbeat.Alive, Heartbeat.Alive,
PbMessageSvc.PbMsgWithDraw PbMessageSvc.PbMsgWithDraw,
MultiMsg.ApplyUp
) )
object IncomingFactories : List<IncomingPacketFactory<*>> by mutableListOf( object IncomingFactories : List<IncomingPacketFactory<*>> by mutableListOf(

View File

@ -17,6 +17,7 @@ import kotlinx.io.core.toByteArray
import kotlinx.io.core.writeFully import kotlinx.io.core.writeFully
import net.mamoe.mirai.qqandroid.network.protocol.LoginType import net.mamoe.mirai.qqandroid.network.protocol.LoginType
import net.mamoe.mirai.qqandroid.utils.NetworkType 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.MiraiInternalAPI
import net.mamoe.mirai.utils.MiraiPlatformUtils import net.mamoe.mirai.utils.MiraiPlatformUtils
import net.mamoe.mirai.utils.currentTimeMillis import net.mamoe.mirai.utils.currentTimeMillis

View File

@ -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=<size=${data.size}>, 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<ApplyUp.Response>("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}" })
}
}
}
}
}

View File

@ -126,7 +126,7 @@ internal class MessageSvc {
object EmptyResponse : GetMsgSuccess(emptyList()) 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 { 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 // 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()) val resp = readProtoBuf(MsgSvc.PbGetMsgResp.serializer())

View File

@ -38,7 +38,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.buildResponseUniPacket
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.debug import net.mamoe.mirai.utils.debug
import net.mamoe.mirai.utils.io.read 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 import net.mamoe.mirai.utils.io.toUHexString
internal class OnlinePush { internal class OnlinePush {

View File

@ -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.GuidSource
import net.mamoe.mirai.qqandroid.utils.MacOrAndroidIdChangeFlag import net.mamoe.mirai.qqandroid.utils.MacOrAndroidIdChangeFlag
import net.mamoe.mirai.qqandroid.utils.guidFlag import net.mamoe.mirai.qqandroid.utils.guidFlag
import net.mamoe.mirai.qqandroid.utils.io.*
import net.mamoe.mirai.utils.* 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.* import net.mamoe.mirai.utils.io.*
internal class WtLogin { internal class WtLogin {

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * 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 import net.mamoe.mirai.utils.io.chunkedHexToBytes

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * 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.core.ByteReadPacket
import kotlinx.io.pool.useInstance import kotlinx.io.pool.useInstance

View File

@ -11,7 +11,7 @@
@file:JvmMultifileClass @file:JvmMultifileClass
@file:JvmName("Utils") @file:JvmName("Utils")
package net.mamoe.mirai.utils.io package net.mamoe.mirai.qqandroid.utils.io
import kotlinx.io.OutputStream import kotlinx.io.OutputStream
import kotlinx.io.charsets.Charset import kotlinx.io.charsets.Charset
@ -27,6 +27,9 @@ import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic import kotlin.jvm.JvmSynthetic
import kotlinx.serialization.InternalSerializationApi 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) @OptIn(MiraiInternalAPI::class, InternalSerializationApi::class)
fun ByteReadPacket.copyTo(outputStream: OutputStream) { fun ByteReadPacket.copyTo(outputStream: OutputStream) {

View File

@ -11,12 +11,12 @@
@file:JvmMultifileClass @file:JvmMultifileClass
@file:JvmName("Utils") @file:JvmName("Utils")
package net.mamoe.mirai.utils.io package net.mamoe.mirai.qqandroid.utils.io
import kotlinx.io.core.* 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.MiraiInternalAPI
import net.mamoe.mirai.utils.coerceAtMostOrFail
import net.mamoe.mirai.utils.cryptor.TEA
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName 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 = inline fun BytePacketBuilder.writeIntLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
BytePacketBuilder().apply(builder).build().use { BytePacketBuilder().apply(builder).build().use {
if (tag != null) writeUByte(tag) if (tag != null) writeUByte(tag)
val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFL) val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFFFFFL)
writeInt(length.toInt()) writeInt(length.toInt())
writePacket(it) writePacket(it)
return length.toInt() 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 = inline fun BytePacketBuilder.writeShortLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
BytePacketBuilder().apply(builder).build().use { BytePacketBuilder().apply(builder).build().use {
if (tag != null) writeUByte(tag) if (tag != null) writeUByte(tag)
val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFL) val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFFFFFL)
writeUShort(length.toUShort()) writeUShort(length.toUShort())
writePacket(it) writePacket(it)
return length.toInt() return length.toInt()

View File

@ -10,7 +10,7 @@
@file:JvmMultifileClass @file:JvmMultifileClass
@file:JvmName("Utils") @file:JvmName("Utils")
package net.mamoe.mirai.utils package net.mamoe.mirai.qqandroid.utils
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
@ -18,8 +18,8 @@ import kotlin.jvm.JvmName
/** /**
* 要求 [this] 最小为 [min]. * 要求 [this] 最小为 [min].
*/ */
@Suppress("NOTHING_TO_INLINE") @PublishedApi
inline fun Int.coerceAtLeastOrFail(min: Int): Int { internal fun Int.coerceAtLeastOrFail(min: Int): Int {
require(this >= min) require(this >= min)
return this return this
} }
@ -27,8 +27,8 @@ inline fun Int.coerceAtLeastOrFail(min: Int): Int {
/** /**
* 要求 [this] 最小为 [min]. * 要求 [this] 最小为 [min].
*/ */
@Suppress("NOTHING_TO_INLINE") @PublishedApi
inline fun Long.coerceAtLeastOrFail(min: Long): Long { internal fun Long.coerceAtLeastOrFail(min: Long): Long {
require(this >= min) require(this >= min)
return this return this
} }
@ -36,12 +36,12 @@ inline fun Long.coerceAtLeastOrFail(min: Long): Long {
/** /**
* 要求 [this] 最大为 [max]. * 要求 [this] 最大为 [max].
*/ */
@Suppress("NOTHING_TO_INLINE") @PublishedApi
inline fun Int.coerceAtMostOrFail(max: Int): Int = internal fun Int.coerceAtMostOrFail(max: Int): Int =
if (this >= max) error("value is greater than its expected maximum value $max") if (this >= max) error("value is greater than its expected maximum value $max")
else this else this
@Suppress("NOTHING_TO_INLINE") @PublishedApi
inline fun Long.coerceAtMostOrFail(max: Long): Long = internal fun Long.coerceAtMostOrFail(max: Long): Long =
if (this >= max) error("value is greater than its expected maximum value $max") if (this >= max) error("value is greater than its expected maximum value $max")
else this else this

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * 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.MiraiInternalAPI
import net.mamoe.mirai.utils.MiraiPlatformUtils import net.mamoe.mirai.utils.MiraiPlatformUtils
@ -27,11 +27,13 @@ internal actual class ECDHKeyPairImpl(
override val privateKey: ECDHPrivateKey get() = delegate.private override val privateKey: ECDHPrivateKey get() = delegate.private
override val publicKey: ECDHPublicKey get() = delegate.public 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") @Suppress("FunctionName")
actual fun ECDH() = ECDH(ECDH.generateKeyPair()) actual fun ECDH() =
ECDH(ECDH.generateKeyPair())
actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
actual companion object { actual companion object {

View File

@ -21,6 +21,8 @@ import java.io.InputStream
import java.net.Inet4Address import java.net.Inet4Address
import java.security.MessageDigest import java.security.MessageDigest
import java.util.zip.Deflater import java.util.zip.Deflater
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import java.util.zip.Inflater import java.util.zip.Inflater
@ -63,6 +65,7 @@ actual object MiraiPlatformUtils {
} }
} }
actual fun md5(data: ByteArray, offset: Int, length: Int): ByteArray { actual fun md5(data: ByteArray, offset: Int, length: Int): ByteArray {
data.checkOffsetAndLength(offset, length) data.checkOffsetAndLength(offset, length)
return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest() return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest()
@ -99,4 +102,18 @@ actual object MiraiPlatformUtils {
block(read) 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() }
}
} }

View File

@ -13,6 +13,7 @@ import kotlinx.coroutines.Job
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.* import net.mamoe.mirai.data.*
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
@ -139,6 +140,14 @@ interface LowLevelBotAPIAccessor {
@LowLevelAPI @LowLevelAPI
@MiraiExperimentalAPI @MiraiExperimentalAPI
suspend fun _lowLevelGetGroupActiveData(groupId: Long): GroupActiveData suspend fun _lowLevelGetGroupActiveData(groupId: Long): GroupActiveData
/**
* 发送长消息
*/
@SinceMirai("0.31.0")
@LowLevelAPI
@MiraiExperimentalAPI
suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message)
} }
/** /**

View File

@ -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.equals
import net.mamoe.mirai.message.data.NullMessageChain.toString import net.mamoe.mirai.message.data.NullMessageChain.toString
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.SinceMirai
import kotlin.js.JsName import kotlin.js.JsName
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
@ -45,6 +46,12 @@ interface MessageChain : Message, Iterable<SingleMessage> {
override operator fun contains(sub: String): Boolean override operator fun contains(sub: String): Boolean
override fun toString(): String override fun toString(): String
/**
* 元素数量
*/
@SinceMirai("0.31.1")
val size: Int
/** /**
* 获取第一个类型为 [key] [Message] 实例 * 获取第一个类型为 [key] [Message] 实例
* *
@ -374,7 +381,6 @@ inline fun MessageChain.flatten(): Sequence<SingleMessage> = this.asSequence() /
// endregion converters // endregion converters
// region implementations
/** /**
* 不含任何元素的 [MessageChain] * 不含任何元素的 [MessageChain]
@ -389,19 +395,25 @@ object EmptyMessageChain : MessageChain by MessageChainImplByIterable(emptyList(
*/ */
object NullMessageChain : MessageChain { object NullMessageChain : MessageChain {
override fun toString(): String = "NullMessageChain" override fun toString(): String = "NullMessageChain"
override val size: Int get() = 0
override fun equals(other: Any?): Boolean = other === this override fun equals(other: Any?): Boolean = other === this
override fun contains(sub: String): Boolean = error("accessing NullMessageChain") override fun contains(sub: String): Boolean = error("accessing NullMessageChain")
override fun followedBy(tail: Message): CombinedMessage = CombinedMessage(left = EmptyMessageChain, tail = tail) override fun followedBy(tail: Message): CombinedMessage = CombinedMessage(left = EmptyMessageChain, tail = tail)
override fun iterator(): MutableIterator<SingleMessage> = error("accessing NullMessageChain") override fun iterator(): MutableIterator<SingleMessage> = error("accessing NullMessageChain")
} }
// region implementations
/** /**
* 使用 [Iterable] 作为委托的 [MessageChain] * 使用 [Iterable] 作为委托的 [MessageChain]
*/ */
@PublishedApi @PublishedApi
internal inline class MessageChainImplByIterable constructor( internal class MessageChainImplByIterable constructor(
private val delegate: Iterable<SingleMessage> private val delegate: Iterable<SingleMessage>
) : Message, Iterable<SingleMessage>, MessageChain { ) : Message, Iterable<SingleMessage>, MessageChain {
override val size: Int by lazy { delegate.count() }
override fun iterator(): Iterator<SingleMessage> = delegate.iterator() override fun iterator(): Iterator<SingleMessage> = delegate.iterator()
override fun toString(): String = this.delegate.joinToString("") { it.toString() } override fun toString(): String = this.delegate.joinToString("") { it.toString() }
override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) } override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) }
@ -411,9 +423,10 @@ internal inline class MessageChainImplByIterable constructor(
* 使用 [Collection] 作为委托的 [MessageChain] * 使用 [Collection] 作为委托的 [MessageChain]
*/ */
@PublishedApi @PublishedApi
internal inline class MessageChainImplByCollection constructor( internal class MessageChainImplByCollection constructor(
private val delegate: Collection<SingleMessage> private val delegate: Collection<SingleMessage>
) : Message, Iterable<SingleMessage>, MessageChain { ) : Message, Iterable<SingleMessage>, MessageChain {
override val size: Int get() = delegate.size
override fun iterator(): Iterator<SingleMessage> = delegate.iterator() override fun iterator(): Iterator<SingleMessage> = delegate.iterator()
override fun toString(): String = this.delegate.joinToString("") { it.toString() } override fun toString(): String = this.delegate.joinToString("") { it.toString() }
override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) } 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( internal class MessageChainImplBySequence constructor(
delegate: Sequence<SingleMessage> delegate: Sequence<SingleMessage>
) : Message, Iterable<SingleMessage>, MessageChain { ) : Message, Iterable<SingleMessage>, MessageChain {
override val size: Int by lazy { collected.size }
/** /**
* [Sequence] 可能只能消耗一遍, 因此需要先转为 [List] * [Sequence] 可能只能消耗一遍, 因此需要先转为 [List]
*/ */
private val collected: List<SingleMessage> by lazy { delegate.toList() } private val collected: List<SingleMessage> by lazy { delegate.toList() }
override fun iterator(): Iterator<SingleMessage> = collected.iterator() override fun iterator(): Iterator<SingleMessage> = collected.iterator()
override fun toString(): String = this.collected.joinToString("") { it.toString() } override fun toString(): String = this.collected.joinToString("") { it.toString() }
override operator fun contains(sub: String): Boolean = collected.any { it.contains(sub) } override operator fun contains(sub: String): Boolean = collected.any { it.contains(sub) }
@ -440,9 +454,10 @@ internal class MessageChainImplBySequence constructor(
* 单个 [SingleMessage] 作为 [MessageChain] * 单个 [SingleMessage] 作为 [MessageChain]
*/ */
@PublishedApi @PublishedApi
internal inline class SingleMessageChainImpl constructor( internal class SingleMessageChainImpl constructor(
private val delegate: SingleMessage private val delegate: SingleMessage
) : Message, Iterable<SingleMessage>, MessageChain { ) : Message, Iterable<SingleMessage>, MessageChain {
override val size: Int get() = 1
override fun toString(): String = this.delegate.toString() override fun toString(): String = this.delegate.toString()
override fun iterator(): Iterator<SingleMessage> = iterator { yield(delegate) } override fun iterator(): Iterator<SingleMessage> = iterator { yield(delegate) }
override operator fun contains(sub: String): Boolean = sub in delegate override operator fun contains(sub: String): Boolean = sub in delegate

View File

@ -15,7 +15,6 @@ package net.mamoe.mirai.message.data
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
import kotlin.jvm.JvmStatic
import kotlin.jvm.JvmSynthetic import kotlin.jvm.JvmSynthetic
/** /**
@ -28,26 +27,13 @@ class PlainText(val stringValue: String) :
Comparable<String> by stringValue, Comparable<String> by stringValue,
CharSequence by stringValue { CharSequence by stringValue {
@Suppress("unused")
constructor(charSequence: CharSequence) : this(charSequence.toString()) constructor(charSequence: CharSequence) : this(charSequence.toString())
override operator fun contains(sub: String): Boolean = sub in stringValue override operator fun contains(sub: String): Boolean = sub in stringValue
override fun toString(): String = stringValue override fun toString(): String = stringValue
companion object Key : Message.Key<PlainText> { companion object Key : Message.Key<PlainText>
@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)
}
}
} }
/** /**

View File

@ -13,6 +13,7 @@
package net.mamoe.mirai.message.data package net.mamoe.mirai.message.data
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.SinceMirai import net.mamoe.mirai.utils.SinceMirai
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
@ -33,6 +34,48 @@ interface RichMessage : MessageContent {
@SinceMirai("0.30.0") @SinceMirai("0.30.0")
companion object Templates : Message.Key<RichMessage> { 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 @MiraiExperimentalAPI
@SinceMirai("0.30.0") @SinceMirai("0.30.0")
fun share(url: String, title: String? = null, content: String? = null, coverUrl: String? = null): XmlMessage = 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 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 消息 * 构造一条 XML 消息
*/ */

View File

@ -11,6 +11,7 @@ package net.mamoe.mirai.utils
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.BotNetworkHandler import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.LoginFailedException
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.jvm.JvmStatic import kotlin.jvm.JvmStatic
@ -18,10 +19,33 @@ import kotlin.jvm.JvmStatic
* 验证码, 设备锁解决器 * 验证码, 设备锁解决器
*/ */
expect abstract class LoginSolver { expect abstract class LoginSolver {
/**
* 处理图片验证码.
* 返回 null 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
*/
abstract suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? 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? abstract suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String?
/**
* 处理不安全设备验证.
* 在处理完成后返回任意内容 (包含 `null`) 均视为处理成功.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止.
*
* @return 任意内容. 返回值保留以供未来更新.
* @throws LoginFailedException
*/
abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String?
companion object { companion object {
@ -38,10 +62,12 @@ expect open class BotConfiguration() {
* 日志记录器 * 日志记录器
*/ */
var botLoggerSupplier: ((Bot) -> MiraiLogger) var botLoggerSupplier: ((Bot) -> MiraiLogger)
/** /**
* 网络层日志构造器 * 网络层日志构造器
*/ */
var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger) var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger)
/** /**
* 设备信息覆盖. 默认使用随机的设备信息. * 设备信息覆盖. 默认使用随机的设备信息.
*/ */
@ -56,23 +82,28 @@ expect open class BotConfiguration() {
* 心跳周期. 过长会导致被服务器断开连接. * 心跳周期. 过长会导致被服务器断开连接.
*/ */
var heartbeatPeriodMillis: Long var heartbeatPeriodMillis: Long
/** /**
* 每次心跳时等待结果的时间. * 每次心跳时等待结果的时间.
* 一旦心跳超时, 整个网络服务将会重启 (将消耗约 5s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响. * 一旦心跳超时, 整个网络服务将会重启 (将消耗约 5s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
*/ */
var heartbeatTimeoutMillis: Long var heartbeatTimeoutMillis: Long
/** /**
* 心跳失败后的第一次重连前的等待时间. * 心跳失败后的第一次重连前的等待时间.
*/ */
var firstReconnectDelayMillis: Long var firstReconnectDelayMillis: Long
/** /**
* 重连失败后, 继续尝试的每次等待时间 * 重连失败后, 继续尝试的每次等待时间
*/ */
var reconnectPeriodMillis: Long var reconnectPeriodMillis: Long
/** /**
* 最多尝试多少次重连 * 最多尝试多少次重连
*/ */
var reconnectionRetryTimes: Int var reconnectionRetryTimes: Int
/** /**
* 验证码处理器 * 验证码处理器
*/ */

View File

@ -11,8 +11,6 @@
package net.mamoe.mirai.utils.io package net.mamoe.mirai.utils.io
import kotlinx.io.core.IoBuffer
import kotlinx.io.pool.ObjectPool
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextInt import kotlin.random.nextInt
@ -202,13 +200,3 @@ 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( (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 255
) shl 0) ) 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 }

View File

@ -14,7 +14,7 @@ package net.mamoe.mirai.utils
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
/** /**
* 时间戳 * 时间戳.
*/ */
expect val currentTimeMillis: Long 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 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 fun md5(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray

View File

@ -14,11 +14,16 @@ import net.mamoe.mirai.utils.io.encodeToString
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@OptIn(MiraiInternalAPI::class)
internal class PlatformUtilsTest { internal class PlatformUtilsTest {
@OptIn(MiraiInternalAPI::class)
@Test @Test
fun testZip() { fun testZip() {
assertEquals("test", MiraiPlatformUtils.unzip(MiraiPlatformUtils.zip("test".toByteArray())).encodeToString()) assertEquals("test", MiraiPlatformUtils.unzip(MiraiPlatformUtils.zip("test".toByteArray())).encodeToString())
} }
@Test
fun testGZip() {
assertEquals("test", MiraiPlatformUtils.ungzip(MiraiPlatformUtils.gzip("test".toByteArray())).encodeToString())
}
} }

View File

@ -22,6 +22,8 @@ import java.io.OutputStream
import java.net.Inet4Address import java.net.Inet4Address
import java.security.MessageDigest import java.security.MessageDigest
import java.util.zip.Deflater import java.util.zip.Deflater
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import java.util.zip.Inflater 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 { actual fun md5(data: ByteArray, offset: Int, length: Int): ByteArray {
data.checkOffsetAndLength(offset, length) data.checkOffsetAndLength(offset, length)
return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest() return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest()