mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-23 06:10:30 +08:00
Support long messages
This commit is contained in:
parent
e30a1ea4b9
commit
a7e9b151e4
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}")
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证码处理器
|
* 验证码处理器
|
||||||
*/
|
*/
|
||||||
|
@ -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 }
|
|
@ -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
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user