Support long messages

This commit is contained in:
Him188 2020-03-29 02:26:59 +08:00
parent e30a1ea4b9
commit a7e9b151e4
17 changed files with 319 additions and 161 deletions

View File

@ -33,18 +33,20 @@ 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.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.MultiMsg import net.mamoe.mirai.qqandroid.network.protocol.data.proto.LongMsg
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.PbMessageSvc import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.*
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
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.math.absoluteValue
@ -367,27 +369,95 @@ internal abstract class QQAndroidBotBase constructor(
@LowLevelAPI @LowLevelAPI
@MiraiExperimentalAPI @MiraiExperimentalAPI
override suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message) { override suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message) {
val chain = message.asMessageChain()
check(chain.toString().length <= 3000 && chain.count { it is Image } <= 10) { "message is too large" }
val group = getGroup(groupCode)
val source = MessageSourceFromSendFriend( val source = MessageSourceFromSendFriend(
messageRandom = Random.nextInt().absoluteValue, messageRandom = Random.nextInt().absoluteValue,
senderId = client.uin, senderId = client.uin,
toUin = Group.calculateGroupUinByGroupCode(groupCode), toUin = Group.calculateGroupUinByGroupCode(groupCode),
time = currentTimeSeconds, time = currentTimeSeconds,
groupId = groupCode, groupId = groupCode,
originalMessage = message.asMessageChain(), originalMessage = chain,
sequenceId = 0 sequenceId = client.atomicNextMessageSequenceId()
// sourceMessage = message // sourceMessage = message
) )
// TODO: 2020/3/26 util 方法来添加单例元素 // TODO: 2020/3/26 util 方法来添加单例元素
val toSend = buildMessageChain { val toSend = buildMessageChain(chain) {
source.originalMessage.filter { it !is MessageSource }.forEach { source.originalMessage.forEach {
if (it !is MessageSource){
add(it) add(it)
} }
}
add(source) add(source)
} }
network.run { network.run {
val response = MultiMsg.ApplyUp.createForLongMessage(this@QQAndroidBotBase.client, toSend, groupCode) val data = toSend.calculateValidationDataForGroup(group)
.sendAndExpect<MultiMsg.ApplyUp.Response>()
val response =
MultiMsg.ApplyUp.createForLongMessage(
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.OK -> {
resId = response.resId
}
is MultiMsg.ApplyUp.Response.RequireUpload -> {
resId = response.proto.msgResid
val body = LongMsg.ReqBody(
subcmd = 1,
platformType = 9,
termType = 5,
msgUpReq = listOf(
LongMsg.MsgUpReq(
msgType = 3, // group
dstUin = Group.calculateGroupUinByGroupCode(groupCode),
msgId = 0,
msgUkey = response.proto.msgUkey,
needCache = 0,
storeType = 2,
msgContent = data.data
)
)
).toByteArray(LongMsg.ReqBody.serializer())
HighwayHelper.uploadImage(
client,
serverIp = response.proto.uint32UpIp!!.first().toIpV4AddressString(),
serverPort = response.proto.uint32UpPort!!.first(),
ticket = response.proto.msgSig, // 104
imageInput = body.toReadPacket(),
inputSize = body.size,
fileMd5 = MiraiPlatformUtils.md5(body),
commandId = 27 // long msg
)
}
}
group.sendMessage(
RichMessage.longMessage(
brief = toSend.joinToString {
when (it) {
is PlainText -> it.stringValue
is At -> it.toString()
else -> ""
}
},
resId = resId,
timeSeconds = source.time
)
)
println(response._miraiContentToString()) println(response._miraiContentToString())
} }
} }

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
@ -289,8 +291,9 @@ internal class GroupImpl(
source.startWaitingSequenceId(this) source.startWaitingSequenceId(this)
}.sendAndExpect() }.sendAndExpect()
if (response is MessageSvc.PbSendMsg.Response.Failed) { if (response is MessageSvc.PbSendMsg.Response.Failed) {
when (response.errorCode) { when (response.resultType) {
120 -> error("bot is being muted.") 120 -> error("bot is being muted.")
34 -> error("internal error: send message failed, illegal arguments: $response")
else -> error("send message failed: $response") else -> error("send message failed: $response")
} }
} }
@ -352,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

@ -234,11 +234,25 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
} }
} }
var longTextResId: String? = null
fun transformOneMessage(it: Message) { fun transformOneMessage(it: Message) {
if (it is RichMessage) { if (it is RichMessage) {
val content = MiraiPlatformUtils.zip(it.content.toByteArray()) val content = MiraiPlatformUtils.zip(it.content.toByteArray())
when (it) { 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( is LightApp -> elements.add(
ImMsgBody.Elem( ImMsgBody.Elem(
lightApp = ImMsgBody.LightAppElem( lightApp = ImMsgBody.LightAppElem(
@ -246,24 +260,13 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
) )
) )
) )
is MergedForwardedMessage -> {
elements.add(
ImMsgBody.Elem(
richMsg = ImMsgBody.RichMsg(
serviceId = 35,
template1 = byteArrayOf(1) + content
)
)
)
transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN) // required
}
else -> elements.add( else -> elements.add(
ImMsgBody.Elem( ImMsgBody.Elem(
richMsg = ImMsgBody.RichMsg( richMsg = ImMsgBody.RichMsg(
serviceId = when (it) { serviceId = when (it) {
is XmlMessage -> 60 is XmlMessage -> 60
is JsonMessage -> 1 is JsonMessage -> 1
is MergedForwardedMessage -> 35 // is MergedForwardedMessage -> 35
else -> error("unsupported RichMessage: ${it::class.simpleName}") else -> error("unsupported RichMessage: ${it::class.simpleName}")
}, },
template1 = byteArrayOf(1) + content template1 = byteArrayOf(1) + content
@ -296,8 +299,9 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
transformOneMessage(PlainText(" ")) transformOneMessage(PlainText(" "))
} }
} }
is QuoteReply, is QuoteReply, // already transformed above
is MessageSource, is MessageSource, // mirai only
is RichMessage, // already transformed above
-> { -> {
} }
@ -306,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
} }
@ -402,7 +420,7 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
last = element last = element
return@forEach return@forEach
} else { } else {
if (last is MergedForwardedMessage && element is PlainText) { if (last is LongMessage && element is PlainText) {
if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) { if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) {
last = element last = element
return@forEach return@forEach
@ -416,6 +434,15 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
} }
} }
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.")
}
/* /*
if (this.any<QuoteReply>()) { if (this.any<QuoteReply>()) {
var removed = false var removed = false
@ -464,7 +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(MergedForwardedMessage(content)) 35 -> message.add(
LongMessage(
content,
this.firstIsInstance<ImMsgBody.GeneralFlags>().longTextResid
)
)
else -> { else -> {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
MiraiLogger.debug { MiraiLogger.debug {

View File

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

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

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

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

@ -44,7 +44,7 @@ internal class MultiMsg : ProtoBuf {
@Serializable @Serializable
class MultiMsgApplyUpRsp( class MultiMsgApplyUpRsp(
@ProtoId(1) val result: Int = 0, @ProtoId(1) val result: Int = 0,
@ProtoId(2) val msgResid: ByteArray = EMPTY_BYTE_ARRAY, @ProtoId(2) val msgResid: String = "",
@ProtoId(3) val msgUkey: ByteArray = EMPTY_BYTE_ARRAY, @ProtoId(3) val msgUkey: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoId(4) val uint32UpIp: List<Int>? = null, @ProtoId(4) val uint32UpIp: List<Int>? = null,
@ProtoId(5) val uint32UpPort: List<Int>? = null, @ProtoId(5) val uint32UpPort: List<Int>? = null,

View File

@ -12,7 +12,7 @@
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat package net.mamoe.mirai.qqandroid.network.protocol.packet.chat
import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.QQAndroidBot import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
@ -29,6 +29,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgTransmit
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MultiMsg import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MultiMsg
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket 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.OutgoingPacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketLogger
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.MiraiPlatformUtils import net.mamoe.mirai.utils.MiraiPlatformUtils
@ -44,8 +45,8 @@ internal class MessageValidationData @OptIn(MiraiInternalAPI::class) constructor
} }
@OptIn(MiraiInternalAPI::class) @OptIn(MiraiInternalAPI::class)
internal fun MessageChain.calculateValidationData( internal fun MessageChain.calculateValidationDataForGroup(
bot: Bot group: Group
): MessageValidationData { ): MessageValidationData {
// top_package.akkv#method_42702 // top_package.akkv#method_42702
val source: MessageSource by this.orElse { error("internal error: calculateValidationData: cannot find MessageSource, chain=${this._miraiContentToString()}") } val source: MessageSource by this.orElse { error("internal error: calculateValidationData: cannot find MessageSource, chain=${this._miraiContentToString()}") }
@ -55,37 +56,39 @@ internal fun MessageChain.calculateValidationData(
} }
val richTextElems = this.toRichTextElems(source is MessageSourceFromSendGroup) val richTextElems = this.toRichTextElems(source is MessageSourceFromSendGroup)
.filterNot { it.generalFlags != null }
val msgTransmit = MsgTransmit.PbMultiMsgTransmit( val msgTransmit = MsgTransmit.PbMultiMsgTransmit(
msg = listOf( msg = listOf(
MsgComm.Msg( MsgComm.Msg(
msgHead = MsgComm.MsgHead( msgHead = MsgComm.MsgHead(
fromUin = source.senderId, fromUin = group.bot.uin,
msgSeq = source.sequenceId, msgSeq = source.sequenceId,
msgTime = source.time.toInt(), msgTime = source.time.toInt(),
msgUid = source.messageRandom.toLong(), // TODO: 2020/3/26 CHECK IT msgUid = 0x01000000000000000L or source.messageRandom.toLong(), // TODO: 2020/3/26 CHECK IT
mutiltransHead = MsgComm.MutilTransHead( mutiltransHead = MsgComm.MutilTransHead(
status = 0, status = 0,
msgId = 1 msgId = 1
), ),
msgType = 82, // troop msgType = 82, // troop
groupInfo = MsgComm.GroupInfo( groupInfo = MsgComm.GroupInfo(
groupCode = source.toUin, groupCode = group.id,
groupCard = bot.nick, groupCard = "Cinnamon"// group.botAsMember.nameCard, // Cinnamon
), ),
isSrcMsg = false
), ),
msgBody = ImMsgBody.MsgBody( msgBody = ImMsgBody.MsgBody(
richText = ImMsgBody.RichText( richText = ImMsgBody.RichText(
elems = richTextElems elems = richTextElems.toMutableList()
)
)
) )
),
),
) )
) )
val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer()) val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer())
return MessageValidationData(MiraiPlatformUtils.zip(bytes)) return MessageValidationData(MiraiPlatformUtils.gzip(bytes))
} }
/* /*
@ -154,18 +157,27 @@ Packet 20:02:50 : =======================共有 0 个包=======================
internal class MultiMsg { internal class MultiMsg {
object ApplyUp : OutgoingPacketFactory<ApplyUp.Response>("MultiMsg.ApplyUp") { object ApplyUp : OutgoingPacketFactory<ApplyUp.Response>("MultiMsg.ApplyUp") {
class Response( sealed class Response : Packet {
data class RequireUpload(
val proto: MultiMsg.MultiMsgApplyUpRsp val proto: MultiMsg.MultiMsgApplyUpRsp
) : Packet ) : Response() {
override fun toString(): String {
if (PacketLogger.isEnabled) {
return _miraiContentToString()
}
return "MultiMsg.ApplyUp.Response.RequireUpload(proto=$proto)"
}
}
fun createForLongMessage( object MessageTooLarge : Response()
client: QQAndroidClient,
message: MessageChain, data class OK(
dstUin: Long, // group uin val resId: String
): OutgoingPacket = createForLongMessage(client, message.calculateValidationData(client.bot), dstUin) ) : Response()
}
// captured from group // captured from group
private fun createForLongMessage( fun createForLongMessage(
client: QQAndroidClient, client: QQAndroidClient,
messageData: MessageValidationData, messageData: MessageValidationData,
dstUin: Long // group uin dstUin: Long // group uin
@ -173,21 +185,24 @@ internal class MultiMsg {
writeProtoBuf( writeProtoBuf(
MultiMsg.ReqBody.serializer(), MultiMsg.ReqBody.serializer(),
MultiMsg.ReqBody( MultiMsg.ReqBody(
subcmd = 1,
termType = 5,
platformType = 9,
netType = 3, // wifi=3, wap=5
buildVer = client.buildVer,
buType = 1, buType = 1,
buildVer = "8.2.0.1296",
multimsgApplyupReq = listOf( multimsgApplyupReq = listOf(
MultiMsg.MultiMsgApplyUpReq( MultiMsg.MultiMsgApplyUpReq(
applyId = 0, applyId = 0,
dstUin = dstUin, dstUin = dstUin,
msgMd5 = messageData.md5, msgMd5 = messageData.md5,
msgSize = messageData.data.size.toLong(), msgSize = messageData.data.size.toLong().also {
println("data.size = $it")
},
msgType = 3 // TODO 3 for group? msgType = 3 // TODO 3 for group?
) ),
) ),
netType = 3, // wifi=3, wap=5
platformType = 9,
subcmd = 1,
termType = 5,
reqChannelType = 0,
) )
) )
} }
@ -210,13 +225,18 @@ internal class MultiMsg {
} }
*/ */
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
val response = readProtoBuf(MultiMsg.MultiMsgApplyUpRsp.serializer()) val body = readProtoBuf(MultiMsg.RspBody.serializer())
check(response.result == 0) { val response = body.multimsgApplyupRsp!!.first()
kotlin.run { return when (response.result) {
0 -> Response.RequireUpload(response)
193 -> Response.MessageTooLarge
//1 -> Response.OK(resId = response.msgResid)
else -> {
error(kotlin.run {
println(response._miraiContentToString()) println(response._miraiContentToString())
}.let { "Protocol error: MultiMsg.ApplyUp failed with result ${response.result}" } }.let { "Protocol error: MultiMsg.ApplyUp failed with result ${response.result}" })
} }
return Response(response) }
} }
} }
} }

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

@ -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
@ -46,25 +47,33 @@ interface RichMessage : MessageContent {
* *
* @param brief 消息内容纯文本, 显示在图片的前面 * @param brief 消息内容纯文本, 显示在图片的前面
*/ */
@SinceMirai("0.31.0")
@OptIn(MiraiInternalAPI::class)
@MiraiExperimentalAPI @MiraiExperimentalAPI
fun longMessage(brief: String, resId: String, time: Long): XmlMessage { fun longMessage(brief: String, resId: String, timeSeconds: Long): RichMessage {
val limited: String = if (brief.length > 30) {
brief.take(30) + ""
} else {
brief
}
val template = """ val template = """
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?> <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<msg serviceID="35" templateID="1" action="viewMultiMsg" <msg serviceID="35" templateID="1" action="viewMultiMsg"
brief="$brief" brief="$limited"
m_resid="$resId" m_resid="$resId"
m_fileName="$time" sourceMsgId="0" url="" m_fileName="$timeSeconds" sourceMsgId="0" url=""
flag="3" adverSign="0" multiMsgFlag="1"> flag="3" adverSign="0" multiMsgFlag="1">
<item layout="1"> <item layout="1">
<title>$brief</title> <title>$limited</title>
<hr hidden="false" style="0"/> <hr hidden="false" style="0"/>
<summary>点击查看完整消息</summary> <summary>点击查看完整消息</summary>
</item> </item>
<source name="聊天记录" icon="" action="" appid="-1"/> <source name="聊天记录" icon="" action="" appid="-1"/>
</msg> </msg>
""" """.trimIndent()
return XmlMessage(template) return LongMessage(template, resId)
} }
@MiraiExperimentalAPI @MiraiExperimentalAPI
@ -142,11 +151,12 @@ class XmlMessage constructor(override val content: String) : RichMessage {
} }
/** /**
* 合并转发消息 * 消息
*/ */
@SinceMirai("0.31.0") @SinceMirai("0.31.0")
@MiraiExperimentalAPI @MiraiExperimentalAPI
class MergedForwardedMessage(override val content: String) : RichMessage { @MiraiInternalAPI
class LongMessage(override val content: String, val resId: String) : RichMessage {
companion object Key : Message.Key<XmlMessage> companion object Key : Message.Key<XmlMessage>
// serviceId = 35 // serviceId = 35

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

@ -18,6 +18,7 @@ import kotlin.jvm.JvmName
/** /**
* 要求 [this] 最小为 [min]. * 要求 [this] 最小为 [min].
*/ */
@PublishedApi
internal fun Int.coerceAtLeastOrFail(min: Int): Int { internal fun Int.coerceAtLeastOrFail(min: Int): Int {
require(this >= min) require(this >= min)
return this return this
@ -26,6 +27,7 @@ internal fun Int.coerceAtLeastOrFail(min: Int): Int {
/** /**
* 要求 [this] 最小为 [min]. * 要求 [this] 最小为 [min].
*/ */
@PublishedApi
internal fun Long.coerceAtLeastOrFail(min: Long): Long { internal fun Long.coerceAtLeastOrFail(min: Long): Long {
require(this >= min) require(this >= min)
return this return this
@ -34,10 +36,12 @@ internal fun Long.coerceAtLeastOrFail(min: Long): Long {
/** /**
* 要求 [this] 最大为 [max]. * 要求 [this] 最大为 [max].
*/ */
@PublishedApi
internal 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
@PublishedApi
internal 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

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