mirror of
https://github.com/mamoe/mirai.git
synced 2025-03-10 20:20:08 +08:00
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:
commit
06515bc0be
22
README.md
22
README.md
@ -5,7 +5,7 @@
|
|||||||
[](https://bintray.com/him188moe/mirai/mirai-core/)
|
[](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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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`
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
@ -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
|
||||||
)
|
)
|
@ -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,
|
||||||
|
@ -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
|
@ -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())
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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 // 可能还可以改变 `[动画表情]`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -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)
|
|
||||||
|
|
@ -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)
|
||||||
|
@ -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 ->
|
||||||
|
@ -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
|
||||||
|
@ -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> {
|
||||||
|
@ -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}]"
|
||||||
|
|
||||||
|
@ -47,6 +47,13 @@ abstract class BotNetworkHandler : CoroutineScope {
|
|||||||
@MiraiInternalAPI
|
@MiraiInternalAPI
|
||||||
abstract suspend fun login()
|
abstract suspend fun login()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化获取好友列表等值.
|
||||||
|
*/
|
||||||
|
@MiraiInternalAPI
|
||||||
|
open suspend fun init() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 等待直到与服务器断开连接. 若未连接则立即返回
|
* 等待直到与服务器断开连接. 若未连接则立即返回
|
||||||
*/
|
*/
|
||||||
|
@ -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) {
|
||||||
|
@ -130,7 +130,7 @@ fun Bot.messageDSL() {
|
|||||||
|
|
||||||
|
|
||||||
// 当消息中包含 "复读" 时
|
// 当消息中包含 "复读" 时
|
||||||
contains("复读") {
|
val listener = (contains("复读1") or contains("复读2")) {
|
||||||
reply(message)
|
reply(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user