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/)
**[English](README-eng.md)**
平台 **TIM PC 和 QQ Android** 协议支持库.
平台 **TIM PC 和 QQ Android** 协议支持库.
纯 Kotlin 实现协议和支持框架,模块全部开源。
目前可运行在 JVM 或 Android。
@ -13,16 +13,16 @@
加入 Gitter, 或加入 QQ 群: 655057127
## Update log
## CHANGELOG
在 [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
#### mirai-core
### mirai-core
通用 API 模块,一套 API 适配两套协议。
**请参考此模块的 API**
#### mirai-core-timpc
### 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 月)协议的实现,目前还未完成。
- 高兼容性:Mirai 协议仅含极少部分为硬编码,其余全部随官方方式动态生成
- 高安全性密匙随机ECDH 动态计算硬件信息真机模拟Android 平台获取真机信息)
- 高兼容性:协议仅含极少部分为硬编码,其余全部随官方方式动态生成
- 高安全性密匙随机ECDH 动态计算
开发进度:
- 完成 密码登录 2020/1/23
- 完成 群消息解析 (2020/1/25
- 完成 图片验证码登录 (2020/1/26
- 完成 设备锁登录 (2020/1/29)
- 进行中 消息解析和发送
- 完成 "不安全"状态登录, 设备锁登录 (2020/1/29)
- 完成 群消息解析: 图片, 文字 (2020/1/31)
- 进行中 好友消息同步
- 进行中 好友列表, 群列表, 分组列表
- 进行中 图片上传和下载
## Use directly

View File

@ -8,12 +8,8 @@ import kotlinx.serialization.modules.EmptyModule
import kotlinx.serialization.modules.SerialModule
import net.mamoe.mirai.qqandroid.io.JceStruct
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.toIoBuffer
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import net.mamoe.mirai.utils.io.toReadPacket
@PublishedApi
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) {
parentEncoder.writeHead(MAP, this.tag)
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())
}*/
@ -344,7 +340,7 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
* [KSerializer.serialize]
*/
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) {
// 由于 Byte 的数组有两种方式写入, 需特定读取器
ByteArraySerializer.descriptor -> {
@ -383,11 +379,10 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
}
}
if (!input.input.endOfInput) {
val tag = currentTagOrNull
if (tag != null && input.peakHead().tag > tag) {
return NullReader(this.input)
}
val tag = currentTagOrNull
val jceHead = input.peakHeadOrNull()
if (tag != null && (jceHead == null || jceHead.tag > tag)) {
return NullReader(this.input)
}
return super.beginStructure(desc, *typeParams)
@ -402,12 +397,13 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
}
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")
override fun <T : Any> decodeNullableSerializableValue(deserializer: DeserializationStrategy<T?>): T? {
// println("decodeNullableSerializableValue: ${deserializer::class.qualifiedName}")
// // println("decodeNullableSerializableValue: ${deserializer::class.qualifiedName}")
if (deserializer is NullReader) {
return null
}
@ -434,7 +430,7 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
else input.readByteArray(tag).toMutableList() as T
}
val tag = currentTag
// println(tag)
// // println(tag)
@Suppress("SENSELESS_COMPARISON") // false positive
if (input.skipToTagOrNull(tag) {
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")
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): 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)
internal inner class JceInput(
@PublishedApi
internal val input: IoBuffer
internal val input: ByteReadPacket,
maxReadSize: Long = input.remaining
) : 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
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
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")
@PublishedApi
internal fun peakHeadOrNull(): JceHead? = input.makeView().readHead()
@Suppress("NOTHING_TO_INLINE") // 避免 stacktrace 出现两个 readHead
private inline fun IoBuffer.readHead(): JceHead? {
if (endOfInput) return null
/**
* 读取下一个 head 存储到 [currentJceHead]
*/
private fun ByteReadPacket.doReadHead(): JceHead? {
if (isEndOfInput) {
currentJceHead = null
// println("doReadHead: endOfInput")
return null
}
val var2 = readUByte()
val type = var2 and 15u
var tag = var2.toUInt() shr 4
if (tag == 15u) {
if (endOfInput) return null
if (isEndOfInput) {
currentJceHead = null
// println("doReadHead: endOfInput2")
return null
}
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")
@ -583,6 +594,7 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
LIST -> ByteArray(readInt(0)) { readByte(0) }
SIMPLE_LIST -> {
val head = readHead()
readHead()
check(head.type.toInt() == 0) { "type mismatch" }
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" } },
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()
}
/**
* 注意 close [packet]!!
*/
fun <T> load(deserializer: DeserializationStrategy<T>, packet: ByteReadPacket, length: Int = packet.remaining.toInt()): T {
packet.readIoBuffer(n = length).withUse {
val decoder = JceDecoder(JceInput(this))
return decoder.decode(deserializer)
}
return JceDecoder(JceInput(packet, length.toLong())).decode(deserializer)
}
override fun <T> load(deserializer: DeserializationStrategy<T>, bytes: ByteArray): T {
return bytes.toIoBuffer().withUse {
val decoder = JceDecoder(JceInput(this))
return bytes.toReadPacket().use {
val decoder = JceDecoder(JceInput(it))
decoder.decode(deserializer)
}
}
}
@UseExperimental(ExperimentalContracts::class)
internal inline fun <R> Jce.JceInput.skipToTagOrNull(tag: Int, block: (JceHead) -> R): R? {
contract {
callsInPlace(block, kotlin.contracts.InvocationKind.UNKNOWN)
}
// println("skipping to $tag start")
while (true) {
if (this.input.endOfInput) {
if (isEndOfInput) { // 读不了了
currentJceHead = null
// println("skipping to $tag: endOfInput")
return null
}
val head = peakHead()
var head = currentJceHead
if (head == null) { // 没有新的 head 了
head = readHeadOrNull() ?: return null
}
if (head.tag > tag) {
// println("skipping to $tag: head.tag > tag")
return null
}
readHead()
// readHead()
if (head.tag == tag) {
// readHeadOrNull()
currentJceHead = null
// println("skipping to $tag: run block")
return block(head)
} else {
// println("skipping to $tag: tag not matching")
}
// println("skipping to $tag: skipField")
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.utils.firstValue
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 {
@ -22,7 +21,7 @@ fun <T : JceStruct> BytePacketBuilder.writeJceStruct(serializer: SerializationSt
this.writePacket(Jce.byCharSet(charset).dumpAsPacket(serializer, struct))
}
fun <T : JceStruct> ByteReadPacket.readRemainingAsJceStruct(
fun <T : JceStruct> ByteReadPacket.readJceStruct(
serializer: DeserializationStrategy<T>,
charset: JceCharset = JceCharset.UTF8,
length: Int = this.remaining.toInt()
@ -37,7 +36,7 @@ fun <T : JceStruct> ByteReadPacket.decodeUniPacket(deserializer: Deserialization
return decodeUniRequestPacketAndDeserialize(name) {
it.read {
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) {
it.read {
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 {
val request = this.readRemainingAsJceStruct(RequestPacket.serializer())
val request = this.readJceStruct(RequestPacket.serializer())
return block(if (name == null) when (request.iVersion.toInt()) {
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 : ProtoBuf> BytePacketBuilder.writeProtoBuf(serializer: SerializationStrategy<T>, v: T) {
this.writeFully(v.toByteArray(serializer).also {
println("发送 protobuf: ${it.toUHexString()}")
})
this.writeFully(v.toByteArray(serializer))
}
/**
@ -93,7 +90,7 @@ fun <T : ProtoBuf> ByteArray.loadAs(deserializer: DeserializationStrategy<T>): T
/**
* 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))
}

View File

@ -1,11 +1,12 @@
package net.mamoe.mirai.qqandroid.network
import io.ktor.client.HttpClient
import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import kotlinx.io.core.*
import kotlinx.io.pool.ObjectPool
import kotlinx.io.core.ByteReadPacket
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.Packet
import net.mamoe.mirai.event.BroadcastControllable
@ -38,88 +39,82 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
override suspend fun login() {
suspend fun doLogin() {
channel = PlatformSocket()
channel.connect("113.96.13.208", 8080)
launch(CoroutineName("Incoming Packet Receiver")) { processReceive() }
if (::channel.isInitialized) {
channel.close()
}
channel = PlatformSocket()
channel.connect("113.96.13.208", 8080)
launch(CoroutineName("Incoming Packet Receiver")) { processReceive() }
bot.logger.info("Trying login")
var response: LoginPacket.LoginPacketResponse = LoginPacket.SubCommand9(bot.client).sendAndExpect()
mainloop@ while (true) {
when (response) {
is LoginPacket.LoginPacketResponse.UnsafeLogin -> {
bot.configuration.loginSolver.onSolveUnsafeDeviceLoginVerify(bot, response.url)
response = LoginPacket.SubCommand9(bot.client).sendAndExpect()
}
// bot.logger.info("Trying login")
var response: LoginPacket.LoginPacketResponse = LoginPacket.SubCommand9(bot.client).sendAndExpect()
mainloop@ while (true) {
when (response) {
is LoginPacket.LoginPacketResponse.UnsafeLogin -> {
bot.configuration.loginSolver.onSolveUnsafeDeviceLoginVerify(bot, response.url)
response = LoginPacket.SubCommand9(bot.client).sendAndExpect()
}
is LoginPacket.LoginPacketResponse.Captcha -> when (response) {
is LoginPacket.LoginPacketResponse.Captcha.Picture -> {
var result = response.data.withUse {
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 -> when (response) {
is LoginPacket.LoginPacketResponse.Captcha.Picture -> {
var result = response.data.withUse {
bot.configuration.loginSolver.onSolvePicCaptcha(bot, this)
}
is LoginPacket.LoginPacketResponse.Captcha.Slider -> {
var ticket = bot.configuration.loginSolver.onSolveSliderCaptcha(bot, response.url)
if (ticket == null) {
ticket = ""
}
response = LoginPacket.SubCommand2.SubmitSliderCaptcha(bot.client, ticket).sendAndExpect()
continue@mainloop
if (result == null || result.length != 4) {
//refresh captcha
result = "ABCD"
}
}
is LoginPacket.LoginPacketResponse.Error -> error(response.toString())
is LoginPacket.LoginPacketResponse.DeviceLockLogin -> {
response = LoginPacket.SubCommand20(
bot.client,
response.t402,
response.t403
).sendAndExpect()
response = LoginPacket.SubCommand2.SubmitPictureCaptcha(bot.client, response.sign, result).sendAndExpect()
continue@mainloop
}
is LoginPacket.LoginPacketResponse.Success -> {
bot.logger.info("Login successful")
break@mainloop
is LoginPacket.LoginPacketResponse.Captcha.Slider -> {
var ticket = bot.configuration.loginSolver.onSolveSliderCaptcha(bot, response.url)
if (ticket == null) {
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() {
//start updating friend/group list
bot.logger.info("Start updating friend/group list")
println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>(6000)
}
/*
val data = FriendList.GetFriendGroupList(
bot.client,
0,
1,
0,
2
).sendAndExpect<FriendList.GetFriendGroupList.Response>()
*/
val data = FriendList.GetTroopListSimplify(
bot.client
).sendAndExpect<FriendList.GetTroopListSimplify.Response>(100000)
println(data.contentToString())
}
doLogin()
doInit()
override suspend fun init() {
//start updating friend/group list
bot.logger.info("Start updating friend/group list")
/*
val data = FriendList.GetFriendGroupList(
bot.client,
0,
1,
0,
2
).sendAndExpect<FriendList.GetFriendGroupList.Response>()
*/
val data = FriendList.GetTroopList(
bot.client
).sendAndExpect<FriendList.GetTroopList.Response>()
println(data.contentToString())
}
@ -150,28 +145,10 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
/**
* [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 包长度
*/
fun parsePacketAsync(input: Input): Job {
require(input !is IoBuffer) { "input cannot be IoBuffer" }
return this.launch(PacketProcessDispatcher) {
input.use { parsePacket(it) }
}
@ -187,6 +164,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
generifiedParsePacket<Packet>(input)
}
// with generic type, less mistakes
private suspend inline fun <P : Packet> generifiedParsePacket(input: Input) {
KnownPacketFactories.parseIncomingPacket(bot, input) { packetFactory: PacketFactory<P>, packet: P, commandName: String, sequenceId: Int ->
handlePacket(packetFactory, packet, commandName, sequenceId)
@ -237,7 +215,6 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
* 处理从服务器接收过来的包. 这些包可能是粘在一起的, 也可能是不完整的. 将会自动处理.
* 处理后的包会调用 [parsePacketAsync]
*/
@UseExperimental(ExperimentalCoroutinesApi::class)
internal fun processPacket(rawInput: ByteReadPacket) {
if (rawInput.remaining == 0L) {
return
@ -255,7 +232,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
}
// 循环所有完整的包
while (rawInput.remaining > length) {
parsePacketAsync(rawInput.readIoBuffer(length))
parsePacketAsync(rawInput.readPacket(length))
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
}
override fun dispose(cause: Throwable?) {
channel.close()
super.dispose(cause)
}
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.PacketLogger
import net.mamoe.mirai.qqandroid.network.protocol.packet.Tlv
import net.mamoe.mirai.qqandroid.utils.*
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.qqandroid.utils.Context
import net.mamoe.mirai.qqandroid.utils.DeviceInfo
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.contentToString
import net.mamoe.mirai.utils.cryptor.decryptBy
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.io.*
import net.mamoe.mirai.utils.unsafeWeakRef
/*
APP ID:
@ -111,7 +111,7 @@ internal open class QQAndroidClient(
val protocolVersion: Short = 8001
class C2cMessageSyncData {
var syncCookie = EMPTY_BYTE_ARRAY
var syncCookie: ByteArray? = null
var pubAccountCookie = EMPTY_BYTE_ARRAY
var syncFlag: Int = 0
var msgCtrlBuf: ByteArray = EMPTY_BYTE_ARRAY
@ -174,7 +174,11 @@ internal open class QQAndroidClient(
lateinit var t104: ByteArray
}
class ReserveUinInfo(
internal fun generateTgtgtKey(guid: ByteArray): ByteArray =
md5(getRandomByteArray(16) + guid)
internal class ReserveUinInfo(
val imgType: ByteArray,
val imgFormat: ByteArray,
val imgUrl: ByteArray
@ -184,7 +188,7 @@ class ReserveUinInfo(
}
}
class WFastLoginInfo(
internal class WFastLoginInfo(
val outA1: ByteReadPacket,
var adUrl: String = "",
var iconUrl: String = "",
@ -196,7 +200,7 @@ class WFastLoginInfo(
}
}
class WLoginSimpleInfo(
internal class WLoginSimpleInfo(
val uin: Long, // uin
val face: Int, // ubyte actually
val age: Int, // ubyte
@ -212,7 +216,7 @@ class WLoginSimpleInfo(
}
}
class LoginExtraData(
internal class LoginExtraData(
val uin: Long,
val ip: ByteArray,
val time: Int,
@ -223,7 +227,7 @@ class LoginExtraData(
}
}
class WLoginSigInfo(
internal class WLoginSigInfo(
val uin: Long,
val encryptA1: ByteArray?, // sigInfo[0]
val noPicSig: ByteArray?, // sigInfo[1]
@ -275,24 +279,24 @@ class WLoginSigInfo(
}
}
class UserStSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class LSKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class UserStWebSig(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class UserA8(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class UserA5(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class SKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class UserSig64(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class OpenKey(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class VKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class AccessToken(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
class D2(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class Sid(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
class AqSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class UserStSig(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class LSKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
internal class UserStWebSig(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
internal class UserA8(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
internal class UserA5(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class SKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
internal class UserSig64(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class OpenKey(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class VKey(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
internal class AccessToken(data: ByteArray, creationTime: Long) : KeyWithCreationTime(data, creationTime)
internal class D2(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
internal class Sid(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
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>
typealias Pt4TokenMap = MutableMap<String, Pt4Token>
internal typealias PSKeyMap = MutableMap<String, PSKey>
internal typealias Pt4TokenMap = MutableMap<String, Pt4Token>
internal fun parsePSKeyMapAndPt4TokenMap(data: ByteArray, creationTime: Long, expireTime: Long, outPSKeyMap: PSKeyMap, outPt4TokenMap: Pt4TokenMap) =
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,
creationTime: Long,
val expireTime: Long
) : KeyWithCreationTime(data, creationTime)
open class KeyWithCreationTime(
internal open class KeyWithCreationTime(
val data: ByteArray,
val creationTime: Long
)

View File

@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumberType
import kotlinx.serialization.protobuf.ProtoType
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
@Serializable
@ -624,7 +625,7 @@ internal class ImMsgBody : ProtoBuf {
internal class NotOnlineImage(
@SerialId(1) val filePath: String = "",
@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(5) val imgType: Int = 0,
@SerialId(6) val previewsImage: ByteArray = EMPTY_BYTE_ARRAY,
@ -653,6 +654,20 @@ internal class ImMsgBody : ProtoBuf {
@SerialId(29) val pbReserve: ByteArray = EMPTY_BYTE_ARRAY
) : 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
internal class OnlineImage(
@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
@ -233,7 +233,8 @@ internal inline fun BytePacketBuilder.writeSsoPacket(
writeInt(subAppId.toInt())
writeInt(subAppId.toInt())
writeHex(unknownHex)
if (extraData === BRP_STUB) {
if (extraData === BRP_STUB || extraData.remaining == 0L) {
// fast-path
writeInt(0x04)
} else {
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.event.ForceOfflineEvent
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.network.QQAndroidClient
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.MsgComm
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.PacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
@ -41,7 +43,7 @@ internal class MessageSvc {
override suspend fun QQAndroidBot.handle(packet: RequestPushNotify) {
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(
client: QQAndroidClient,
from: RequestPushNotify
msgTime: Long //PbPushMsg.msg.msgHead.msgTime
): OutgoingPacket = buildOutgoingUniPacket(
client,
extraData = EXTRA_DATA.toReadPacket()
@ -64,14 +66,16 @@ internal class MessageSvc {
writeProtoBuf(
MsgSvc.PbGetMsgReq.serializer(),
MsgSvc.PbGetMsgReq(
msgReqType = from.ctype.toInt(),
msgReqType = 1, // from.ctype.toInt()
contextFlag = 1,
rambleFlag = 0,
latestRambleNumber = 20,
otherRambleNumber = 3,
onlineSyncFlag = 1,
whisperSessionId = 0,
// 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 = client.c2cMessageSync.syncFlag,
//msgCtrlBuf = client.c2cMessageSync.msgCtrlBuf,
@ -83,7 +87,11 @@ internal class MessageSvc {
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
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.pubAccountCookie = resp.pubAccountCookie
@ -193,7 +201,7 @@ internal class MessageSvc {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
discardExact(4)
val response = readRemainingAsProtoBuf(MsgSvc.PbSendMsgResp.serializer())
val response = readProtoBuf(MsgSvc.PbSendMsgResp.serializer())
return if (response.result == 0) {
Response.SUCCESS
} else {

View File

@ -3,7 +3,10 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.list
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.data.Packet
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.protocol.data.jce.GetFriendListReq
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
@Suppress("EnumEntryName")
enum class RegPushReason {
internal enum class RegPushReason {
appRegister,
createDefaultRegInfo,
fillRegProxy,
@ -32,7 +32,7 @@ enum class RegPushReason {
unknown
}
class StatSvc {
internal class StatSvc {
internal object Register : PacketFactory<Register.Response>("StatSvc.register") {
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.message.data.*
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.utils.io.hexToBytes
internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> {
@ -20,7 +21,16 @@ internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> {
elems.add(
ImMsgBody.Elem(
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(4) val long: Long = 123,
@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 {
override fun writeTo(output: JceOutput) = output.run {
writeString(string, 0)
@ -30,7 +32,78 @@ class JceDecoderTest {
writeLong(long, 4)
writeFloat(float, 5)
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
fun testNestedList() {
@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))
) : JceStruct
@ -133,6 +206,28 @@ class JceDecoderTest {
}.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
fun testNullableEncode() {
@ -186,6 +281,9 @@ class JceDecoderTest {
@SerialId(0) val innerStructList: List<TestSimpleJceStruct>
) : JceStruct
println(buildJcePacket {
writeCollection(listOf(TestSimpleJceStruct(), TestSimpleJceStruct()), 0)
}.readBytes().loadAs(OuterStruct.serializer()).innerStructList.toString())
assertEquals(
buildJcePacket {
writeCollection(listOf(TestSimpleJceStruct(), TestSimpleJceStruct()), 0)

View File

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

View File

@ -27,7 +27,8 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
@Suppress("CanBePrimaryConstructorProperty") // for logger
final override val account: BotAccount = account
@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 } }
init {
@ -98,16 +99,28 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
}
_network = createNetworkHandler(this.coroutineContext)
while (true){
loginLoop@ while (true) {
try {
return _network.login()
} catch (e: Exception){
_network.login()
break@loginLoop
} catch (e: Exception) {
e.logStacktrace()
_network.dispose(e)
}
logger.warning("Login failed. Retrying in 3s...")
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

View File

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

View File

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

View File

@ -47,6 +47,13 @@ abstract class BotNetworkHandler : CoroutineScope {
@MiraiInternalAPI
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
): 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 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)
}