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
*/
package net.mamoe.mirai.utils.cryptor
package net.mamoe.mirai.qqandroid.utils.cryptor
import android.annotation.SuppressLint
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.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<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) {
is OnlineFriendImageImpl -> image.originUrl
is OnlineGroupImageImpl -> image.originUrl

View File

@ -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}")

View File

@ -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

View File

@ -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
/**

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.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

View File

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

View File

@ -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<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) {
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) {
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<ImMsgB
is QuoteReplyToSend -> {
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<ImMsgB
}
this.forEach(::transformOneMessage)
if (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
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<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
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 <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>()) {
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<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilder) {
this.forEach {
when {
@ -425,6 +491,12 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilde
when (it.richMsg.serviceId) {
1 -> message.add(JsonMessage(content))
60 -> message.add(XmlMessage(content))
35 -> message.add(
LongMessage(
content,
this.firstIsInstance<ImMsgBody.GeneralFlags>().longTextResid
)
)
else -> {
@Suppress("DEPRECATION")
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.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) {

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.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

View File

@ -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

View File

@ -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<ByteReadPacket> {
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,

View File

@ -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

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(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,

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.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 {

View File

@ -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?,

View File

@ -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<IncomingPacketFactory<*>> by mutableListOf(

View File

@ -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

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())
@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())

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.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 {

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.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 {

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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()

View File

@ -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

View File

@ -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 {

View File

@ -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() }
}
}

View File

@ -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)
}
/**

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.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<SingleMessage> {
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<SingleMessage> = 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<SingleMessage> = error("accessing NullMessageChain")
}
// region implementations
/**
* 使用 [Iterable] 作为委托的 [MessageChain]
*/
@PublishedApi
internal inline class MessageChainImplByIterable constructor(
internal class MessageChainImplByIterable constructor(
private val delegate: Iterable<SingleMessage>
) : Message, Iterable<SingleMessage>, MessageChain {
override val size: Int by lazy { delegate.count() }
override fun iterator(): Iterator<SingleMessage> = 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<SingleMessage>
) : Message, Iterable<SingleMessage>, MessageChain {
override val size: Int get() = delegate.size
override fun iterator(): Iterator<SingleMessage> = 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<SingleMessage>
) : Message, Iterable<SingleMessage>, MessageChain {
override val size: Int by lazy { collected.size }
/**
* [Sequence] 可能只能消耗一遍, 因此需要先转为 [List]
*/
private val collected: List<SingleMessage> by lazy { delegate.toList() }
override fun iterator(): Iterator<SingleMessage> = 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<SingleMessage>, MessageChain {
override val size: Int get() = 1
override fun toString(): String = this.delegate.toString()
override fun iterator(): Iterator<SingleMessage> = iterator { yield(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.JvmName
import kotlin.jvm.JvmStatic
import kotlin.jvm.JvmSynthetic
/**
@ -28,26 +27,13 @@ class PlainText(val stringValue: String) :
Comparable<String> 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<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)
}
}
companion object Key : Message.Key<PlainText>
}
/**

View File

@ -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 消息
*/

View File

@ -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
/**
* 验证码处理器
*/

View File

@ -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 }
) shl 0)

View File

@ -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

View File

@ -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())
}
}

View File

@ -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()