Commit 06515bc0 authored by jiahua.liu's avatar jiahua.liu

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
parents 0b01fd7b b85c91b2
......@@ -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
......
# io.serialization
**序列化支持**
包含:
- QQ 的 JceStruct 相关的全自动序列化和反序列化: [Jce.kt](Jce.kt)
- Protocol Buffers 的 optional 支持: [ProtoBufWithNullableSupport.kt](ProtoBufWithNullableSupport.kt)
其中, ProtoBufWithNullableSupport.kt 的绝大部分源码来自 `kotlinx.serialization`. 原著权归该项目作者所有.
Mirai 所做的修改已经标记上了 `MIRAI MODIFY START`
\ No newline at end of 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))
}
......
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,12 +39,14 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
override suspend fun login() {
suspend fun doLogin() {
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")
// bot.logger.info("Trying login")
var response: LoginPacket.LoginPacketResponse = LoginPacket.SubCommand9(bot.client).sendAndExpect()
mainloop@ while (true) {
when (response) {
......@@ -93,13 +96,12 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
}
println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>()
StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>(6000)
}
suspend fun doInit() {
override suspend fun init() {
//start updating friend/group list
bot.logger.info("Start updating friend/group list")
/*
val data = FriendList.GetFriendGroupList(
bot.client,
......@@ -109,17 +111,10 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
2
).sendAndExpect<FriendList.GetFriendGroupList.Response>()
*/
val data = FriendList.GetTroopListSimplify(
val data = FriendList.GetTroopList(
bot.client
).sendAndExpect<FriendList.GetTroopListSimplify.Response>(100000)
).sendAndExpect<FriendList.GetTroopList.Response>()
println(data.contentToString())
}
doLogin()
doInit()
}
......@@ -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()
}
\ No newline at end of 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)
class Pt4Token(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
typealias PSKeyMap = MutableMap<String, PSKey>
typealias Pt4TokenMap = MutableMap<String, Pt4Token>
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)
internal class Pt4Token(data: ByteArray, creationTime: Long, expireTime: Long) : KeyWithExpiry(data, creationTime, expireTime)
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
)
\ No newline at end of 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,
......
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
\ No newline at end of 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())
......
......@@ -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 {
......
......@@ -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
......
......@@ -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 {
......
......@@ -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 // 可能还可以改变 `[动画表情]`
)
)
)
......
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(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)
......
......@@ -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 ->
......
......@@ -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
......
......@@ -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> {
......
......@@ -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}]"
......
......@@ -47,6 +47,13 @@ abstract class BotNetworkHandler : CoroutineScope {
@MiraiInternalAPI
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
): 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) {
......
......@@ -130,7 +130,7 @@ fun Bot.messageDSL() {
// 当消息中包含 "复读" 时
contains("复读") {
val listener = (contains("复读1") or contains("复读2")) {
reply(message)
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment