Merge remote-tracking branch 'origin/master'

# Conflicts:
#	mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt
#	mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/list/FriendListPacket.kt
This commit is contained in:
jiahua.liu 2020-01-31 18:10:29 +08:00
commit 06515bc0be
23 changed files with 405 additions and 219 deletions

View File

@ -5,7 +5,7 @@
[![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/) [![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/)
**[English](README-eng.md)** **[English](README-eng.md)**
平台 **TIM PC 和 QQ Android** 协议支持库. 平台 **TIM PC 和 QQ Android** 协议支持库.
纯 Kotlin 实现协议和支持框架,模块全部开源。 纯 Kotlin 实现协议和支持框架,模块全部开源。
目前可运行在 JVM 或 Android。 目前可运行在 JVM 或 Android。
@ -13,16 +13,16 @@
加入 Gitter, 或加入 QQ 群: 655057127 加入 Gitter, 或加入 QQ 群: 655057127
## Update log ## CHANGELOG
在 [Project](https://github.com/mamoe/mirai/projects/1) 查看已支持功能和计划(更新不及时) 在 [Project](https://github.com/mamoe/mirai/projects/1) 查看已支持功能和计划(更新不及时)
在 [UpdateLog](https://github.com/mamoe/mirai/blob/master/UpdateLog.md) 查看版本更新记录(准确更新发布的版本) 在 [CHANGELOG](https://github.com/mamoe/mirai/blob/master/CHANGELOG.md) 查看版本更新记录(准确更新发布的版本)
## Modules ## Modules
#### mirai-core ### mirai-core
通用 API 模块,一套 API 适配两套协议。 通用 API 模块,一套 API 适配两套协议。
**请参考此模块的 API** **请参考此模块的 API**
#### mirai-core-timpc ### mirai-core-timpc
TIM PC 2.3.2 版本2019 年 8 月)协议的实现,相较于 core仅新增少量 API. 详见 [README.md](mirai-core-timpc/) TIM PC 2.3.2 版本2019 年 8 月)协议的实现,相较于 core仅新增少量 API. 详见 [README.md](mirai-core-timpc/)
支持的功能: 支持的功能:
- 消息收发:图片文字复合消息,图片消息 - 消息收发:图片文字复合消息,图片消息
@ -30,17 +30,19 @@ TIM PC 2.3.2 版本2019 年 8 月)协议的实现,相较于 core
(目前不再更新此协议,请关注下文的安卓协议) (目前不再更新此协议,请关注下文的安卓协议)
#### mirai-core-qqandroid ### mirai-core-qqandroid
QQ for Android 8.2.0 版本2019 年 12 月)协议的实现,目前还未完成。 QQ for Android 8.2.0 版本2019 年 12 月)协议的实现,目前还未完成。
- 高兼容性:Mirai 协议仅含极少部分为硬编码,其余全部随官方方式动态生成 - 高兼容性:协议仅含极少部分为硬编码,其余全部随官方方式动态生成
- 高安全性密匙随机ECDH 动态计算硬件信息真机模拟Android 平台获取真机信息) - 高安全性密匙随机ECDH 动态计算
开发进度: 开发进度:
- 完成 密码登录 2020/1/23 - 完成 密码登录 2020/1/23
- 完成 群消息解析 (2020/1/25 - 完成 群消息解析 (2020/1/25
- 完成 图片验证码登录 (2020/1/26 - 完成 图片验证码登录 (2020/1/26
- 完成 设备锁登录 (2020/1/29) - 完成 "不安全"状态登录, 设备锁登录 (2020/1/29)
- 进行中 消息解析和发送 - 完成 群消息解析: 图片, 文字 (2020/1/31)
- 进行中 好友消息同步
- 进行中 好友列表, 群列表, 分组列表
- 进行中 图片上传和下载 - 进行中 图片上传和下载
## Use directly ## Use directly

View File

@ -8,12 +8,8 @@ import kotlinx.serialization.modules.EmptyModule
import kotlinx.serialization.modules.SerialModule import kotlinx.serialization.modules.SerialModule
import net.mamoe.mirai.qqandroid.io.JceStruct import net.mamoe.mirai.qqandroid.io.JceStruct
import net.mamoe.mirai.qqandroid.io.ProtoBuf import net.mamoe.mirai.qqandroid.io.ProtoBuf
import net.mamoe.mirai.qqandroid.network.protocol.packet.withUse
import net.mamoe.mirai.utils.io.readIoBuffer
import net.mamoe.mirai.utils.io.readString import net.mamoe.mirai.utils.io.readString
import net.mamoe.mirai.utils.io.toIoBuffer import net.mamoe.mirai.utils.io.toReadPacket
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@PublishedApi @PublishedApi
internal val CharsetGBK = Charset.forName("GBK") internal val CharsetGBK = Charset.forName("GBK")
@ -59,7 +55,7 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
override fun endEncode(desc: SerialDescriptor) { override fun endEncode(desc: SerialDescriptor) {
parentEncoder.writeHead(MAP, this.tag) parentEncoder.writeHead(MAP, this.tag)
parentEncoder.encodeTaggedInt(Int.STUB_FOR_PRIMITIVE_NUMBERS_GBK, count) parentEncoder.encodeTaggedInt(Int.STUB_FOR_PRIMITIVE_NUMBERS_GBK, count)
println(this.output.toByteArray().toUHexString()) // println(this.output.toByteArray().toUHexString())
parentEncoder.output.write(this.output.toByteArray()) parentEncoder.output.write(this.output.toByteArray())
}*/ }*/
@ -344,7 +340,7 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
* [KSerializer.serialize] * [KSerializer.serialize]
*/ */
override fun beginStructure(desc: SerialDescriptor, vararg typeParams: KSerializer<*>): CompositeDecoder { override fun beginStructure(desc: SerialDescriptor, vararg typeParams: KSerializer<*>): CompositeDecoder {
//println("beginStructure: desc=${desc.getClassName()}, typeParams: ${typeParams.contentToString()}") //// println("beginStructure: desc=${desc.getClassName()}, typeParams: ${typeParams.contentToString()}")
when (desc) { when (desc) {
// 由于 Byte 的数组有两种方式写入, 需特定读取器 // 由于 Byte 的数组有两种方式写入, 需特定读取器
ByteArraySerializer.descriptor -> { ByteArraySerializer.descriptor -> {
@ -383,11 +379,10 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
} }
} }
if (!input.input.endOfInput) { val tag = currentTagOrNull
val tag = currentTagOrNull val jceHead = input.peakHeadOrNull()
if (tag != null && input.peakHead().tag > tag) { if (tag != null && (jceHead == null || jceHead.tag > tag)) {
return NullReader(this.input) return NullReader(this.input)
}
} }
return super.beginStructure(desc, *typeParams) return super.beginStructure(desc, *typeParams)
@ -402,12 +397,13 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
} }
fun isTagOptional(tag: Int): Boolean { fun isTagOptional(tag: Int): Boolean {
return input.input.endOfInput || input.peakHead().tag > tag val head = input.peakHeadOrNull()
return input.isEndOfInput || head == null || head.tag > tag
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : Any> decodeNullableSerializableValue(deserializer: DeserializationStrategy<T?>): T? { override fun <T : Any> decodeNullableSerializableValue(deserializer: DeserializationStrategy<T?>): T? {
// println("decodeNullableSerializableValue: ${deserializer::class.qualifiedName}") // // println("decodeNullableSerializableValue: ${deserializer::class.qualifiedName}")
if (deserializer is NullReader) { if (deserializer is NullReader) {
return null return null
} }
@ -434,7 +430,7 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
else input.readByteArray(tag).toMutableList() as T else input.readByteArray(tag).toMutableList() as T
} }
val tag = currentTag val tag = currentTag
// println(tag) // // println(tag)
@Suppress("SENSELESS_COMPARISON") // false positive @Suppress("SENSELESS_COMPARISON") // false positive
if (input.skipToTagOrNull(tag) { if (input.skipToTagOrNull(tag) {
return deserializer.deserialize(JceListReader(input.readInt(0), input)) return deserializer.deserialize(JceListReader(input.readInt(0), input))
@ -492,7 +488,7 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T { override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
return decodeNullableSerializableValue(deserializer as DeserializationStrategy<Any?>) as? T return decodeNullableSerializableValue(deserializer as DeserializationStrategy<Any?>) as? T
?: error("value with tag $currentTagOrNull(by ${deserializer.getClassName()}) is not optional but cannot find") ?: error("value with tag $currentTagOrNull(by ${deserializer.getClassName()}) is not optional but cannot find. currentJceHead = ${input.currentJceHead}")
} }
} }
@ -500,33 +496,48 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
@UseExperimental(ExperimentalUnsignedTypes::class) @UseExperimental(ExperimentalUnsignedTypes::class)
internal inner class JceInput( internal inner class JceInput(
@PublishedApi @PublishedApi
internal val input: IoBuffer internal val input: ByteReadPacket,
maxReadSize: Long = input.remaining
) : Closeable { ) : Closeable {
override fun close() = IoBuffer.Pool.recycle(input) internal val leastRemaining = input.remaining - maxReadSize
internal val isEndOfInput: Boolean get() = input.remaining <= leastRemaining
internal var currentJceHead: JceHead? = input.doReadHead()
override fun close() = input.close()
internal fun peakHeadOrNull(): JceHead? = currentJceHead ?: readHeadOrNull()
internal fun peakHead(): JceHead = peakHeadOrNull() ?: error("no enough data to read head")
@PublishedApi @PublishedApi
internal fun readHead(): JceHead = input.readHead() ?: error("no enough data to read head") internal fun readHead(): JceHead = readHeadOrNull() ?: error("no enough data to read head")
@PublishedApi @PublishedApi
internal fun readHeadOrNull(): JceHead? = input.readHead() internal fun readHeadOrNull(): JceHead? = input.doReadHead()
@PublishedApi /**
internal fun peakHead(): JceHead = input.makeView().readHead() ?: error("no enough data to read head") * 读取下一个 head 存储到 [currentJceHead]
*/
@PublishedApi private fun ByteReadPacket.doReadHead(): JceHead? {
internal fun peakHeadOrNull(): JceHead? = input.makeView().readHead() if (isEndOfInput) {
currentJceHead = null
@Suppress("NOTHING_TO_INLINE") // 避免 stacktrace 出现两个 readHead // println("doReadHead: endOfInput")
private inline fun IoBuffer.readHead(): JceHead? { return null
if (endOfInput) return null }
val var2 = readUByte() val var2 = readUByte()
val type = var2 and 15u val type = var2 and 15u
var tag = var2.toUInt() shr 4 var tag = var2.toUInt() shr 4
if (tag == 15u) { if (tag == 15u) {
if (endOfInput) return null if (isEndOfInput) {
currentJceHead = null
// println("doReadHead: endOfInput2")
return null
}
tag = readUByte().toUInt() tag = readUByte().toUInt()
} }
return JceHead(tag = tag.toInt(), type = type.toByte()) currentJceHead = JceHead(tag = tag.toInt(), type = type.toByte())
// println("doReadHead: $currentJceHead")
return currentJceHead
} }
fun readBoolean(tag: Int): Boolean = readBooleanOrNull(tag) ?: error("cannot find tag $tag") fun readBoolean(tag: Int): Boolean = readBooleanOrNull(tag) ?: error("cannot find tag $tag")
@ -583,6 +594,7 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
LIST -> ByteArray(readInt(0)) { readByte(0) } LIST -> ByteArray(readInt(0)) { readByte(0) }
SIMPLE_LIST -> { SIMPLE_LIST -> {
val head = readHead() val head = readHead()
readHead()
check(head.type.toInt() == 0) { "type mismatch" } check(head.type.toInt() == 0) { "type mismatch" }
input.readBytes(readInt(0)) input.readBytes(readInt(0))
} }
@ -610,7 +622,7 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
input.readUInt().toInt().also { require(it in 1 until 104857600) { "bad string length: $it" } }, input.readUInt().toInt().also { require(it in 1 until 104857600) { "bad string length: $it" } },
charset = charset.kotlinCharset charset = charset.kotlinCharset
) )
else -> error("type mismatch: ${head.type}") else -> error("type mismatch: ${head.type}, expecting 6 or 7 (for string)")
} }
} }
@ -762,40 +774,51 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
return dumpAsPacket(serializer, obj).readBytes() return dumpAsPacket(serializer, obj).readBytes()
} }
/**
* 注意 close [packet]!!
*/
fun <T> load(deserializer: DeserializationStrategy<T>, packet: ByteReadPacket, length: Int = packet.remaining.toInt()): T { fun <T> load(deserializer: DeserializationStrategy<T>, packet: ByteReadPacket, length: Int = packet.remaining.toInt()): T {
packet.readIoBuffer(n = length).withUse { return JceDecoder(JceInput(packet, length.toLong())).decode(deserializer)
val decoder = JceDecoder(JceInput(this))
return decoder.decode(deserializer)
}
} }
override fun <T> load(deserializer: DeserializationStrategy<T>, bytes: ByteArray): T { override fun <T> load(deserializer: DeserializationStrategy<T>, bytes: ByteArray): T {
return bytes.toIoBuffer().withUse { return bytes.toReadPacket().use {
val decoder = JceDecoder(JceInput(this)) val decoder = JceDecoder(JceInput(it))
decoder.decode(deserializer) decoder.decode(deserializer)
} }
} }
} }
@UseExperimental(ExperimentalContracts::class)
internal inline fun <R> Jce.JceInput.skipToTagOrNull(tag: Int, block: (JceHead) -> R): R? { internal inline fun <R> Jce.JceInput.skipToTagOrNull(tag: Int, block: (JceHead) -> R): R? {
contract { // println("skipping to $tag start")
callsInPlace(block, kotlin.contracts.InvocationKind.UNKNOWN)
}
while (true) { while (true) {
if (this.input.endOfInput) { if (isEndOfInput) { // 读不了了
currentJceHead = null
// println("skipping to $tag: endOfInput")
return null return null
} }
val head = peakHead() var head = currentJceHead
if (head == null) { // 没有新的 head 了
head = readHeadOrNull() ?: return null
}
if (head.tag > tag) { if (head.tag > tag) {
// println("skipping to $tag: head.tag > tag")
return null return null
} }
readHead() // readHead()
if (head.tag == tag) { if (head.tag == tag) {
// readHeadOrNull()
currentJceHead = null
// println("skipping to $tag: run block")
return block(head) return block(head)
} else {
// println("skipping to $tag: tag not matching")
} }
// println("skipping to $tag: skipField")
this.skipField(head.type) this.skipField(head.type)
currentJceHead = readHeadOrNull()
} }
} }

View File

@ -0,0 +1,10 @@
# io.serialization
**序列化支持**
包含:
- QQ 的 JceStruct 相关的全自动序列化和反序列化: [Jce.kt](Jce.kt)
- Protocol Buffers 的 optional 支持: [ProtoBufWithNullableSupport.kt](ProtoBufWithNullableSupport.kt)
其中, ProtoBufWithNullableSupport.kt 的绝大部分源码来自 `kotlinx.serialization`. 原著权归该项目作者所有.
Mirai 所做的修改已经标记上了 `MIRAI MODIFY START`

View File

@ -11,7 +11,6 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestDataVersion3
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPacket import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPacket
import net.mamoe.mirai.utils.firstValue import net.mamoe.mirai.utils.firstValue
import net.mamoe.mirai.utils.io.read import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.toUHexString
fun <T : JceStruct> ByteArray.loadAs(deserializer: DeserializationStrategy<T>, c: JceCharset = JceCharset.UTF8): T { fun <T : JceStruct> ByteArray.loadAs(deserializer: DeserializationStrategy<T>, c: JceCharset = JceCharset.UTF8): T {
@ -22,7 +21,7 @@ fun <T : JceStruct> BytePacketBuilder.writeJceStruct(serializer: SerializationSt
this.writePacket(Jce.byCharSet(charset).dumpAsPacket(serializer, struct)) this.writePacket(Jce.byCharSet(charset).dumpAsPacket(serializer, struct))
} }
fun <T : JceStruct> ByteReadPacket.readRemainingAsJceStruct( fun <T : JceStruct> ByteReadPacket.readJceStruct(
serializer: DeserializationStrategy<T>, serializer: DeserializationStrategy<T>,
charset: JceCharset = JceCharset.UTF8, charset: JceCharset = JceCharset.UTF8,
length: Int = this.remaining.toInt() length: Int = this.remaining.toInt()
@ -37,7 +36,7 @@ fun <T : JceStruct> ByteReadPacket.decodeUniPacket(deserializer: Deserialization
return decodeUniRequestPacketAndDeserialize(name) { return decodeUniRequestPacketAndDeserialize(name) {
it.read { it.read {
discardExact(1) discardExact(1)
this.readRemainingAsJceStruct(deserializer, length = (this.remaining - 1).toInt()) this.readJceStruct(deserializer, length = (this.remaining - 1).toInt())
} }
} }
} }
@ -49,13 +48,13 @@ fun <T : ProtoBuf> ByteReadPacket.decodeUniPacket(deserializer: DeserializationS
return decodeUniRequestPacketAndDeserialize(name) { return decodeUniRequestPacketAndDeserialize(name) {
it.read { it.read {
discardExact(1) discardExact(1)
this.readRemainingAsProtoBuf(deserializer, (this.remaining - 1).toInt()) this.readProtoBuf(deserializer, (this.remaining - 1).toInt())
} }
} }
} }
fun <R> ByteReadPacket.decodeUniRequestPacketAndDeserialize(name: String? = null, block: (ByteArray) -> R): R { fun <R> ByteReadPacket.decodeUniRequestPacketAndDeserialize(name: String? = null, block: (ByteArray) -> R): R {
val request = this.readRemainingAsJceStruct(RequestPacket.serializer()) val request = this.readJceStruct(RequestPacket.serializer())
return block(if (name == null) when (request.iVersion.toInt()) { return block(if (name == null) when (request.iVersion.toInt()) {
2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.firstValue().firstValue() 2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.firstValue().firstValue()
@ -71,9 +70,7 @@ fun <R> ByteReadPacket.decodeUniRequestPacketAndDeserialize(name: String? = null
fun <T : JceStruct> T.toByteArray(serializer: SerializationStrategy<T>, c: JceCharset = JceCharset.GBK): ByteArray = Jce.byCharSet(c).dump(serializer, this) fun <T : JceStruct> T.toByteArray(serializer: SerializationStrategy<T>, c: JceCharset = JceCharset.GBK): ByteArray = Jce.byCharSet(c).dump(serializer, this)
fun <T : ProtoBuf> BytePacketBuilder.writeProtoBuf(serializer: SerializationStrategy<T>, v: T) { fun <T : ProtoBuf> BytePacketBuilder.writeProtoBuf(serializer: SerializationStrategy<T>, v: T) {
this.writeFully(v.toByteArray(serializer).also { this.writeFully(v.toByteArray(serializer))
println("发送 protobuf: ${it.toUHexString()}")
})
} }
/** /**
@ -93,7 +90,7 @@ fun <T : ProtoBuf> ByteArray.loadAs(deserializer: DeserializationStrategy<T>): T
/** /**
* load * load
*/ */
fun <T : ProtoBuf> ByteReadPacket.readRemainingAsProtoBuf(serializer: DeserializationStrategy<T>, length: Int = this.remaining.toInt()): T { fun <T : ProtoBuf> ByteReadPacket.readProtoBuf(serializer: DeserializationStrategy<T>, length: Int = this.remaining.toInt()): T {
return ProtoBufWithNullableSupport.load(serializer, this.readBytes(length)) return ProtoBufWithNullableSupport.load(serializer, this.readBytes(length))
} }

View File

@ -1,11 +1,12 @@
package net.mamoe.mirai.qqandroid.network package net.mamoe.mirai.qqandroid.network
import io.ktor.client.HttpClient
import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic import kotlinx.atomicfu.atomic
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.io.core.* import kotlinx.io.core.ByteReadPacket
import kotlinx.io.pool.ObjectPool import kotlinx.io.core.Input
import kotlinx.io.core.buildPacket
import kotlinx.io.core.use
import net.mamoe.mirai.data.MultiPacket import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.Packet import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.BroadcastControllable import net.mamoe.mirai.event.BroadcastControllable
@ -38,88 +39,82 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
override suspend fun login() { override suspend fun login() {
suspend fun doLogin() { if (::channel.isInitialized) {
channel = PlatformSocket() channel.close()
channel.connect("113.96.13.208", 8080) }
launch(CoroutineName("Incoming Packet Receiver")) { processReceive() } channel = PlatformSocket()
channel.connect("113.96.13.208", 8080)
launch(CoroutineName("Incoming Packet Receiver")) { processReceive() }
bot.logger.info("Trying login") // bot.logger.info("Trying login")
var response: LoginPacket.LoginPacketResponse = LoginPacket.SubCommand9(bot.client).sendAndExpect() var response: LoginPacket.LoginPacketResponse = LoginPacket.SubCommand9(bot.client).sendAndExpect()
mainloop@ while (true) { mainloop@ while (true) {
when (response) { when (response) {
is LoginPacket.LoginPacketResponse.UnsafeLogin -> { is LoginPacket.LoginPacketResponse.UnsafeLogin -> {
bot.configuration.loginSolver.onSolveUnsafeDeviceLoginVerify(bot, response.url) bot.configuration.loginSolver.onSolveUnsafeDeviceLoginVerify(bot, response.url)
response = LoginPacket.SubCommand9(bot.client).sendAndExpect() response = LoginPacket.SubCommand9(bot.client).sendAndExpect()
} }
is LoginPacket.LoginPacketResponse.Captcha -> when (response) { is LoginPacket.LoginPacketResponse.Captcha -> when (response) {
is LoginPacket.LoginPacketResponse.Captcha.Picture -> { is LoginPacket.LoginPacketResponse.Captcha.Picture -> {
var result = response.data.withUse { var result = response.data.withUse {
bot.configuration.loginSolver.onSolvePicCaptcha(bot, this) bot.configuration.loginSolver.onSolvePicCaptcha(bot, this)
}
if (result == null || result.length != 4) {
//refresh captcha
result = "ABCD"
}
response = LoginPacket.SubCommand2.SubmitPictureCaptcha(bot.client, response.sign, result).sendAndExpect()
continue@mainloop
} }
is LoginPacket.LoginPacketResponse.Captcha.Slider -> { if (result == null || result.length != 4) {
var ticket = bot.configuration.loginSolver.onSolveSliderCaptcha(bot, response.url) //refresh captcha
if (ticket == null) { result = "ABCD"
ticket = ""
}
response = LoginPacket.SubCommand2.SubmitSliderCaptcha(bot.client, ticket).sendAndExpect()
continue@mainloop
} }
} response = LoginPacket.SubCommand2.SubmitPictureCaptcha(bot.client, response.sign, result).sendAndExpect()
is LoginPacket.LoginPacketResponse.Error -> error(response.toString())
is LoginPacket.LoginPacketResponse.DeviceLockLogin -> {
response = LoginPacket.SubCommand20(
bot.client,
response.t402,
response.t403
).sendAndExpect()
continue@mainloop continue@mainloop
} }
is LoginPacket.LoginPacketResponse.Captcha.Slider -> {
is LoginPacket.LoginPacketResponse.Success -> { var ticket = bot.configuration.loginSolver.onSolveSliderCaptcha(bot, response.url)
bot.logger.info("Login successful") if (ticket == null) {
break@mainloop ticket = ""
}
response = LoginPacket.SubCommand2.SubmitSliderCaptcha(bot.client, ticket).sendAndExpect()
continue@mainloop
} }
} }
is LoginPacket.LoginPacketResponse.Error -> error(response.toString())
is LoginPacket.LoginPacketResponse.DeviceLockLogin -> {
response = LoginPacket.SubCommand20(
bot.client,
response.t402,
response.t403
).sendAndExpect()
continue@mainloop
}
is LoginPacket.LoginPacketResponse.Success -> {
bot.logger.info("Login successful")
break@mainloop
}
} }
println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>()
} }
suspend fun doInit() { println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
//start updating friend/group list StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>(6000)
bot.logger.info("Start updating friend/group list") }
/* override suspend fun init() {
val data = FriendList.GetFriendGroupList( //start updating friend/group list
bot.client, bot.logger.info("Start updating friend/group list")
0, /*
1, val data = FriendList.GetFriendGroupList(
0, bot.client,
2 0,
).sendAndExpect<FriendList.GetFriendGroupList.Response>() 1,
*/ 0,
2
val data = FriendList.GetTroopListSimplify( ).sendAndExpect<FriendList.GetFriendGroupList.Response>()
bot.client */
).sendAndExpect<FriendList.GetTroopListSimplify.Response>(100000) val data = FriendList.GetTroopList(
println(data.contentToString()) bot.client
).sendAndExpect<FriendList.GetTroopList.Response>()
println(data.contentToString())
}
doLogin()
doInit()
} }
@ -150,28 +145,10 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
/** /**
* [PacketProcessDispatcher] 调度器中解析包内容. * [PacketProcessDispatcher] 调度器中解析包内容.
* [input] 将会被 [ObjectPool.recycle].
*
* @param input 一个完整的包的内容, 去掉开头的 int 包长度
*/
fun parsePacketAsync(input: IoBuffer, pool: ObjectPool<IoBuffer> = IoBuffer.Pool): Job =
this.launch(PacketProcessDispatcher) {
try {
parsePacket(input)
} finally {
input.discard()
input.release(pool)
}
}
/**
* [PacketProcessDispatcher] 调度器中解析包内容.
* [input] 将会被 [Input.close], 因此 [input] 不能为 [IoBuffer]
* *
* @param input 一个完整的包的内容, 去掉开头的 int 包长度 * @param input 一个完整的包的内容, 去掉开头的 int 包长度
*/ */
fun parsePacketAsync(input: Input): Job { fun parsePacketAsync(input: Input): Job {
require(input !is IoBuffer) { "input cannot be IoBuffer" }
return this.launch(PacketProcessDispatcher) { return this.launch(PacketProcessDispatcher) {
input.use { parsePacket(it) } input.use { parsePacket(it) }
} }
@ -187,6 +164,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
generifiedParsePacket<Packet>(input) generifiedParsePacket<Packet>(input)
} }
// with generic type, less mistakes
private suspend inline fun <P : Packet> generifiedParsePacket(input: Input) { private suspend inline fun <P : Packet> generifiedParsePacket(input: Input) {
KnownPacketFactories.parseIncomingPacket(bot, input) { packetFactory: PacketFactory<P>, packet: P, commandName: String, sequenceId: Int -> KnownPacketFactories.parseIncomingPacket(bot, input) { packetFactory: PacketFactory<P>, packet: P, commandName: String, sequenceId: Int ->
handlePacket(packetFactory, packet, commandName, sequenceId) handlePacket(packetFactory, packet, commandName, sequenceId)
@ -237,7 +215,6 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
* 处理从服务器接收过来的包. 这些包可能是粘在一起的, 也可能是不完整的. 将会自动处理. * 处理从服务器接收过来的包. 这些包可能是粘在一起的, 也可能是不完整的. 将会自动处理.
* 处理后的包会调用 [parsePacketAsync] * 处理后的包会调用 [parsePacketAsync]
*/ */
@UseExperimental(ExperimentalCoroutinesApi::class)
internal fun processPacket(rawInput: ByteReadPacket) { internal fun processPacket(rawInput: ByteReadPacket) {
if (rawInput.remaining == 0L) { if (rawInput.remaining == 0L) {
return return
@ -255,7 +232,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
} }
// 循环所有完整的包 // 循环所有完整的包
while (rawInput.remaining > length) { while (rawInput.remaining > length) {
parsePacketAsync(rawInput.readIoBuffer(length)) parsePacketAsync(rawInput.readPacket(length))
length = rawInput.readInt() - 4 length = rawInput.readInt() - 4
} }
@ -381,5 +358,10 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
fun filter(commandName: String, sequenceId: Int) = this.commandName == commandName && this.sequenceId == sequenceId fun filter(commandName: String, sequenceId: Int) = this.commandName == commandName && this.sequenceId == sequenceId
} }
override fun dispose(cause: Throwable?) {
channel.close()
super.dispose(cause)
}
override suspend fun awaitDisconnection() = supervisor.join() override suspend fun awaitDisconnection() = supervisor.join()
} }

View File

@ -11,15 +11,15 @@ import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketLogger import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketLogger
import net.mamoe.mirai.qqandroid.network.protocol.packet.Tlv import net.mamoe.mirai.qqandroid.network.protocol.packet.Tlv
import net.mamoe.mirai.qqandroid.utils.* import net.mamoe.mirai.qqandroid.utils.Context
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.qqandroid.utils.DeviceInfo
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.qqandroid.utils.NetworkType
import net.mamoe.mirai.qqandroid.utils.SystemDeviceInfo
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.cryptor.ECDH import net.mamoe.mirai.utils.cryptor.ECDH
import net.mamoe.mirai.utils.cryptor.contentToString import net.mamoe.mirai.utils.cryptor.contentToString
import net.mamoe.mirai.utils.cryptor.decryptBy import net.mamoe.mirai.utils.cryptor.decryptBy
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.io.* import net.mamoe.mirai.utils.io.*
import net.mamoe.mirai.utils.unsafeWeakRef
/* /*
APP ID: APP ID:
@ -111,7 +111,7 @@ internal open class QQAndroidClient(
val protocolVersion: Short = 8001 val protocolVersion: Short = 8001
class C2cMessageSyncData { class C2cMessageSyncData {
var syncCookie = EMPTY_BYTE_ARRAY var syncCookie: ByteArray? = null
var pubAccountCookie = EMPTY_BYTE_ARRAY var pubAccountCookie = EMPTY_BYTE_ARRAY
var syncFlag: Int = 0 var syncFlag: Int = 0
var msgCtrlBuf: ByteArray = EMPTY_BYTE_ARRAY var msgCtrlBuf: ByteArray = EMPTY_BYTE_ARRAY
@ -174,7 +174,11 @@ internal open class QQAndroidClient(
lateinit var t104: ByteArray lateinit var t104: ByteArray
} }
class ReserveUinInfo( internal fun generateTgtgtKey(guid: ByteArray): ByteArray =
md5(getRandomByteArray(16) + guid)
internal class ReserveUinInfo(
val imgType: ByteArray, val imgType: ByteArray,
val imgFormat: ByteArray, val imgFormat: ByteArray,
val imgUrl: ByteArray val imgUrl: ByteArray
@ -184,7 +188,7 @@ class ReserveUinInfo(
} }
} }
class WFastLoginInfo( internal class WFastLoginInfo(
val outA1: ByteReadPacket, val outA1: ByteReadPacket,
var adUrl: String = "", var adUrl: String = "",
var iconUrl: String = "", var iconUrl: String = "",
@ -196,7 +200,7 @@ class WFastLoginInfo(
} }
} }
class WLoginSimpleInfo( internal class WLoginSimpleInfo(
val uin: Long, // uin val uin: Long, // uin
val face: Int, // ubyte actually val face: Int, // ubyte actually
val age: Int, // ubyte val age: Int, // ubyte
@ -212,7 +216,7 @@ class WLoginSimpleInfo(
} }
} }
class LoginExtraData( internal class LoginExtraData(
val uin: Long, val uin: Long,
val ip: ByteArray, val ip: ByteArray,
val time: Int, val time: Int,
@ -223,7 +227,7 @@ class LoginExtraData(
} }
} }
class WLoginSigInfo( internal class WLoginSigInfo(
val uin: Long, val uin: Long,
val encryptA1: ByteArray?, // sigInfo[0] val encryptA1: ByteArray?, // sigInfo[0]
val noPicSig: ByteArray?, // sigInfo[1] val noPicSig: ByteArray?, // sigInfo[1]
@ -275,24 +279,24 @@ class WLoginSigInfo(
} }
} }
class UserStSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime) internal class UserStSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class LSKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime) internal class LSKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class UserStWebSig(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime) internal class UserStWebSig(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class UserA8(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime) internal class UserA8(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class UserA5(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime) internal class UserA5(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class SKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime) internal class SKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class UserSig64(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime) internal class UserSig64(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class OpenKey(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime) internal class OpenKey(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class VKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime) internal class VKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class AccessToken(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime) internal class AccessToken(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class D2(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime) internal class D2(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class Sid(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime) internal class Sid(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class AqSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime) internal class AqSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class Pt4Token(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime) internal class Pt4Token(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
typealias PSKeyMap = MutableMap<String, PSKey> internal typealias PSKeyMap = MutableMap<String, PSKey>
typealias Pt4TokenMap = MutableMap<String, Pt4Token> internal typealias Pt4TokenMap = MutableMap<String, Pt4Token>
internal fun parsePSKeyMapAndPt4TokenMap(data: ByteArray, creationTime: Long, expireTime: Long, outPSKeyMap: PSKeyMap, outPt4TokenMap: Pt4TokenMap) = internal fun parsePSKeyMapAndPt4TokenMap(data: ByteArray, creationTime: Long, expireTime: Long, outPSKeyMap: PSKeyMap, outPt4TokenMap: Pt4TokenMap) =
data.read { data.read {
@ -308,17 +312,17 @@ internal fun parsePSKeyMapAndPt4TokenMap(data: ByteArray, creationTime: Long, ex
} }
} }
class PSKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime) internal class PSKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class WtSessionTicket(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime) internal class WtSessionTicket(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
open class KeyWithExpiry( internal open class KeyWithExpiry(
data: ByteArray, data: ByteArray,
creationTime: Long, creationTime: Long,
val expireTime: Long val expireTime: Long
) : KeyWithCreationTime(data, creationTime) ) : KeyWithCreationTime(data, creationTime)
open class KeyWithCreationTime( internal open class KeyWithCreationTime(
val data: ByteArray, val data: ByteArray,
val creationTime: Long val creationTime: Long
) )

View File

@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumberType import kotlinx.serialization.protobuf.ProtoNumberType
import kotlinx.serialization.protobuf.ProtoType import kotlinx.serialization.protobuf.ProtoType
import net.mamoe.mirai.qqandroid.io.ProtoBuf import net.mamoe.mirai.qqandroid.io.ProtoBuf
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
@Serializable @Serializable
@ -624,7 +625,7 @@ internal class ImMsgBody : ProtoBuf {
internal class NotOnlineImage( internal class NotOnlineImage(
@SerialId(1) val filePath: String = "", @SerialId(1) val filePath: String = "",
@SerialId(2) val fileLen: Int = 0, @SerialId(2) val fileLen: Int = 0,
@SerialId(3) val downloadPath: ByteArray = EMPTY_BYTE_ARRAY, @SerialId(3) val downloadPath: String = "",
@SerialId(4) val oldVerSendFile: ByteArray = EMPTY_BYTE_ARRAY, @SerialId(4) val oldVerSendFile: ByteArray = EMPTY_BYTE_ARRAY,
@SerialId(5) val imgType: Int = 0, @SerialId(5) val imgType: Int = 0,
@SerialId(6) val previewsImage: ByteArray = EMPTY_BYTE_ARRAY, @SerialId(6) val previewsImage: ByteArray = EMPTY_BYTE_ARRAY,
@ -653,6 +654,20 @@ internal class ImMsgBody : ProtoBuf {
@SerialId(29) val pbReserve: ByteArray = EMPTY_BYTE_ARRAY @SerialId(29) val pbReserve: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf ) : ProtoBuf
@Serializable // 非官方.
internal data class PbReserve(
@SerialId(1) val unknown1: Int = 1,
@SerialId(2) val unknown2: Int = 0,
@SerialId(6) val unknown3: Int = 0,
@SerialId(8) val hint: String = "[动画表情]",
@SerialId(10) val unknown5: Int = 0,
@SerialId(15) val unknwon6: Int = 5
) : ProtoBuf {
companion object {
val DEFAULT: ByteArray = PbReserve().toByteArray(serializer())
}
}
@Serializable @Serializable
internal class OnlineImage( internal class OnlineImage(
@SerialId(1) val guid: ByteArray = EMPTY_BYTE_ARRAY, @SerialId(1) val guid: ByteArray = EMPTY_BYTE_ARRAY,

View File

@ -0,0 +1,16 @@
package net.mamoe.mirai.qqandroid.network.protocol.data.proto
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import net.mamoe.mirai.qqandroid.io.ProtoBuf
@Serializable
class SyncCookie(
@SerialId(2) val time: Long,
@SerialId(3) val unknown1: Long = 2994099792,
@SerialId(4) val unknown2: Long = 3497826378,
@SerialId(5) val const1: Long = 1680172298,
@SerialId(6) val const2: Long = 2424173273,
@SerialId(7) val unknown3: Long = 83,
@SerialId(8) val unknown4: Long = 0
) : ProtoBuf

View File

@ -192,7 +192,7 @@ internal inline fun PacketFactory<*>.buildLoginOutgoingPacket(
}) })
} }
private val BRP_STUB = ByteReadPacket(EMPTY_BYTE_ARRAY) private inline val BRP_STUB get() = ByteReadPacket.Empty
/** /**
* The second outermost packet for login * The second outermost packet for login
@ -233,7 +233,8 @@ internal inline fun BytePacketBuilder.writeSsoPacket(
writeInt(subAppId.toInt()) writeInt(subAppId.toInt())
writeInt(subAppId.toInt()) writeInt(subAppId.toInt())
writeHex(unknownHex) writeHex(unknownHex)
if (extraData === BRP_STUB) { if (extraData === BRP_STUB || extraData.remaining == 0L) {
// fast-path
writeInt(0x04) writeInt(0x04)
} else { } else {
writeInt((extraData.remaining + 4).toInt()) writeInt((extraData.remaining + 4).toInt())

View File

@ -9,7 +9,8 @@ import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.qqandroid.QQAndroidBot import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.event.ForceOfflineEvent import net.mamoe.mirai.qqandroid.event.ForceOfflineEvent
import net.mamoe.mirai.qqandroid.io.serialization.decodeUniPacket import net.mamoe.mirai.qqandroid.io.serialization.decodeUniPacket
import net.mamoe.mirai.qqandroid.io.serialization.readRemainingAsProtoBuf 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.io.serialization.writeProtoBuf
import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPushForceOffline import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPushForceOffline
@ -17,6 +18,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPushNotify
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody 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.MsgComm
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgSvc import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgSvc
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SyncCookie
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.PacketFactory import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
@ -41,7 +43,7 @@ internal class MessageSvc {
override suspend fun QQAndroidBot.handle(packet: RequestPushNotify) { override suspend fun QQAndroidBot.handle(packet: RequestPushNotify) {
network.run { network.run {
PbGetMsg(client, packet).sendAndExpect<MultiPacket<FriendMessage>>() PbGetMsg(client, packet.stMsgInfo?.uMsgTime ?: 0).sendAndExpect<MultiPacket<FriendMessage>>()
} }
} }
} }
@ -56,7 +58,7 @@ internal class MessageSvc {
operator fun invoke( operator fun invoke(
client: QQAndroidClient, client: QQAndroidClient,
from: RequestPushNotify msgTime: Long //PbPushMsg.msg.msgHead.msgTime
): OutgoingPacket = buildOutgoingUniPacket( ): OutgoingPacket = buildOutgoingUniPacket(
client, client,
extraData = EXTRA_DATA.toReadPacket() extraData = EXTRA_DATA.toReadPacket()
@ -64,14 +66,16 @@ internal class MessageSvc {
writeProtoBuf( writeProtoBuf(
MsgSvc.PbGetMsgReq.serializer(), MsgSvc.PbGetMsgReq.serializer(),
MsgSvc.PbGetMsgReq( MsgSvc.PbGetMsgReq(
msgReqType = from.ctype.toInt(), msgReqType = 1, // from.ctype.toInt()
contextFlag = 1, contextFlag = 1,
rambleFlag = 0, rambleFlag = 0,
latestRambleNumber = 20, latestRambleNumber = 20,
otherRambleNumber = 3, otherRambleNumber = 3,
onlineSyncFlag = 1, onlineSyncFlag = 1,
whisperSessionId = 0,
// serverBuf = from.serverBuf ?: EMPTY_BYTE_ARRAY, // serverBuf = from.serverBuf ?: EMPTY_BYTE_ARRAY,
syncCookie = client.c2cMessageSync.syncCookie, syncCookie = client.c2cMessageSync.syncCookie
?: SyncCookie(msgTime).toByteArray(SyncCookie.serializer()).also { client.c2cMessageSync.syncCookie = it },
syncFlag = 1 syncFlag = 1
// syncFlag = client.c2cMessageSync.syncFlag, // syncFlag = client.c2cMessageSync.syncFlag,
//msgCtrlBuf = client.c2cMessageSync.msgCtrlBuf, //msgCtrlBuf = client.c2cMessageSync.msgCtrlBuf,
@ -83,7 +87,11 @@ internal class MessageSvc {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): MultiPacket<FriendMessage> { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): MultiPacket<FriendMessage> {
// 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
discardExact(4) discardExact(4)
val resp = readRemainingAsProtoBuf(MsgSvc.PbGetMsgResp.serializer()) val resp = readProtoBuf(MsgSvc.PbGetMsgResp.serializer())
if (resp.result != 0) {
return MultiPacket(emptyList())
}
bot.client.c2cMessageSync.syncCookie = resp.syncCookie bot.client.c2cMessageSync.syncCookie = resp.syncCookie
bot.client.c2cMessageSync.pubAccountCookie = resp.pubAccountCookie bot.client.c2cMessageSync.pubAccountCookie = resp.pubAccountCookie
@ -193,7 +201,7 @@ internal class MessageSvc {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
discardExact(4) discardExact(4)
val response = readRemainingAsProtoBuf(MsgSvc.PbSendMsgResp.serializer()) val response = readProtoBuf(MsgSvc.PbSendMsgResp.serializer())
return if (response.result == 0) { return if (response.result == 0) {
Response.SUCCESS Response.SUCCESS
} else { } else {

View File

@ -3,7 +3,10 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.list
import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.data.Packet import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.qqandroid.QQAndroidBot import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.io.serialization.* import net.mamoe.mirai.qqandroid.io.serialization.decodeUniPacket
import net.mamoe.mirai.qqandroid.io.serialization.jceRequestSBuffer
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
import net.mamoe.mirai.qqandroid.io.serialization.writeJceStruct
import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.GetFriendListReq import net.mamoe.mirai.qqandroid.network.protocol.data.jce.GetFriendListReq
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.GetFriendListResp import net.mamoe.mirai.qqandroid.network.protocol.data.jce.GetFriendListResp

View File

@ -20,7 +20,7 @@ import net.mamoe.mirai.utils.io.toReadPacket
import net.mamoe.mirai.utils.localIpAddress import net.mamoe.mirai.utils.localIpAddress
@Suppress("EnumEntryName") @Suppress("EnumEntryName")
enum class RegPushReason { internal enum class RegPushReason {
appRegister, appRegister,
createDefaultRegInfo, createDefaultRegInfo,
fillRegProxy, fillRegProxy,
@ -32,7 +32,7 @@ enum class RegPushReason {
unknown unknown
} }
class StatSvc { internal class StatSvc {
internal object Register : PacketFactory<Register.Response>("StatSvc.register") { internal object Register : PacketFactory<Register.Response>("StatSvc.register") {
internal object Response : Packet { internal object Response : Packet {

View File

@ -3,6 +3,7 @@ package net.mamoe.mirai.qqandroid.utils
import net.mamoe.mirai.data.ImageLink import net.mamoe.mirai.data.ImageLink
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.utils.io.hexToBytes
internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> { internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> {
@ -20,7 +21,16 @@ internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> {
elems.add( elems.add(
ImMsgBody.Elem( ImMsgBody.Elem(
notOnlineImage = ImMsgBody.NotOnlineImage( notOnlineImage = ImMsgBody.NotOnlineImage(
filePath = it.id.value filePath = it.id.value, // 错了, 应该是 2B23D705CAD1F2CF3710FE582692FCC4.jpg
fileLen = 1149, // 假的
downloadPath = it.id.value,
imgType = 1000, // 不确定
picMd5 = "2B 23 D7 05 CA D1 F2 CF 37 10 FE 58 26 92 FC C4".hexToBytes(),
picHeight = 66,
picWidth = 66,
resId = it.id.value,
bizType = 5,
pbReserve = ImMsgBody.PbReserve.DEFAULT // 可能还可以改变 `[动画表情]`
) )
) )
) )

View File

@ -1,8 +0,0 @@
package net.mamoe.mirai.qqandroid.utils
import net.mamoe.mirai.utils.io.getRandomByteArray
import net.mamoe.mirai.utils.md5
fun generateTgtgtKey(guid: ByteArray): ByteArray =
md5(getRandomByteArray(16) + guid)

View File

@ -20,7 +20,9 @@ class JceDecoderTest {
@SerialId(3) val int: Int = 123, @SerialId(3) val int: Int = 123,
@SerialId(4) val long: Long = 123, @SerialId(4) val long: Long = 123,
@SerialId(5) val float: Float = 123f, @SerialId(5) val float: Float = 123f,
@SerialId(6) val double: Double = 123.0 @SerialId(6) val double: Double = 123.0,
@SerialId(7) val byteArray: ByteArray = byteArrayOf(1, 2, 3),
@SerialId(8) val byteArray2: ByteArray = byteArrayOf(1, 2, 3)
) : JceStruct { ) : JceStruct {
override fun writeTo(output: JceOutput) = output.run { override fun writeTo(output: JceOutput) = output.run {
writeString(string, 0) writeString(string, 0)
@ -30,7 +32,78 @@ class JceDecoderTest {
writeLong(long, 4) writeLong(long, 4)
writeFloat(float, 5) writeFloat(float, 5)
writeDouble(double, 6) writeDouble(double, 6)
writeFully(byteArray, 7)
writeFully(byteArray2, 8)
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TestSimpleJceStruct
if (string != other.string) return false
if (byte != other.byte) return false
if (short != other.short) return false
if (int != other.int) return false
if (long != other.long) return false
if (float != other.float) return false
if (double != other.double) return false
if (!byteArray.contentEquals(other.byteArray)) return false
if (!byteArray2.contentEquals(other.byteArray2)) return false
return true
}
override fun hashCode(): Int {
var result = string.hashCode()
result = 31 * result + byte
result = 31 * result + short
result = 31 * result + int
result = 31 * result + long.hashCode()
result = 31 * result + float.hashCode()
result = 31 * result + double.hashCode()
result = 31 * result + byteArray.contentHashCode()
result = 31 * result + byteArray2.contentHashCode()
return result
}
}
@Test
fun testByteArray() {
@Serializable
data class TestByteArray(
@SerialId(0) val byteArray: ByteArray = byteArrayOf(1, 2, 3)
) : JceStruct {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TestByteArray
if (!byteArray.contentEquals(other.byteArray)) return false
return true
}
override fun hashCode(): Int {
return byteArray.contentHashCode()
}
}
assertEquals(
TestByteArray(),
TestByteArray().toByteArray(TestByteArray.serializer()).loadAs(TestByteArray.serializer())
)
}
@Test
fun testSimpleStruct() {
assertEquals(
TestSimpleJceStruct(),
TestSimpleJceStruct().toByteArray(TestSimpleJceStruct.serializer()).loadAs(TestSimpleJceStruct.serializer())
)
} }
@ -77,7 +150,7 @@ class JceDecoderTest {
@Test @Test
fun testNestedList() { fun testNestedList() {
@Serializable @Serializable
class TestNestedList( data class TestNestedList(
@SerialId(7) val array: List<List<Int>> = listOf(listOf(1, 2, 3), listOf(1, 2, 3), listOf(1, 2, 3)) @SerialId(7) val array: List<List<Int>> = listOf(listOf(1, 2, 3), listOf(1, 2, 3), listOf(1, 2, 3))
) : JceStruct ) : JceStruct
@ -133,6 +206,28 @@ class JceDecoderTest {
}.readBytes().loadAs(TestNestedMap.serializer()).map.entries.first().value.contentToString(), "{01=[0x0002(2)]}") }.readBytes().loadAs(TestNestedMap.serializer()).map.entries.first().value.contentToString(), "{01=[0x0002(2)]}")
} }
@Test
fun testMap3() {
@Serializable
class TestNestedMap(
@SerialId(7) val map: Map<Byte, ShortArray> = mapOf(1.toByte() to shortArrayOf(2))
) : JceStruct
assertEquals("{0x01(1)=[0x0002(2)]}", buildJcePacket {
writeMap(mapOf(1.toByte() to shortArrayOf(2)), 7)
}.readBytes().loadAs(TestNestedMap.serializer()).map.contentToString())
}
@Test
fun testNestedMap2() {
@Serializable
class TestNestedMap(
@SerialId(7) val map: Map<Int, Map<Byte, ShortArray>> = mapOf(1 to mapOf(1.toByte() to shortArrayOf(2)))
) : JceStruct
assertEquals(buildJcePacket {
writeMap(mapOf(1 to mapOf(1.toByte() to shortArrayOf(2))), 7)
}.readBytes().loadAs(TestNestedMap.serializer()).map.entries.first().value.contentToString(), "{0x01(1)=[0x0002(2)]}")
}
@Test @Test
fun testNullableEncode() { fun testNullableEncode() {
@ -186,6 +281,9 @@ class JceDecoderTest {
@SerialId(0) val innerStructList: List<TestSimpleJceStruct> @SerialId(0) val innerStructList: List<TestSimpleJceStruct>
) : JceStruct ) : JceStruct
println(buildJcePacket {
writeCollection(listOf(TestSimpleJceStruct(), TestSimpleJceStruct()), 0)
}.readBytes().loadAs(OuterStruct.serializer()).innerStructList.toString())
assertEquals( assertEquals(
buildJcePacket { buildJcePacket {
writeCollection(listOf(TestSimpleJceStruct(), TestSimpleJceStruct()), 0) writeCollection(listOf(TestSimpleJceStruct(), TestSimpleJceStruct()), 0)

View File

@ -55,6 +55,7 @@ internal class TIMPCBotNetworkHandler internal constructor(coroutineContext: Cor
private var heartbeatJob: Job? = null private var heartbeatJob: Job? = null
@MiraiInternalAPI
override suspend fun login() { override suspend fun login() {
TIMProtocol.SERVER_IP.shuffled().forEach { ip -> TIMProtocol.SERVER_IP.shuffled().forEach { ip ->

View File

@ -27,7 +27,8 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
@Suppress("CanBePrimaryConstructorProperty") // for logger @Suppress("CanBePrimaryConstructorProperty") // for logger
final override val account: BotAccount = account final override val account: BotAccount = account
@UseExperimental(RawAccountIdUse::class) @UseExperimental(RawAccountIdUse::class)
override val uin: Long get() = account.id override val uin: Long
get() = account.id
final override val logger: MiraiLogger by lazy { configuration.logger ?: DefaultLogger("Bot($uin)").also { configuration.logger = it } } final override val logger: MiraiLogger by lazy { configuration.logger ?: DefaultLogger("Bot($uin)").also { configuration.logger = it } }
init { init {
@ -98,16 +99,28 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
} }
_network = createNetworkHandler(this.coroutineContext) _network = createNetworkHandler(this.coroutineContext)
while (true){ loginLoop@ while (true) {
try { try {
return _network.login() _network.login()
} catch (e: Exception){ break@loginLoop
} catch (e: Exception) {
e.logStacktrace() e.logStacktrace()
_network.dispose(e) _network.dispose(e)
} }
logger.warning("Login failed. Retrying in 3s...") logger.warning("Login failed. Retrying in 3s...")
delay(3000) delay(3000)
} }
while (true) {
try {
return _network.init()
} catch (e: Exception) {
e.logStacktrace()
_network.dispose(e)
}
logger.warning("Init failed. Retrying in 3s...")
delay(3000)
}
} }
protected abstract fun createNetworkHandler(coroutineContext: CoroutineContext): N protected abstract fun createNetworkHandler(coroutineContext: CoroutineContext): N

View File

@ -157,7 +157,7 @@ class MessageSubscribersBuilder<T : MessagePacket<*, *>>(
ListeningFilter { !filter.invoke(this, it) || !another.filter.invoke(this, it) } ListeningFilter { !filter.invoke(this, it) || !another.filter.invoke(this, it) }
/** /**
* 启动时间监听. * 启动事件监听.
*/ */
// do not inline due to kotlin (1.3.61) bug: java.lang.IllegalAccessError // do not inline due to kotlin (1.3.61) bug: java.lang.IllegalAccessError
operator fun invoke(onEvent: MessageListener<T>): Listener<T> { operator fun invoke(onEvent: MessageListener<T>): Listener<T> {

View File

@ -14,7 +14,7 @@ fun Image(id: String) = Image(ImageId(id))
* 由接收消息时构建, 可直接发送 * 由接收消息时构建, 可直接发送
* *
* @param id 这个图片的 [ImageId] * @param id 这个图片的 [ImageId]
*/ */ // TODO: 2020/1/31 去掉 Image. 将 Image 改为 interface/class
inline class Image(inline val id: ImageId) : Message { inline class Image(inline val id: ImageId) : Message {
override fun toString(): String = "[${id.value}]" override fun toString(): String = "[${id.value}]"

View File

@ -47,6 +47,13 @@ abstract class BotNetworkHandler : CoroutineScope {
@MiraiInternalAPI @MiraiInternalAPI
abstract suspend fun login() abstract suspend fun login()
/**
* 初始化获取好友列表等值.
*/
@MiraiInternalAPI
open suspend fun init() {
}
/** /**
* 等待直到与服务器断开连接. 若未连接则立即返回 * 等待直到与服务器断开连接. 若未连接则立即返回
*/ */

View File

@ -54,6 +54,10 @@ fun ByteReadPacket.readIoBuffer(
n: Int = remaining.toInt()//not that safe but adequate n: Int = remaining.toInt()//not that safe but adequate
): IoBuffer = IoBuffer.Pool.borrow().also { this.readFully(it, n) } ): IoBuffer = IoBuffer.Pool.borrow().also { this.readFully(it, n) }
fun ByteReadPacket.readPacket(
n: Int = remaining.toInt()//not that safe but adequate
): ByteReadPacket = this.readBytes(n).toReadPacket()
fun ByteReadPacket.readIoBuffer(n: Short) = this.readIoBuffer(n.toInt()) fun ByteReadPacket.readIoBuffer(n: Short) = this.readIoBuffer(n.toInt())
fun Input.readIP(): String = buildString(4 + 3) { fun Input.readIP(): String = buildString(4 + 3) {

View File

@ -130,7 +130,7 @@ fun Bot.messageDSL() {
// 当消息中包含 "复读" 时 // 当消息中包含 "复读" 时
contains("复读") { val listener = (contains("复读1") or contains("复读2")) {
reply(message) reply(message)
} }