Redesign packets

This commit is contained in:
Him188 2019-11-06 19:49:37 +08:00
parent 6f498554b7
commit bd9cf3bfd1
60 changed files with 1537 additions and 1494 deletions

View File

@ -0,0 +1,3 @@
package net.mamoe.mirai
actual object MiraiEnvironment

View File

@ -0,0 +1,7 @@
package net.mamoe.mirai.event
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
internal actual val EventDispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()

View File

@ -0,0 +1,12 @@
package net.mamoe.mirai.network.protocol.tim
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
/**
* 包处理协程调度器.
*
* JVM: 独立的 4 thread 调度器
*/
actual val NetworkDispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()

View File

@ -0,0 +1,51 @@
package net.mamoe.mirai.utils
import android.util.Log
actual typealias PlatformLogger = AndroidLogger
/**
* Android 平台的默认的日志记录器, 使用 [Log]
* 不应该直接构造这个类的实例. 需使用 [DefaultLogger]
*/
open class AndroidLogger internal constructor(override val identity: String?) : MiraiLoggerPlatformBase() {
override fun verbose0(any: Any?) {
Log.v(identity, any.toString())
}
override fun verbose0(message: String?, e: Throwable?) {
Log.v(identity, message.toString(), e)
}
override fun debug0(any: Any?) {
Log.d(identity, any.toString())
}
override fun debug0(message: String?, e: Throwable?) {
Log.d(identity, message.toString(), e)
}
override fun info0(any: Any?) {
Log.i(identity, any.toString())
}
override fun info0(message: String?, e: Throwable?) {
Log.i(identity, message.toString(), e)
}
override fun warning0(any: Any?) {
Log.w(identity, any.toString())
}
override fun warning0(message: String?, e: Throwable?) {
Log.w(identity, message.toString(), e)
}
override fun error0(any: Any?) {
Log.e(identity, any.toString())
}
override fun error0(message: String?, e: Throwable?) {
Log.e(identity, message.toString(), e)
}
}

View File

@ -0,0 +1,100 @@
package net.mamoe.mirai.utils
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.http.ContentType
import io.ktor.http.content.OutgoingContent
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.io.ByteWriteChannel
import kotlinx.io.core.Input
import java.io.DataInput
import java.io.EOFException
import java.io.InputStream
import java.net.InetAddress
import java.security.MessageDigest
import java.util.zip.CRC32
/**
* 设备名
*/
actual val deviceName: String get() = InetAddress.getLocalHost().hostName
/**
* Ktor HttpClient. 不同平台使用不同引擎.
*/
@KtorExperimentalAPI
internal actual val httpClient: HttpClient
get() = HttpClient(CIO)
/**
* Localhost 解析
*/
actual fun localIpAddress(): String = InetAddress.getLocalHost().hostAddress
internal actual fun HttpRequestBuilder.configureBody(
inputSize: Long,
input: Input
) {
body = object : OutgoingContent.WriteChannelContent() {
override val contentType: ContentType = ContentType.Image.PNG
override val contentLength: Long = inputSize
override suspend fun writeTo(channel: ByteWriteChannel) {//不知道为什么这个 channel 在 common 找不到...
val buffer = byteArrayOf(1)
repeat(contentLength.toInt()) {
input.readFully(buffer, 0, 1)
channel.writeFully(buffer, 0, 1)
}
}
}
}
/**
* MD5 算法
*
* @return 16 bytes
*/
actual fun md5(byteArray: ByteArray): ByteArray = MessageDigest.getInstance("MD5").digest(byteArray)
fun InputStream.md5(): ByteArray {
val digest = MessageDigest.getInstance("md5")
digest.reset()
this.readInSequence {
digest.update(it.toByte())
}
return digest.digest()
}
fun DataInput.md5(): ByteArray {
val digest = MessageDigest.getInstance("md5")
digest.reset()
val buffer = byteArrayOf(1)
while (true) {
try {
this.readFully(buffer)
} catch (e: EOFException) {
break
}
digest.update(buffer[0])
}
return digest.digest()
}
private inline fun InputStream.readInSequence(block: (Int) -> Unit) {
var read: Int
while (this.read().also { read = it } != -1) {
block(read)
}
}
/**
* CRC32 算法
*/
actual fun crc32(key: ByteArray): Int = CRC32().apply { update(key) }.value.toInt()
/**
* hostname 解析 ipv4
*/
actual fun solveIpAddress(hostname: String): String = InetAddress.getByName(hostname).hostAddress

View File

@ -0,0 +1,12 @@
package net.mamoe.mirai.utils
import kotlinx.io.core.IoBuffer
/**
* 让用户处理验证码
*
* @return 用户输入得到的验证码. null 时一定 `length==4`.
*/
internal actual suspend fun solveCaptcha(captchaBuffer: IoBuffer): String? {
TODO("Unsupported yet")
}

View File

@ -0,0 +1,47 @@
package net.mamoe.mirai.utils.io
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.io.core.Closeable
import kotlinx.io.core.IoBuffer
import kotlinx.io.nio.read
import java.net.InetSocketAddress
import java.nio.channels.DatagramChannel
import java.nio.channels.ReadableByteChannel
/**
* 多平台适配的 DatagramChannel.
*/
actual class PlatformDatagramChannel actual constructor(serverHost: String, serverPort: Short) : Closeable {
private val serverAddress: InetSocketAddress = InetSocketAddress(serverHost, serverPort.toInt())
private val channel: DatagramChannel = DatagramChannel.open().connect(serverAddress)
@Throws(ReadPacketInternalException::class)
actual suspend fun read(buffer: IoBuffer) = withContext(Dispatchers.IO) {
try {
(channel as ReadableByteChannel).read(buffer)
} catch (e: Throwable) {
throw ReadPacketInternalException(e)
}
}
@Throws(SendPacketInternalException::class)
actual suspend fun send(buffer: IoBuffer) = withContext(Dispatchers.IO) {
buffer.readDirect {
try {
channel.send(it, serverAddress)
} catch (e: Throwable) {
throw SendPacketInternalException(e)
}
}
}
override fun close() {
channel.close()
}
actual val isOpen: Boolean get() = channel.isOpen
}
actual typealias ClosedChannelException = java.nio.channels.ClosedChannelException

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.mamoe.mirai">
<application
android:allowBackup="true"
android:supportsRtl="true">
</application>
</manifest>

View File

@ -11,6 +11,7 @@ import net.mamoe.mirai.message.singleChain
import net.mamoe.mirai.network.BotSession
import net.mamoe.mirai.network.protocol.tim.handler.EventPacketHandler
import net.mamoe.mirai.network.protocol.tim.packet.action.RequestProfileDetailsPacket
import net.mamoe.mirai.network.protocol.tim.packet.action.RequestProfileDetailsResponse
import net.mamoe.mirai.qqAccount
import net.mamoe.mirai.utils.SuspendLazy
import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail
@ -129,7 +130,7 @@ open class QQ internal constructor(bot: Bot, id: UInt) : Contact(bot, id) {
*/
suspend fun updateProfile(): Profile = bot.withSession {
RequestProfileDetailsPacket(bot.qqAccount, id, sessionKey)
.sendAndExpect<RequestProfileDetailsPacket.Response, Profile> { it.profile }
.sendAndExpect<RequestProfileDetailsResponse, Profile> { it.profile }
.await().let {
@Suppress("UNCHECKED_CAST")
if ((::profile as SuspendLazy<Profile>).isInitialized()) {

View File

@ -34,4 +34,9 @@ class FriendConversationInitializedEvent(bot: Bot, sender: QQ) : FriendEvent(bot
/**
* 好友在线状态改变事件
*/
class FriendOnlineStatusChangedEvent(bot: Bot, sender: QQ, val newStatus: OnlineStatus) : FriendEvent(bot, sender)
class FriendOnlineStatusChangedEvent(bot: Bot, sender: QQ, val newStatus: OnlineStatus) : FriendEvent(bot, sender)
/**
* 机器人账号被 [sender] 删除好友
*/
class DeletedByFriendEvent(bot: Bot, qq: QQ) : FriendEvent(bot, qq)

View File

@ -4,7 +4,6 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.Cancellable
import net.mamoe.mirai.network.protocol.tim.packet.OutgoingPacket
import net.mamoe.mirai.network.protocol.tim.packet.Packet
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
/* Abstract */
@ -43,10 +42,10 @@ class BeforePacketSendEvent(bot: Bot, packet: OutgoingPacket) : OutgoingPacketEv
/**
* 来自服务器的数据包的相关事件
*/
sealed class ServerPacketEvent<P : ServerPacket>(bot: Bot, packet: P) : PacketEvent<P>(bot, packet)
sealed class ServerPacketEvent<P : Packet>(bot: Bot, packet: P) : PacketEvent<P>(bot, packet)
/**
* 服务器数据包接收事件. 此时包已经解密完成.
*/
class ServerPacketReceivedEvent<P : ServerPacket>(bot: Bot, packet: P) : ServerPacketEvent<P>(bot, packet),
class ServerPacketReceivedEvent<P : Packet>(bot: Bot, packet: P) : ServerPacketEvent<P>(bot, packet),
Cancellable

View File

@ -4,14 +4,13 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler.BotSocketAdapter
import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler.LoginHandler
import net.mamoe.mirai.network.protocol.tim.handler.*
import net.mamoe.mirai.network.protocol.tim.packet.HeartbeatPacket
import net.mamoe.mirai.network.protocol.tim.packet.OutgoingPacket
import net.mamoe.mirai.network.protocol.tim.packet.Packet
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
import net.mamoe.mirai.network.protocol.tim.packet.event.ServerEventPacket
import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
import net.mamoe.mirai.network.protocol.tim.packet.login.RequestSKeyPacket
import net.mamoe.mirai.utils.BotNetworkConfiguration
@ -48,6 +47,7 @@ import net.mamoe.mirai.utils.io.PlatformDatagramChannel
@Suppress("PropertyName")
interface BotNetworkHandler<Socket : DataPacketSocketAdapter> : CoroutineScope {
val socket: Socket
val bot: Bot
/**
* 得到 [PacketHandler].

View File

@ -11,7 +11,8 @@ import net.mamoe.mirai.network.protocol.tim.handler.ActionPacketHandler
import net.mamoe.mirai.network.protocol.tim.handler.DataPacketSocketAdapter
import net.mamoe.mirai.network.protocol.tim.handler.TemporaryPacketHandler
import net.mamoe.mirai.network.protocol.tim.packet.OutgoingPacket
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
import net.mamoe.mirai.network.protocol.tim.packet.Packet
import net.mamoe.mirai.network.protocol.tim.packet.SessionKey
import net.mamoe.mirai.utils.getGTK
import kotlin.coroutines.coroutineContext
@ -21,7 +22,7 @@ import kotlin.coroutines.coroutineContext
@Suppress("FunctionName", "NOTHING_TO_INLINE")
internal inline fun TIMBotNetworkHandler.BotSession(
bot: Bot,
sessionKey: ByteArray,
sessionKey: SessionKey,
socket: DataPacketSocketAdapter
) = BotSession(bot, sessionKey, socket, this)
@ -33,7 +34,7 @@ internal inline fun TIMBotNetworkHandler.BotSession(
*/
class BotSession(
val bot: Bot,
val sessionKey: ByteArray,
val sessionKey: SessionKey,
val socket: DataPacketSocketAdapter,
val NetworkScope: CoroutineScope
) {
@ -79,7 +80,10 @@ class BotSession(
*
* @see Bot.withSession 转换接收器 (receiver, `this` 的指向) [BotSession]
*/
suspend inline fun <reified P : ServerPacket, R> OutgoingPacket.sendAndExpect(checkSequence: Boolean = true, noinline handler: suspend (P) -> R): CompletableDeferred<R> {
suspend inline fun <reified P : Packet, R> OutgoingPacket.sendAndExpect(
checkSequence: Boolean = true,
noinline handler: suspend (P) -> R
): CompletableDeferred<R> {
val deferred: CompletableDeferred<R> = CompletableDeferred(coroutineContext[Job])
bot.network.addHandler(TemporaryPacketHandler(P::class, deferred, this@BotSession, checkSequence, coroutineContext + deferred).also {
it.toSend(this)
@ -92,13 +96,13 @@ class BotSession(
* 发送一个数据包, 并期待接受一个特定的 [ServerPacket][P].
* 您将能从本函数的返回值 [CompletableDeferred] 接收到所期待的 [P]
*/
suspend inline fun <reified P : ServerPacket> OutgoingPacket.sendAndExpect(checkSequence: Boolean = true): CompletableDeferred<P> = sendAndExpect<P, P>(checkSequence) { it }
suspend inline fun <reified P : Packet> OutgoingPacket.sendAndExpect(checkSequence: Boolean = true): CompletableDeferred<P> =
sendAndExpect<P, P>(checkSequence) { it }
suspend inline fun OutgoingPacket.send() = socket.sendPacket(this)
}
suspend inline fun BotSession.distributePacket(packet: ServerPacket) = this.socket.distributePacket(packet)
inline val BotSession.isOpen: Boolean get() = socket.isOpen
inline val BotSession.qqAccount: UInt get() = bot.account.id
@ -106,22 +110,22 @@ inline val BotSession.qqAccount: UInt get() = bot.account.id
* 取得 [BotNetworkHandler] [BotSession].
* 实际上是一个捷径.
*/
val BotNetworkHandler<*>.session get() = this[ActionPacketHandler].session
val BotNetworkHandler<*>.session: BotSession get() = this[ActionPacketHandler].session
/**
* 取得 [BotNetworkHandler] sessionKey.
* 实际上是一个捷径.
*/
inline val BotNetworkHandler<*>.sessionKey get() = this.session.sessionKey
inline val BotNetworkHandler<*>.sessionKey: SessionKey get() = this.session.sessionKey
/**
* 取得 [Bot] [BotSession].
* 实际上是一个捷径.
*/
inline val Bot.session get() = this.network.session
inline val Bot.session: BotSession get() = this.network.session
/**
* 取得 [Bot] `sessionKey`.
* 实际上是一个捷径.
*/
inline val Bot.sessionKey get() = this.session.sessionKey
inline val Bot.sessionKey: SessionKey get() = this.session.sessionKey

View File

@ -1,9 +1,12 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.network.protocol.tim
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.io.core.*
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.ListeningStatus
import net.mamoe.mirai.event.broadcast
@ -16,7 +19,6 @@ import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.BotSession
import net.mamoe.mirai.network.protocol.tim.handler.*
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.network.protocol.tim.packet.event.ServerEventPacket
import net.mamoe.mirai.network.protocol.tim.packet.login.*
import net.mamoe.mirai.network.session
import net.mamoe.mirai.qqAccount
@ -25,6 +27,7 @@ import net.mamoe.mirai.utils.OnlineStatus
import net.mamoe.mirai.utils.io.*
import net.mamoe.mirai.utils.solveCaptcha
import kotlin.coroutines.CoroutineContext
import kotlin.properties.Delegates
/**
* 包处理协程调度器.
@ -38,7 +41,7 @@ expect val NetworkDispatcher: CoroutineDispatcher
*
* @see BotNetworkHandler
*/
internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
internal class TIMBotNetworkHandler internal constructor(override val bot: Bot) :
BotNetworkHandler<TIMBotNetworkHandler.BotSocketAdapter>, PacketHandlerList() {
override val coroutineContext: CoroutineContext =
@ -50,7 +53,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
override lateinit var socket: BotSocketAdapter
private set
internal val temporaryPacketHandlers = mutableListOf<TemporaryPacketHandler<*, *>>()
private val temporaryPacketHandlers = mutableListOf<TemporaryPacketHandler<*, *>>()
private val handlersLock = Mutex()
private var heartbeatJob: Job? = null
@ -85,7 +88,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
//private | internal
private fun onLoggedIn(sessionKey: ByteArray) {
private fun onLoggedIn() {
require(size == 0) { "Already logged in" }
val session = BotSession(bot, sessionKey, socket)
@ -94,7 +97,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
bot.logger.info("Successfully logged in")
}
private lateinit var sessionKey: ByteArray
private var sessionKey: SessionKey by Delegates.notNull()
override suspend fun awaitDisconnection() {
heartbeatJob?.join()
@ -123,6 +126,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
internal inner class BotSocketAdapter(override val serverIp: String, val configuration: BotNetworkConfiguration) :
DataPacketSocketAdapter {
override val channel: PlatformDatagramChannel = PlatformDatagramChannel(serverIp, 8000)
override val isOpen: Boolean get() = channel.isOpen
@ -151,14 +155,19 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
continue
}// sometimes exceptions are thrown without this `if` clause
}
//buffer.resetForRead()
launch {
// `.use`: Ensure that the packet is consumed **totally**
// so that all the buffers are released
ByteArrayPool.useInstance {
val length = buffer.readRemaining - 1
buffer.readFully(it, 0, length)
buffer.resetForWrite()
buffer.writeFully(it, 0, length)
}
ByteReadPacket(buffer, IoBuffer.Pool).use {
try {
distributePacket(it.parseServerPacket(buffer.readRemaining))
processPacket(it)
} catch (e: Exception) {
bot.logger.error(e)
}
@ -173,19 +182,19 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
loginHandler = LoginHandler(configuration)
val expect = expectPacket<TouchResponsePacket>()
val expect = expectPacket<TouchPacket.TouchResponse>()
launch { processReceive() }
launch {
if (withTimeoutOrNull(configuration.touchTimeout.millisecondsLong) { expect.join() } == null) {
loginResult.complete(LoginResult.TIMEOUT)
}
}
sendPacket(TouchPacket(bot.qqAccount, serverIp))
sendPacket(TouchPacket(bot.qqAccount, serverIp, false))
return loginResult.await()
}
private suspend inline fun <reified P : ServerPacket> expectPacket(): CompletableDeferred<P> {
private suspend inline fun <reified P : Packet> expectPacket(): CompletableDeferred<P> {
val receiving = CompletableDeferred<P>(coroutineContext[Job])
subscribe<ServerPacketReceivedEvent<*>> {
if (it.packet is P && it.bot === bot) {
@ -197,47 +206,41 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
return receiving
}
override suspend fun distributePacket(packet: ServerPacket) {
try {
packet.decode()
} catch (e: Exception) {
bot.logger.error("When distributePacket", e)
bot.logger.debug("Packet=$packet")
bot.logger.debug("Packet size=" + packet.input.readBytes().size)
bot.logger.debug("Packet data=" + packet.input.readBytes().toUHexString())
packet.close()
throw e
private suspend inline fun processPacket(input: ByteReadPacket) = with(input) {
discardExact(3)
val id = PacketId(readUShort())
val sequenceId = readUShort()
discardExact(7)//4 for qq number, 3 for 0x00 0x00 0x00. 但更可能是应该 discard 8
val packet: Packet = with(id.factory) {
try {
loginHandler.provideDecrypter(id.factory)
.decrypt(input)
.decode(id, sequenceId, this@TIMBotNetworkHandler)
} finally {
input.close()
}
}
packet.use {
packet::class.takeUnless { ResponsePacket::class.isInstance(packet) }
?.simpleName?.takeUnless { it.endsWith("Encrypted") || it.endsWith("Raw") }
?.let {
bot.logger.verbose("Packet received: $packet")
}
bot.logger.verbose("Packet received: $packet")
// Remove first to release the lock
handlersLock.withLock {
temporaryPacketHandlers.filter { it.filter(session, packet) }
.also { temporaryPacketHandlers.removeAll(it) }
}.forEach {
it.doReceiveWithoutExceptions(packet)
}
// Remove first to release the lock
handlersLock.withLock {
temporaryPacketHandlers.filter { it.filter(session, packet, sequenceId) }
.also { temporaryPacketHandlers.removeAll(it) }
}.forEach {
it.doReceiveWithoutExceptions(packet)
}
if (packet is ServerEventPacket) {
// Ensure the response packet is sent
sendPacket(packet.ResponsePacket(bot.qqAccount, sessionKey))
}
if (ServerPacketReceivedEvent(bot, packet).broadcast().cancelled)
return
if (ServerPacketReceivedEvent(bot, packet).broadcast().cancelled) {
return
}
// They should be called in sequence because packet is lock-free
loginHandler.onPacketReceived(packet)
this@TIMBotNetworkHandler.forEach {
it.instance.onPacketReceived(packet)
}
// They should be called in sequence because packet is lock-free
loginHandler.onPacketReceived(packet)
this@TIMBotNetworkHandler.forEach {
it.instance.onPacketReceived(packet)
}
}
@ -264,9 +267,9 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
}
packet.takeUnless { _ ->
packet.packetId is KnownPacketId && packet.packetId.builder?.let {
packet.packetId is KnownPacketId && packet.packetId.factory.let {
it::class.annotations.filterIsInstance<NoLog>().any()
} == true
}
}?.let {
bot.logger.verbose("Packet sent: $it")
}
@ -284,7 +287,6 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
}
}
/**
* 处理登录过程
*/
@ -293,9 +295,9 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
private lateinit var token0825: ByteArray//56
private var loginTime: Int = 0
private lateinit var loginIP: String
private var privateKey: ByteArray = getRandomByteArray(16)
private var privateKey: PrivateKey = PrivateKey(getRandomByteArray(16))
private lateinit var sessionResponseDecryptionKey: IoBuffer
private var sessionResponseDecryptionKey: SessionResponseDecryptionKey by Delegates.notNull()
private var captchaSectionId: Int = 1
private var captchaCache: IoBuffer? = null
@ -311,9 +313,27 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
field = value
}
suspend fun onPacketReceived(packet: ServerPacket) {//complex function, but it doesn't matter
@Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
internal fun <D : Decrypter> provideDecrypter(factory: PacketFactory<*, D>): D =
when (factory.decrypterType) {
TouchKey -> TouchKey
CaptchaKey -> CaptchaKey
ShareKey -> ShareKey
NoDecrypter -> NoDecrypter
SessionResponseDecryptionKey -> sessionResponseDecryptionKey
SubmitPasswordResponseDecrypter -> SubmitPasswordResponseDecrypter(privateKey)
PrivateKey -> privateKey
SessionKey -> sessionKey
else -> {
error("No decrypter found")
}
} as D
suspend fun onPacketReceived(packet: Any) {//complex function, but it doesn't matter
when (packet) {
is TouchResponsePacket -> {
is TouchPacket.TouchResponse -> {
if (packet.serverIP != null) {//redirection
socket.close()
socket = BotSocketAdapter(packet.serverIP!!, socket.configuration)
@ -339,13 +359,13 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
}
}
is LoginResponseFailedPacket -> {
loginResult.complete(packet.loginResult)
is SubmitPasswordPacket.LoginResponse.Failed -> {
loginResult.complete(packet.result)
return
}
is CaptchaCorrectPacket -> {
this.privateKey = getRandomByteArray(16)//似乎是必须的
is CaptchaPacket.CaptchaResponse.Correct -> {
this.privateKey = PrivateKey(getRandomByteArray(16))//似乎是必须的
this.token00BA = packet.token00BA
socket.sendPacket(
@ -362,7 +382,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
)
}
is LoginResponseCaptchaInitPacket -> {
is SubmitPasswordPacket.LoginResponse.CaptchaInit -> {
//[token00BA]来源之一: 验证码
this.token00BA = packet.token00BA
this.captchaCache = packet.captchaPart1
@ -370,7 +390,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
if (packet.unknownBoolean) {
this.captchaSectionId = 1
socket.sendPacket(
RequestCaptchaTransmissionPacket(
CaptchaPacket.RequestTransmission(
bot.qqAccount,
this.token0825,
this.captchaSectionId++,
@ -380,7 +400,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
}
}
is CaptchaTransmissionResponsePacket -> {
is CaptchaPacket.CaptchaResponse.Transmission -> {
//packet is ServerCaptchaWrongPacket
if (this.captchaSectionId == 0) {
bot.logger.warning("验证码错误, 请重新输入")
@ -397,11 +417,11 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
this.captchaCache = null
if (code == null) {
this.captchaSectionId = 1//意味着正在刷新验证码
socket.sendPacket(OutgoingCaptchaRefreshPacket(bot.qqAccount, token0825))
socket.sendPacket(CaptchaPacket.Refresh(bot.qqAccount, token0825))
} else {
this.captchaSectionId = 0//意味着已经提交验证码
socket.sendPacket(
SubmitCaptchaPacket(
CaptchaPacket.Submit(
bot.qqAccount,
token0825,
code,
@ -411,7 +431,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
}
} else {
socket.sendPacket(
RequestCaptchaTransmissionPacket(
CaptchaPacket.RequestTransmission(
bot.qqAccount,
token0825,
captchaSectionId++,
@ -421,7 +441,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
}
}
is LoginResponseSuccessPacket -> {
is SubmitPasswordPacket.LoginResponse.Success -> {
this.sessionResponseDecryptionKey = packet.sessionResponseDecryptionKey
socket.sendPacket(
RequestSessionPacket(
@ -435,8 +455,8 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
}
//是ClientPasswordSubmissionPacket之后服务器回复的可能之一
is LoginResponseKeyExchangeResponsePacket -> {
this.privateKey = packet.privateKeyUpdate
is SubmitPasswordPacket.LoginResponse.KeyExchange -> {
this.privateKey = packet.privateKeyUpdate!!
socket.sendPacket(
SubmitPasswordPacket(
@ -453,9 +473,9 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
)
}
is SessionKeyResponsePacket -> {
sessionKey = packet.sessionKey
bot.logger.info("sessionKey = ${sessionKey.toUHexString()}")
is RequestSessionPacket.SessionKeyResponse -> {
sessionKey = packet.sessionKey!!
bot.logger.info("sessionKey = ${sessionKey.value.toUHexString()}")
heartbeatJob = launch {
while (socket.isOpen) {
@ -467,7 +487,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
HeartbeatPacket(
bot.qqAccount,
sessionKey
).sendAndExpect<HeartbeatPacket.Response>().join()
).sendAndExpect<HeartbeatPacketResponse>().join()
} == null) {
bot.logger.warning("Heartbeat timed out")
bot.reinitializeNetworkHandler(configuration, HeartbeatTimeoutException())
@ -482,25 +502,12 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
setOnlineStatus(OnlineStatus.ONLINE)//required
}
is ServerLoginSuccessPacket -> {
is ChangeOnlineStatusPacket.ChangeOnlineStatusResponse -> {
BotLoginSucceedEvent(bot).broadcast()
onLoggedIn(sessionKey)
onLoggedIn()
this.close()//The LoginHandler is useless since then
}
is ServerCaptchaPacket.Encrypted -> socket.distributePacket(packet.decrypt())
is LoginResponseCaptchaInitPacket.Encrypted -> socket.distributePacket(packet.decrypt())
is LoginResponseKeyExchangeResponsePacket.Encrypted -> socket.distributePacket(packet.decrypt(this.privateKey))
is LoginResponseSuccessPacket.Encrypted -> socket.distributePacket(packet.decrypt(this.privateKey))
is SessionKeyResponsePacket.Encrypted -> socket.distributePacket(packet.decrypt(this.sessionResponseDecryptionKey))
is TouchResponsePacket.Encrypted -> socket.distributePacket(packet.decrypt())
is UnknownServerPacket.Encrypted -> socket.distributePacket(packet.decrypt(sessionKey))
else -> {
}
}
}
@ -511,9 +518,6 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) :
fun close() {
this.captchaCache = null
if (::sessionResponseDecryptionKey.isInitialized && sessionResponseDecryptionKey.readRemaining != 0)
this.sessionResponseDecryptionKey.release(IoBuffer.Pool)
}
}
}

View File

@ -8,11 +8,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.mamoe.mirai.network.BotSession
import net.mamoe.mirai.network.isOpen
import net.mamoe.mirai.network.protocol.tim.packet.Packet
import net.mamoe.mirai.network.protocol.tim.packet.RequestAccountInfoPacket
import net.mamoe.mirai.network.protocol.tim.packet.ResponsePacket
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
import net.mamoe.mirai.network.protocol.tim.packet.event.ServerEventPacket
import net.mamoe.mirai.network.protocol.tim.packet.login.RequestSKeyPacket
import net.mamoe.mirai.network.protocol.tim.packet.login.SKey
import net.mamoe.mirai.network.qqAccount
/**
@ -29,10 +28,10 @@ class ActionPacketHandler(session: BotSession) : PacketHandler(session) {
@ExperimentalStdlibApi
override suspend fun onPacketReceived(packet: ServerPacket): Unit = with(session) {
override suspend fun onPacketReceived(packet: Packet): Unit = with(session) {
when (packet) {
is RequestSKeyPacket.Response -> {
sKey = packet.sKey
is SKey -> {
sKey = packet.delegate
cookies = "uin=o$qqAccount;skey=$sKey;"
@ -50,10 +49,6 @@ class ActionPacketHandler(session: BotSession) : PacketHandler(session) {
}
}
is ServerEventPacket.Raw.Encrypted -> socket.distributePacket(packet.decrypt(sessionKey))
is ServerEventPacket.Raw -> socket.distributePacket(packet.distribute())
is ResponsePacket.Encrypted<*> -> socket.distributePacket(packet.decrypt(sessionKey))
else -> {
}
}

View File

@ -1,3 +1,5 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.network.protocol.tim.handler
import kotlinx.io.core.Closeable
@ -6,7 +8,6 @@ import net.mamoe.mirai.event.events.ServerPacketReceivedEvent
import net.mamoe.mirai.network.BotSession
import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.packet.OutgoingPacket
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
import net.mamoe.mirai.utils.io.PlatformDatagramChannel
/**
@ -35,11 +36,6 @@ interface DataPacketSocketAdapter : Closeable {
*/
val isOpen: Boolean
/**
* 分发数据包给 [PacketHandler]
*/
suspend fun distributePacket(packet: ServerPacket)
/**
* 发送一个数据包(非异步).
*

View File

@ -10,13 +10,12 @@ import net.mamoe.mirai.getGroup
import net.mamoe.mirai.getQQ
import net.mamoe.mirai.message.MessageChain
import net.mamoe.mirai.network.BotSession
import net.mamoe.mirai.network.distributePacket
import net.mamoe.mirai.network.protocol.tim.packet.FriendOnlineStatusChangedPacket
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
import net.mamoe.mirai.network.protocol.tim.packet.EventPacket
import net.mamoe.mirai.network.protocol.tim.packet.FriendStatusChanged
import net.mamoe.mirai.network.protocol.tim.packet.Packet
import net.mamoe.mirai.network.protocol.tim.packet.action.FriendImageIdRequestPacket
import net.mamoe.mirai.network.protocol.tim.packet.action.SendFriendMessagePacket
import net.mamoe.mirai.network.protocol.tim.packet.action.SendGroupMessagePacket
import net.mamoe.mirai.network.protocol.tim.packet.event.*
import net.mamoe.mirai.network.qqAccount
/**
@ -28,35 +27,29 @@ import net.mamoe.mirai.network.qqAccount
class EventPacketHandler(session: BotSession) : PacketHandler(session) {
companion object Key : PacketHandler.Key<EventPacketHandler>
override suspend fun onPacketReceived(packet: ServerPacket): Unit = with(session) {
override suspend fun onPacketReceived(packet: Packet): Unit = with(session) {
when (packet) {
is ServerGroupUploadFileEventPacket -> {
//todo
is EventPacket.FriendMessage -> {
if (!packet.isPrevious) FriendMessageEvent(bot, bot.getQQ(packet.qq), packet.message) else null
}
is FriendMessageEventPacket -> {
if (!packet.isPrevious) FriendMessageEvent(bot, bot.getQQ(packet.qq), packet.message).broadcast()
}
is GroupMessageEventPacket -> {
is EventPacket.GroupMessage -> {
if (packet.qq == bot.account.id) return
GroupMessageEvent(
bot, bot.getGroup(GroupId(packet.groupNumber)), bot.getQQ(packet.qq), packet.message, packet.senderPermission, packet.senderName
).broadcast()
)
}
is FriendConversationInitializedEventPacket -> FriendConversationInitializedEvent(bot, bot.getQQ(packet.qq)).broadcast()
is FriendOnlineStatusChangedPacket -> FriendOnlineStatusChangedEvent(bot, bot.getQQ(packet.qq), packet.status).broadcast()
is EventPacket.FriendConversationInitialize -> FriendConversationInitializedEvent(bot, bot.getQQ(packet.qq))
is FriendStatusChanged -> FriendOnlineStatusChangedEvent(bot, bot.getQQ(packet.qq), packet.status)
is FriendImageIdRequestPacket.Response -> packet.imageId?.let { FriendImageIdObtainedEvent(bot, it) }
is GroupMemberPermissionChangedEventPacket -> MemberPermissionChangedEvent(
bot, bot.getGroup(packet.groupId.groupId()), bot.getQQ(packet.qq), packet.kind
).broadcast()
is EventPacket.MemberPermissionChange ->
MemberPermissionChangedEvent(bot, bot.getGroup(packet.groupId.groupId()), bot.getQQ(packet.qq), packet.kind)
is FriendOnlineStatusChangedPacket.Encrypted -> distributePacket(packet.decrypt(sessionKey))
}
else -> null
}?.broadcast()
}
suspend fun sendFriendMessage(qq: QQ, message: MessageChain) {

View File

@ -1,7 +1,7 @@
package net.mamoe.mirai.network.protocol.tim.handler
import net.mamoe.mirai.network.BotSession
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
import net.mamoe.mirai.network.protocol.tim.packet.Packet
/**
* 数据包(接受/发送)处理器
@ -9,7 +9,7 @@ import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
abstract class PacketHandler(
val session: BotSession
) {
abstract suspend fun onPacketReceived(packet: ServerPacket)
abstract suspend fun onPacketReceived(packet: Packet)
interface Key<T : PacketHandler>

View File

@ -6,7 +6,7 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.withContext
import net.mamoe.mirai.network.BotSession
import net.mamoe.mirai.network.protocol.tim.packet.OutgoingPacket
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
import net.mamoe.mirai.network.protocol.tim.packet.Packet
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.KClass
@ -23,7 +23,7 @@ import kotlin.reflect.KClass
*
* @see BotSession.sendAndExpect
*/
class TemporaryPacketHandler<P : ServerPacket, R>(
class TemporaryPacketHandler<P : Packet, R>(
private val expectationClass: KClass<P>,
private val deferred: CompletableDeferred<R>,
private val fromSession: BotSession,
@ -54,10 +54,10 @@ class TemporaryPacketHandler<P : ServerPacket, R>(
session.socket.sendPacket(toSend)
}
internal fun filter(session: BotSession, packet: ServerPacket): Boolean =
expectationClass.isInstance(packet) && session === this.fromSession && if (checkSequence) packet.sequenceId == toSend.sequenceId else true
internal fun filter(session: BotSession, packet: Packet, sequenceId: UShort): Boolean =
expectationClass.isInstance(packet) && session === this.fromSession && if (checkSequence) sequenceId == toSend.sequenceId else true
internal suspend fun doReceiveWithoutExceptions(packet: ServerPacket) {
internal suspend fun doReceiveWithoutExceptions(packet: Packet) {
@Suppress("UNCHECKED_CAST")
val ret = try {
withContext(callerContext) {

View File

@ -7,7 +7,7 @@ import kotlin.reflect.KClass
/**
* ID. 除特殊外, [OutgoingPacketBuilder] 都需要这个注解来指定包 ID.
* ID. 除特殊外, [PacketFactory] 都需要这个注解来指定包 ID.
*/
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@ -21,7 +21,7 @@ inline val AnnotatedId.value: UShort get() = id.value
/**
* 标记这个包对应的事件.
* 这个注解应该被标记在 [ServerPacket]
* 这个注解应该被标记在 [Packet]
*/
@MustBeDocumented
@Retention(AnnotationRetention.BINARY)
@ -34,7 +34,7 @@ annotation class CorrespondingEvent(
* 版本信息
*/
@MustBeDocumented
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
internal annotation class PacketVersion(val date: String, val timVersion: String)

View File

@ -0,0 +1,52 @@
package net.mamoe.mirai.network.protocol.tim.packet
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.IoBuffer
/**
* 会话密匙
*/
inline class SessionKey(override val value: ByteArray) : DecrypterByteArray {
companion object Type : DecrypterType<SessionKey>
}
/**
* [ByteArray] 解密器
*/
interface DecrypterByteArray : Decrypter {
val value: ByteArray
override fun decrypt(packet: ByteReadPacket): ByteReadPacket = packet.decryptBy(value)
}
/**
* [IoBuffer] 解密器
*/
interface DecrypterIoBuffer : Decrypter {
val value: IoBuffer
override fun decrypt(packet: ByteReadPacket): ByteReadPacket = packet.decryptBy(value)
}
/**
* 连接在一起的解密器
*/
inline class LinkedDecrypter(inline val block: (ByteReadPacket) -> ByteReadPacket) : Decrypter {
override fun decrypt(packet: ByteReadPacket): ByteReadPacket = block(packet)
}
object NoDecrypter : Decrypter, DecrypterType<NoDecrypter> {
override fun decrypt(packet: ByteReadPacket): ByteReadPacket = packet
}
/**
* 解密器
*/
interface Decrypter {
fun decrypt(packet: ByteReadPacket): ByteReadPacket
/**
* 连接后将会先用 this 解密, 再用 [another] 解密
*/
operator fun plus(another: Decrypter): Decrypter = LinkedDecrypter { another.decrypt(this.decrypt(it)) }
}
interface DecrypterType<D : Decrypter>

View File

@ -1,4 +1,4 @@
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "JoinDeclarationAndAssignment")
package net.mamoe.mirai.network.protocol.tim.packet
@ -6,30 +6,35 @@ import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUByte
import kotlinx.io.core.readUInt
import net.mamoe.mirai.event.events.FriendOnlineStatusChangedEvent
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.utils.OnlineStatus
import kotlin.properties.Delegates
@CorrespondingEvent(FriendOnlineStatusChangedEvent::class)
abstract class FriendStatusChanged : Packet {
abstract val qq: UInt
abstract val status: OnlineStatus
}
/**
* 好友在线状态改变
*/
@AnnotatedId(KnownPacketId.SEND_FRIEND_MESSAGE)
class FriendOnlineStatusChangedPacket(input: ByteReadPacket) : ServerPacket(input) {
var qq: UInt by Delegates.notNull()
lateinit var status: OnlineStatus
@AnnotatedId(KnownPacketId.FRIEND_ONLINE_STATUS_CHANGE)
object FriendOnlineStatusChangedPacket : SessionPacketFactory<FriendStatusChanged>() {
override fun decode() = with(input) {
qq = readUInt()
discardExact(8)
val id = readUByte()
status = OnlineStatus.ofId(id) ?: error("Unknown online status id $id")
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): FriendStatusChanged =
object : FriendStatusChanged() {
override val qq: UInt
override val status: OnlineStatus
init {
qq = readUInt()
discardExact(8)
val id = readUByte()
status = OnlineStatus.ofId(id) ?: error("Unknown online status id $id")
}
}
//在线 XX XX XX XX 01 00 00 00 00 00 00 00 0A 15 E3 10 00 01 2E 01 00 00 00 00 00 00 00 00 00 00 00 13 08 02 C2 76 E4 B8 DD 00 00 00 00 00 00 00 00 00 00 00
//忙碌 XX XX XX XX 01 00 00 00 00 00 00 00 32 15 E3 10 00 01 2E 01 00 00 00 00 00 00 00 00 00 00 00 13 08 02 C2 76 E4 B8 DD 00 00 00 00 00 00 00 00 00 00 00
@AnnotatedId(KnownPacketId.SEND_FRIEND_MESSAGE)
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
fun decrypt(sessionKey: ByteArray): FriendOnlineStatusChangedPacket =
FriendOnlineStatusChangedPacket(this.decryptBy(sessionKey)).applySequence(sequenceId)
}
}

View File

@ -4,6 +4,7 @@ package net.mamoe.mirai.network.protocol.tim.packet
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.writeUByte
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.utils.io.encryptAndWrite
import net.mamoe.mirai.utils.io.writeHex
@ -15,11 +16,11 @@ import net.mamoe.mirai.utils.io.writeQQ
* @author Him188moe
*/
@AnnotatedId(KnownPacketId.ACCOUNT_INFO)
object RequestAccountInfoPacket : OutgoingPacketBuilder {
object RequestAccountInfoPacket : SessionPacketFactory<RequestAccountInfoPacket.Response>() {
operator fun invoke(
qq: UInt,
sessionKey: ByteArray
) = buildOutgoingPacket {
sessionKey: SessionKey
): OutgoingPacket = buildOutgoingPacket {
writeQQ(qq)
writeHex(TIMProtocol.fixVer2)
encryptAndWrite(sessionKey) {
@ -29,10 +30,11 @@ object RequestAccountInfoPacket : OutgoingPacketBuilder {
}
}
@AnnotatedId(KnownPacketId.ACCOUNT_INFO)
class Response(input: ByteReadPacket) : ResponsePacket(input) {
object Response : Packet {
//等级
//升级剩余活跃天数
//ignored
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): Response = Response
}

View File

@ -3,6 +3,7 @@
package net.mamoe.mirai.network.protocol.tim.packet
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.utils.io.encryptAndWrite
import net.mamoe.mirai.utils.io.writeHex
@ -10,10 +11,10 @@ import net.mamoe.mirai.utils.io.writeQQ
@NoLog
@AnnotatedId(KnownPacketId.HEARTBEAT)
object HeartbeatPacket : OutgoingPacketBuilder {
object HeartbeatPacket : SessionPacketFactory<HeartbeatPacketResponse>() {
operator fun invoke(
bot: UInt,
sessionKey: ByteArray
sessionKey: SessionKey
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
writeHex(TIMProtocol.fixVer)
@ -22,7 +23,10 @@ object HeartbeatPacket : OutgoingPacketBuilder {
}
}
@NoLog
@AnnotatedId(KnownPacketId.HEARTBEAT)
class Response(input: ByteReadPacket) : ResponsePacket(input)
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): HeartbeatPacketResponse =
HeartbeatPacketResponse
}
@NoLog
@AnnotatedId(KnownPacketId.HEARTBEAT)
object HeartbeatPacketResponse : Packet

View File

@ -1,8 +1,7 @@
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "MemberVisibilityCanBePrivate")
package net.mamoe.mirai.network.protocol.tim.packet
import kotlinx.atomicfu.atomic
import kotlinx.io.core.BytePacketBuilder
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.use
@ -18,8 +17,8 @@ import kotlin.jvm.JvmOverloads
*/
class OutgoingPacket(
name: String?,
override val packetId: PacketId,
override val sequenceId: UShort,
val packetId: PacketId,
val sequenceId: UShort,
internal val delegate: ByteReadPacket
) : Packet {
private val name: String by lazy {
@ -31,28 +30,15 @@ class OutgoingPacket(
constructor(annotation: AnnotatedId, sequenceId: UShort, delegate: ByteReadPacket) :
this(annotation.toString(), annotation.id, sequenceId, delegate)
override fun toString(): String = packetToString(name)
override fun toString(): String = packetToString(packetId.value, sequenceId, name)
}
/**
* 发给服务器的数据包的构建器.
* 应由一个 `object` 实现, 且实现 `operator fun invoke`
* 登录完成建立 session 之后发出的包.
* 均使用 sessionKey 加密
*/
interface OutgoingPacketBuilder {
/**
* 2 Ubyte.
* 默认为读取注解 [AnnotatedId]
*/
val annotatedId: AnnotatedId
get() = (this::class.annotations.firstOrNull { it is AnnotatedId } as? AnnotatedId)
?: error("Annotation AnnotatedId not found")
companion object {
private val sequenceIdInternal = atomic(1)
@PublishedApi
internal fun atomicNextSequenceId(): UShort = sequenceIdInternal.getAndIncrement().toUShort()
}
abstract class SessionPacketFactory<out TPacket : Packet> : PacketFactory<TPacket, SessionKey>(SessionKey) {
final override fun decrypt(input: ByteReadPacket, decrypter: SessionKey): ByteReadPacket = decrypter.decrypt(input)
}
/**
@ -61,10 +47,10 @@ interface OutgoingPacketBuilder {
* 若不提供参数 [id], 则会通过注解 [AnnotatedId] 获取 id.
*/
@JvmOverloads
fun OutgoingPacketBuilder.buildOutgoingPacket(
fun PacketFactory<*, *>.buildOutgoingPacket(
name: String? = null,
id: PacketId = this.annotatedId.id,
sequenceId: UShort = OutgoingPacketBuilder.atomicNextSequenceId(),
id: PacketId = this.id,
sequenceId: UShort = PacketFactory.atomicNextSequenceId(),
headerSizeHint: Int = 0,
block: BytePacketBuilder.() -> Unit
): OutgoingPacket {
@ -88,12 +74,12 @@ fun OutgoingPacketBuilder.buildOutgoingPacket(
* 若不提供参数 [id], 则会通过注解 [AnnotatedId] 获取 id.
*/
@JvmOverloads
fun OutgoingPacketBuilder.buildSessionPacket(
fun PacketFactory<*, *>.buildSessionPacket(
bot: UInt,
sessionKey: ByteArray,
sessionKey: SessionKey,
name: String? = null,
id: PacketId = this.annotatedId.id,
sequenceId: UShort = OutgoingPacketBuilder.atomicNextSequenceId(),
id: PacketId = this.id,
sequenceId: UShort = PacketFactory.atomicNextSequenceId(),
headerSizeHint: Int = 0,
block: BytePacketBuilder.() -> Unit
): OutgoingPacket = buildOutgoingPacket(name, id, sequenceId, headerSizeHint) {

View File

@ -1,24 +0,0 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.network.protocol.tim.packet
import kotlinx.io.core.writeFully
import net.mamoe.mirai.utils.io.encryptAndWrite
import net.mamoe.mirai.utils.io.writeQQ
object OutgoingRawPacket : OutgoingPacketBuilder {
operator fun invoke(
id: PacketId,
bot: UInt,
version: ByteArray,
sessionKey: ByteArray,
data: ByteArray
): OutgoingPacket = buildOutgoingPacket(id = id) {
writeQQ(bot)
writeFully(version)
encryptAndWrite(sessionKey) {
writeFully(data)
}
}
}

View File

@ -2,80 +2,142 @@
package net.mamoe.mirai.network.protocol.tim.packet
import kotlinx.atomicfu.atomic
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.IoBuffer
import net.mamoe.mirai.network.protocol.tim.packet.NullPacketId.value
import net.mamoe.mirai.event.events.FriendConversationInitializedEvent
import net.mamoe.mirai.event.events.FriendMessageEvent
import net.mamoe.mirai.event.events.GroupMessageEvent
import net.mamoe.mirai.event.events.MemberPermissionChangedEvent
import net.mamoe.mirai.message.MessageChain
import net.mamoe.mirai.message.NullMessageChain
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.packet.action.*
import net.mamoe.mirai.network.protocol.tim.packet.event.ServerEventPacket
import net.mamoe.mirai.network.protocol.tim.packet.event.EventPacketFactory
import net.mamoe.mirai.network.protocol.tim.packet.event.SenderPermission
import net.mamoe.mirai.network.protocol.tim.packet.login.*
import net.mamoe.mirai.utils.io.toUHexString
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty0
import kotlin.reflect.KProperty1
import kotlin.reflect.KVisibility
/**
* 数据包.
* 一种数据包的处理工厂. 它可以解密解码服务器发来的这个包, 也可以编码加密要发送给服务器的这个包
* 应由一个 `object` 实现, 且实现 `operator fun invoke`
*
* @param TPacket 服务器回复包解析结果
* @param TDecrypter 服务器回复包解密器
*/
interface Packet {
/**
* 包序列 ID. 唯一. 所有包共用一个原子自增序列 ID 生成
*/
val sequenceId: UShort
abstract class PacketFactory<out TPacket : Packet, TDecrypter : Decrypter>(internal val decrypterType: DecrypterType<TDecrypter>) {
/**
* 包识别 ID
* 2 Ubyte.
* 读取注解 [AnnotatedId]
*/
val packetId: PacketId
private val annotatedId: AnnotatedId
get() = (this::class.annotations.firstOrNull { it is AnnotatedId } as? AnnotatedId)
?: error("Annotation AnnotatedId not found")
/**
* ID.
*/
open val id: PacketId by lazy { annotatedId.id }
init {
@Suppress("LeakingThis")
PacketFactoryList.add(this)
}
/**
* **解码**服务器的回复数据包
*/
abstract suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): TPacket
/**
* **解密**服务器的回复数据包. 这个函数将会被 [BotNetworkHandler]
*/
open fun decrypt(input: ByteReadPacket, decrypter: TDecrypter): ByteReadPacket = decrypter.decrypt(input)
companion object {
private val sequenceIdInternal = atomic(1)
@PublishedApi
internal fun atomicNextSequenceId(): UShort = sequenceIdInternal.getAndIncrement().toUShort()
}
}
/**
* ID Hex. 格式为 `00 00 00 00`
*/
val Packet.idHexString: String get() = (packetId.value.toInt().shl(16) or sequenceId.toInt()).toUHexString()
object PacketFactoryList : MutableList<PacketFactory<*, *>> by mutableListOf()
object UnknownPacketFactory : SessionPacketFactory<UnknownPacket>() {
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): UnknownPacket {
return UnknownPacket
}
}
object IgnoredPacketFactory : SessionPacketFactory<IgnoredPacket>() {
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): IgnoredPacket {
return IgnoredPacket
}
}
// region Packet id
/**
* 通过 [value] 匹配一个 [KnownPacketId], 无匹配则返回一个 [UnknownPacketId].
* 通过 [value] 匹配一个 [IgnoredPacketId] [KnownPacketId], 无匹配则返回一个 [UnknownPacketId].
*/
@Suppress("FunctionName")
fun PacketId(value: UShort): PacketId =
KnownPacketId.values().firstOrNull { it.value == value } ?: UnknownPacketId(value)
IgnoredPacketIds.firstOrNull { it.value == value } ?: KnownPacketId.values().firstOrNull { it.value == value } ?: UnknownPacketId(value)
/**
* ID.
*/
interface PacketId {
val value: UShort
val factory: PacketFactory<*, *>
}
/**
* 用于代表 `null`. 调用属性 [value] 时将会得到一个 [error]
* 用于代表 `null`. 调用任何属性时将会得到一个 [error]
*/
object NullPacketId : PacketId {
override val value: UShort get() = error("Packet id is not initialized")
override val factory: PacketFactory<*, *> get() = error("uninitialized")
override val value: UShort get() = error("uninitialized")
}
/**
* 未知的 [PacketId]
*/
inline class UnknownPacketId(override inline val value: UShort) : PacketId
inline class UnknownPacketId(override inline val value: UShort) : PacketId {
override val factory: PacketFactory<*, *> get() = UnknownPacketFactory
}
object IgnoredPacketIds : List<IgnoredPacketId> by {
listOf<UShort>(
).map { IgnoredPacketId(it.toUShort()) }
}()
inline class IgnoredPacketId constructor(override val value: UShort) : PacketId {
override val factory: PacketFactory<*, *> get() = IgnoredPacketFactory
}
/**
* 已知的 [PacketId]. 所有在 Mirai 中实现过的包都会使用这些 Id
*/
enum class KnownPacketId(override inline val value: UShort, internal inline val builder: OutgoingPacketBuilder?) :
@Suppress("unused")
enum class KnownPacketId(override inline val value: UShort, override inline val factory: PacketFactory<*, *>) :
PacketId {
inline TOUCH(0x0825u, TouchPacket),
inline SESSION_KEY(0x0828u, RequestSessionPacket),
inline LOGIN(0x0836u, SubmitPasswordPacket),
inline CAPTCHA(0x00BAu, SubmitCaptchaPacket),
inline SERVER_EVENT_1(0x00CEu, ServerEventPacket.EventResponse),
inline SERVER_EVENT_2(0x0017u, ServerEventPacket.EventResponse),
inline FRIEND_ONLINE_STATUS_CHANGE(0x0081u, null),
inline CHANGE_ONLINE_STATUS(0x00_ECu, null),
inline CAPTCHA(0x00BAu, CaptchaPacket),
inline SERVER_EVENT_1(0x00CEu, EventPacketFactory),
inline SERVER_EVENT_2(0x0017u, EventPacketFactory),
inline FRIEND_ONLINE_STATUS_CHANGE(0x0081u, FriendOnlineStatusChangedPacket),
inline CHANGE_ONLINE_STATUS(0x00ECu, ChangeOnlineStatusPacket),
inline HEARTBEAT(0x0058u, HeartbeatPacket),
inline S_KEY(0x001Du, RequestSKeyPacket),
@ -86,16 +148,89 @@ enum class KnownPacketId(override inline val value: UShort, internal inline val
inline GROUP_IMAGE_ID(0x0388u, GroupImageIdRequestPacket),
inline FRIEND_IMAGE_ID(0x0352u, FriendImageIdRequestPacket),
inline REQUEST_PROFILE_AVATAR(0x00_31u, RequestProfilePicturePacket),
inline REQUEST_PROFILE_DETAILS(0x00_3Cu, RequestProfilePicturePacket),
@Suppress("DEPRECATION")
inline SUBMIT_IMAGE_FILE_NAME(0x01_BDu, SubmitImageFilenamePacket),
inline REQUEST_PROFILE_AVATAR(0x0031u, RequestProfilePicturePacket),
inline REQUEST_PROFILE_DETAILS(0x003Cu, RequestProfilePicturePacket),
// @Suppress("DEPRECATION")
// inline SUBMIT_IMAGE_FILE_NAME(0x01BDu, SubmitImageFilenamePacket),
;
override fun toString(): String = builder?.let { it::class.simpleName } ?: this.name
override fun toString(): String = factory.let { it::class.simpleName } ?: this.name
}
object IgnoredPacket : Packet
sealed class EventPacket {
class GroupFileUpload(inline val xmlMessage: String) : Packet
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2.21173")
class AndroidDeviceStatusChange(inline val kind: Kind) : Packet {
enum class Kind {
ONLINE,
OFFLINE
}
}
@CorrespondingEvent(MemberPermissionChangedEvent::class)
class MemberPermissionChange : Packet {
var groupId: UInt = 0u
var qq: UInt = 0u
lateinit var kind: MemberPermissionChangedEvent.Kind
}
@CorrespondingEvent(FriendConversationInitializedEvent::class)
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2.21173")
class FriendConversationInitialize : Packet {
var qq: UInt = 0u
}
@CorrespondingEvent(FriendMessageEvent::class)
data class FriendMessage(
val qq: UInt,
/**
* 是否是在这次登录之前的消息, 即消息记录
*/
val isPrevious: Boolean,
val message: MessageChain
) : Packet
@CorrespondingEvent(GroupMessageEvent::class)
class GroupMessage : Packet {
var groupNumber: UInt = 0u
internal set
var qq: UInt = 0u
internal set
lateinit var senderName: String
internal set
/**
* 发送方权限.
*/
lateinit var senderPermission: SenderPermission
internal set
var message: MessageChain = NullMessageChain
internal set
}
}
/**
* 一个包的数据 (body)
*/
@Suppress("unused")
interface Packet
//// TODO: 2019/11/5 Packet.toString
/**
* 未知的包.
*/
object UnknownPacket : Packet {
// TODO: 2019/11/5 添加包数据用于调试
}
/**
* 仅用于替换类型应为 [Unit] 的情况
*/
object NoPacket : Packet
// endregion
// region Internal utils
@ -141,17 +276,19 @@ private object IgnoreIdListInclude : List<String> by listOf(
"RefVolatile"
)
/**
* 这个方法会翻倍内存占用, 考虑修改.
*/
@Suppress("UNCHECKED_CAST")
internal fun Packet.packetToString(name: String = this::class.simpleName.toString()): String =
PacketNameFormatter.adjustName(name + "(${this.idHexString})") +
this::class.members
.filterIsInstance<KProperty<*>>()
.filterNot { it.isConst || it.isSuspend || it.visibility != KVisibility.PUBLIC }
.filterNot { prop -> prop.name in IgnoreIdListEquals || IgnoreIdListInclude.any { it in prop.name } }
.joinToString(", ", "{", "}") { it.briefDescription(this@packetToString) }
internal fun Packet.packetToString(id: UShort, sequenceId: UShort, name: String = this::class.simpleName.toString()): String =
id.toString()
/*PacketNameFormatter.adjustName(name + "(${(id.toInt().shl(16) or sequenceId.toInt()).toUHexString()})") +
this::class.members
.filterIsInstance<KProperty<*>>()
.filterNot { it.isConst || it.isSuspend || it.visibility != KVisibility.PUBLIC }
.filterNot { prop -> prop.name in IgnoreIdListEquals || IgnoreIdListInclude.any { it in prop.name } }
.joinToString(", ", "{", "}") { it.briefDescription(this@packetToString) }*/
@Suppress("UNCHECKED_CAST")
private fun KProperty<*>.briefDescription(thisRef: Packet): String =

View File

@ -1,17 +0,0 @@
package net.mamoe.mirai.network.protocol.tim.packet
import kotlin.reflect.KClass
/**
* 标记一个 [OutgoingPacket] 的服务器回复包.
* 在这个包发送时将会记录回复包信息.
* 收到回复包时将解密为指定的包
*
*
* // TODO: 2019/10/27 暂未实现. 计划中
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Response(
val responseClass: KClass<out ResponsePacket>
)

View File

@ -1,27 +0,0 @@
package net.mamoe.mirai.network.protocol.tim.packet
import kotlinx.io.core.ByteReadPacket
/**
* 返回包.
* 在登录完成后, 任意一个 [OutgoingPacket] 发送后服务器都会给返回包. 则一个 [OutgoingPacket] 一定对应一个 [ResponsePacket]
* 它们都使用 sessionKey 解密.
* 它们都必须有一个公开的仅有一个 [ByteReadPacket] 参数的构造器.
*
* 注意: 需要指定 ID, 通过 [AnnotatedId].
*/
abstract class ResponsePacket(input: ByteReadPacket) : ServerPacket(input) {
/**
* 加密过的 [ResponsePacket]. 将会在处理时解密为对应的 [ResponsePacket]
*/
class Encrypted<P : ResponsePacket>(input: ByteReadPacket, val constructor: (ByteReadPacket) -> P) : ServerPacket(input) {
fun decrypt(sessionKey: ByteArray): P = constructor(decryptBy(sessionKey)).applySequence(sequenceId)
}
companion object {
@Suppress("FunctionName")
inline fun <reified P : ResponsePacket> Encrypted(input: ByteReadPacket): Encrypted<P> = Encrypted(input) { P::class.constructors.first().call(it) }
}
}

View File

@ -3,78 +3,30 @@
package net.mamoe.mirai.network.protocol.tim.packet
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.Closeable
import kotlinx.io.core.IoBuffer
import kotlinx.io.core.readBytes
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.utils.decryptBy
import net.mamoe.mirai.utils.io.ByteArrayPool
import net.mamoe.mirai.utils.io.hexToBytes
import net.mamoe.mirai.utils.io.parseServerPacket
import net.mamoe.mirai.utils.io.toReadPacket
import kotlin.properties.Delegates
/**
* 来自服务器的数据包
*
* @see parseServerPacket 解析包种类
*/
abstract class ServerPacket(val input: ByteReadPacket) : Packet, Closeable {
override val packetId: PacketId by lazy {
(this::class.annotations.firstOrNull { it is AnnotatedId } as? AnnotatedId)?.id
?: error("Annotation AnnotatedId not found")
}
fun ByteReadPacket.decryptBy(key: ByteArray): ByteReadPacket = decryptAsByteArray(key) { data -> ByteReadPacket(data, 0) }
fun ByteReadPacket.decryptBy(key: IoBuffer): ByteReadPacket = decryptAsByteArray(key) { data -> ByteReadPacket(data, 0) }
fun ByteReadPacket.decryptBy(keyHex: String): ByteReadPacket = decryptBy(keyHex.hexToBytes())
override var sequenceId: UShort by Delegates.notNull()
open fun decode() {
}
override fun close() = this.input.close()
override fun toString(): String = this.packetToString()
fun <S : ServerPacket> S.applySequence() = this.applySequence(this@ServerPacket.sequenceId)
}
fun <S : ServerPacket> S.applySequence(sequenceId: UShort): S {
this.sequenceId = sequenceId
return this
}
fun ServerPacket.decryptBy(key: ByteArray): ByteReadPacket = decryptAsByteArray(key) { data -> ByteReadPacket(data, 0) }
fun ServerPacket.decryptBy(key: IoBuffer): ByteReadPacket = decryptAsByteArray(key) { data -> ByteReadPacket(data, 0) }
fun ServerPacket.decryptBy(keyHex: String): ByteReadPacket = this.decryptBy(keyHex.hexToBytes())
fun ServerPacket.decryptBy(key1: ByteArray, key2: ByteArray): ByteReadPacket =
this.decryptAsByteArray(key1) { data ->
data.decryptBy(key2).toReadPacket()
}
fun ServerPacket.decryptBy(key1: String, key2: ByteArray): ByteReadPacket = this.decryptBy(key1.hexToBytes(), key2)
fun ServerPacket.decryptBy(key1: String, key2: IoBuffer): ByteReadPacket =
this.decryptBy(key1.hexToBytes(), key2.readBytes())
fun ServerPacket.decryptBy(key1: ByteArray, key2: String): ByteReadPacket = this.decryptBy(key1, key2.hexToBytes())
fun ServerPacket.decryptBy(keyHex1: String, keyHex2: String): ByteReadPacket =
this.decryptBy(keyHex1.hexToBytes(), keyHex2.hexToBytes())
inline fun <R> ServerPacket.decryptAsByteArray(key: ByteArray, consumer: (ByteArray) -> R): R =
inline fun <R> ByteReadPacket.decryptAsByteArray(key: ByteArray, consumer: (ByteArray) -> R): R =
ByteArrayPool.useInstance {
val length = input.remaining.toInt() - 1
input.readFully(it, 0, length)
val length = remaining.toInt()
readFully(it, 0, length)
consumer(it.decryptBy(key, length))
}.also { input.close() }
}.also { close() }
inline fun <R> ServerPacket.decryptAsByteArray(keyHex: String, consumer: (ByteArray) -> R): R =
inline fun <R> ByteReadPacket.decryptAsByteArray(keyHex: String, consumer: (ByteArray) -> R): R =
this.decryptAsByteArray(keyHex.hexToBytes(), consumer)
inline fun <R> ServerPacket.decryptAsByteArray(key: IoBuffer, consumer: (ByteArray) -> R): R =
inline fun <R> ByteReadPacket.decryptAsByteArray(key: IoBuffer, consumer: (ByteArray) -> R): R =
ByteArrayPool.useInstance {
val length = input.remaining.toInt() - 1
input.readFully(it, 0, length)
val length = remaining.toInt()
readFully(it, 0, length)
consumer(it.decryptBy(key, length))
}.also { input.close() }
}.also { close() }

View File

@ -2,31 +2,6 @@
package net.mamoe.mirai.network.protocol.tim.packet
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.readBytes
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.io.toUHexString
class UnknownServerPacket(
input: ByteReadPacket,
override val packetId: PacketId,
override var sequenceId: UShort
) : ServerPacket(input) {
override fun decode() {
val raw = this.input.readBytes()
MiraiLogger.debug("UnknownServerPacket data: " + raw.toUHexString())
}
class Encrypted(
input: ByteReadPacket,
override val packetId: PacketId,
override var sequenceId: UShort
) : ServerPacket(input) {
fun decrypt(sessionKey: ByteArray): UnknownServerPacket =
UnknownServerPacket(this.decryptBy(sessionKey), this.packetId, this.sequenceId)
}
}
/*
ID: 00 17

View File

@ -4,66 +4,108 @@ package net.mamoe.mirai.network.protocol.tim.packet.action
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUInt
import kotlinx.io.core.readUShort
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.utils.io.encryptAndWrite
import net.mamoe.mirai.utils.io.writeHex
import net.mamoe.mirai.utils.io.writeQQ
import net.mamoe.mirai.network.protocol.tim.packet.action.CanAddFriendResponse.State
import net.mamoe.mirai.utils.io.*
import kotlin.properties.Delegates
// 01BC 曾用名查询. 查到的是这个人的
// 发送 00 00
// 3E 03 3F A2 //bot
// 59 17 3E 05 //目标
//
// 接受: 00 00 00 03
// [00 00 00 0C] E6 A5 BC E4 B8 8A E5 B0 8F E7 99 BD
// [00 00 00 10] 68 69 6D 31 38 38 E7 9A 84 E5 B0 8F 64 69 63 6B
// [00 00 00 0F] E4 B8 B6 E6 9A 97 E8 A3 94 E5 89 91 E9 AD 94
/**
* 查询某人与机器人账号有关的曾用名 (备注).
*
* 曾用名可能是:
* - 昵称
* - 共同群内的群名片
*/
@PacketVersion(date = "2019.11.02", timVersion = "2.3.2.21173")
object QueryPreviousNamePacket : SessionPacketFactory<QueryPreviousNameResponse>() {
operator fun invoke(
bot: UInt,
sessionKey: SessionKey,
target: UInt
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
writeZero(2)
writeQQ(bot)
writeQQ(target)
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): QueryPreviousNameResponse =
QueryPreviousNameResponse().apply {
names = Array(readUInt().toInt()) {
discardExact(2)
readUShortLVString()
}
}
}
class QueryPreviousNameResponse : Packet {
lateinit var names: Array<String>
}
// 需要验证消息
// 0065 发送 03 07 57 37 E8
// 0065 接受 03 07 57 37 E8 10 40 00 00 10 14 20 00 00 00 00 00 00 00 01 00 00 00 00 00
/**
* 添加好友结果
*
* @author Him188moe
*/
enum class AddFriendResult {
/**
* 等待对方处理
*/
WAITING_FOR_AGREEMENT,
/**
* 和对方已经是好友了
*/
ALREADY_ADDED,
/**
* 对方设置为不添加好友等
*/
FAILED,
}
/**
* 向服务器检查是否可添加某人为好友
*
* @author Him188moe
*/
@Response(CanAddFriendPacket.Response::class)
@AnnotatedId(KnownPacketId.CAN_ADD_FRIEND)
object CanAddFriendPacket : OutgoingPacketBuilder {
object CanAddFriendPacket : SessionPacketFactory<CanAddFriendResponse>() {
operator fun invoke(
bot: UInt,
qq: UInt,
sessionKey: ByteArray
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
writeHex(TIMProtocol.fixVer2)
encryptAndWrite(sessionKey) {
writeQQ(qq)
}
sessionKey: SessionKey
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
writeQQ(qq)
}
@AnnotatedId(KnownPacketId.CAN_ADD_FRIEND)
class Response(input: ByteReadPacket) : ResponsePacket(input) {
lateinit var state: State
enum class State {
/**
* 已经添加
*/
ALREADY_ADDED,
/**
* 需要验证信息
*/
REQUIRE_VERIFICATION,
/**
* 不需要验证信息
*/
NOT_REQUIRE_VERIFICATION,
/**
* 对方拒绝添加
*/
FAILED,
}
override fun decode() = with(input) {
//需要验证信息 00 23 24 8B 00 01
if (input.remaining > 20) {//todo check
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): CanAddFriendResponse =
CanAddFriendResponse().apply {
if (remaining > 20) {//todo check
state = State.ALREADY_ADDED
return
return@apply
}
discardExact(4)//对方qq号
qq = readUInt()
state = when (val state = readUShort().toUInt()) {
0x00u -> State.NOT_REQUIRE_VERIFICATION
0x01u -> State.REQUIRE_VERIFICATION//需要验证信息
@ -74,18 +116,43 @@ object CanAddFriendPacket : OutgoingPacketBuilder {
else -> throw IllegalStateException(state.toString())
}
}
}
class CanAddFriendResponse : Packet {
var qq: UInt by Delegates.notNull()
lateinit var state: State
enum class State {
/**
* 已经添加
*/
ALREADY_ADDED,
/**
* 需要验证信息
*/
REQUIRE_VERIFICATION,
/**
* 不需要验证信息
*/
NOT_REQUIRE_VERIFICATION,
/**
* 对方拒绝添加
*/
FAILED,
}
}
/**
* 请求添加好友
*/
@AnnotatedId(KnownPacketId.CAN_ADD_FRIEND)
object AddFriendPacket : OutgoingPacketBuilder {
object AddFriendPacket : SessionPacketFactory<AddFriendPacket.AddFriendResponse>() {
operator fun invoke(
bot: UInt,
qq: UInt,
sessionKey: ByteArray
sessionKey: SessionKey
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
writeHex(TIMProtocol.fixVer2)
@ -94,28 +161,11 @@ object AddFriendPacket : OutgoingPacketBuilder {
writeQQ(qq)
}
}
}
/**
* 添加好友/群的回复
*/
abstract class ServerAddContactResponsePacket(input: ByteReadPacket) : ServerPacket(input) {
class AddFriendResponse : Packet
class Raw(input: ByteReadPacket) : ServerPacket(input) {
override fun decode() {
}
fun distribute(): ServerAddContactResponsePacket {
TODO()
}
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
fun decrypt(sessionKey: ByteArray): Raw = Raw(decryptBy(sessionKey))
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): AddFriendResponse {
TODO()
}
}
}

View File

@ -1,23 +0,0 @@
package net.mamoe.mirai.network.protocol.tim.packet.action
/**
* 添加好友结果
*
* @author Him188moe
*/
enum class AddFriendResult {
/**
* 等待对方处理
*/
WAITING_FOR_AGREEMENT,
/**
* 和对方已经是好友了
*/
ALREADY_ADDED,
/**
* 对方设置为不添加好友等
*/
FAILED,
}

View File

@ -6,6 +6,7 @@ import com.soywiz.klock.Date
import kotlinx.io.core.*
import net.mamoe.mirai.contact.Gender
import net.mamoe.mirai.contact.Profile
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.utils.io.*
import kotlin.properties.Delegates
@ -15,10 +16,12 @@ import kotlin.properties.Delegates
* 请求获取头像
*/
@AnnotatedId(KnownPacketId.REQUEST_PROFILE_AVATAR)
object RequestProfilePicturePacket : OutgoingPacketBuilder {
object RequestProfilePicturePacket : SessionPacketFactory<NoPacket>() {
operator fun invoke(): OutgoingPacket = buildOutgoingPacket {
TODO()
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>) = NoPacket
}
/**
@ -27,84 +30,22 @@ object RequestProfilePicturePacket : OutgoingPacketBuilder {
* @see Profile
*/
@AnnotatedId(KnownPacketId.REQUEST_PROFILE_DETAILS)
object RequestProfileDetailsPacket : OutgoingPacketBuilder {
object RequestProfileDetailsPacket : SessionPacketFactory<RequestProfileDetailsResponse>() {
//00 01 3E F8 FB E3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5
//00 01 B1 89 BE 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5
//00 01 87 73 86 9D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5
operator fun invoke(
bot: UInt,
qq: UInt,
sessionKey: ByteArray
sessionKey: SessionKey
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
writeUShort(0x01u)
writeUInt(qq)
writeHex("00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5")
}
@AnnotatedId(KnownPacketId.REQUEST_PROFILE_DETAILS)
class Response(input: ByteReadPacket) : ResponsePacket(input) {
var qq: UInt by Delegates.notNull()
lateinit var profile: Profile
//00 01 00 99 6B F8 D2 00 00 00 00 00 29
// 4E 22 00 0F E4 B8 8B E9 9B A8 E6 97 B6 E6 B5 81 E6 B3 AA 4E 25 00 00 4E 26 00 0C E4 B8 AD E5 9B BD E6 B2 B3 E5 8C 97 4E 27 00 0B 30 33 31 39 39 39 39 39 39 39 39
// 4E 29 [00 01] 01 4E 2A 00 00 4E 2B 00 17 6D 61 69 6C 2E 71 71 32 35 37 33 39 39 30 30 39 38 2E 40 2E 63 6F 6D 4E 2D 00 00 4E 2E 00 02 31 00 4E 2F 00 04 36 37 38 00 4E 30 00 00 4E 31 00 01 00 4E 33 00 00 4E 35 00 00 4E 36 00 01 00 4E 37 00 01 00 4E 38 00 01 00 4E 3F 00 04 07 C1 01 01 4E 40 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 4E 41 00 02 00 00 4E 42 00 02 00 00 4E 43 00 02 00 00 4E 45 00 01 22 4E 49 00 04 00 00 00 00 4E 4B 00 04 00 00 00 00 4E 4F 00 01 00 4E 54 00 00 4E 5B 00 00 52 0B 00 04 00 C0 00 01 52 0F 00 14 00 00 00 00 00 00 00 00 12 00 00 48 09 10 00 00 00 00 00 00 5D C2 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 5D C8 00 00 65 97 00 01 08 69 9D 00 04 00 00 00 00 69 A9 00 00 9D A5 00 02 00 01 A4 91 00 02 00 00 A4 93 00 02 00 00 A4 94 00 02 00 00 A4 9C 00 02 00 00 A4 B5 00 02 00 00
//00 01 00 87 73 86 9D 00 00 00 00 00 29 4E 22 00 15 E6 98 AF E6 9C 9D E8 8F 8C E4 B8 8D E7 9F A5 E6 99 A6 E6 9C 94 4E 25 00 00 4E 26 00 00 4E 27 00 00
// 4E 29 [00 01] 01 4E 2A 00 00 4E 2B 00 00 4E 2D 00 00 4E 2E 00 02 31 00 4E 2F 00 04 37 32 30 00 4E 30 00 00 4E 31 00 01 01 4E 33 00 00 4E 35 00 00 4E 36 00 01 00 4E 37 00 01 04 4E 38 00 01 00 4E 3F 00 04 07 CF 00 00 4E 40 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 4E 41 00 02 00 00 4E 42 00 02 00 00 4E 43 00 02 00 00 4E 45 00 01 13 4E 49 00 04 00 00 00 00 4E 4B 00 04 00 00 00 00 4E 4F 00 01 00 4E 54 00 00 4E 5B 00 04 00 00 00 00 52 0B 00 04 13 80 02 00 52 0F 00 14 00 04 02 00 00 00 00 00 12 04 10 58 89 50 C0 00 22 00 00 00 5D C2 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 5D C8 00 00 65 97 00 01 08 69 9D 00 04 00 00 00 00 69 A9 00 00 9D A5 00 02 00 01 A4 91 00 02 00 00 A4 93 00 02 00 01 A4 94 00 02 00 00 A4 9C 00 02 00 00 A4 B5 00 02 00 00
//00 01 00 76 E4 B8 DD
// 00 00 00 00 00 29
// 4E 22 [00 0E] 73 74 65 61 6D 63 68 69 6E 61 2E 66 75 6E //昵称
// 4E 25 [00 06] 34 33 33 31 30 30 //邮编
// 4E 26 [00 09] E4 B8 8D E7 9F A5 E9 81 93 //?
// 4E 27 [00 0A] 31 33 38 2A 2A 2A 2A 2A 2A 2A // 手机号
// 4E 29 [00 01] 02 性别, 女02, 男01
// 4E 2A [00 00]
// 4E 2B [00 00]
// 4E 2D [00 23] 68 74 74 70 3A 2F 2F 77 77 77 2E 34 33 39 39 2E 63 6F 6D 2F 66 6C 61 73 68 2F 33 32 39 37 39 2E 68 74 6D //http://www.4399.com/flash/32979.htm //???
// 4E 2E [00 02] 31 00
// 4E 2F [00 04] 36 30 33 00
// 4E 30 [00 00]
// 4E 31 [00 01] 00
// 4E 33 [00 00]
// 4E 35 [00 00]
// 4E 36 [00 01] 0A
// 4E 37 [00 01] 06
// 4E 38 [00 01] 00
// 4E 3F [00 04] 07 DD 0B 13 生日 short byte byte
// 4E 40 [00 0C] 00 41 42 57 0// 0 00 00 00 00 00 00 00
// 4E 41 [00 02] 08 04
// 4E 42 [00 02] 00 00
// 4E 43 [00 02] 0C 04
// 4E 45 [00 01] 05
// 4E 49 [00 04] 00 00 00 00
// 4E 4B [00 04] 00 00 00 00
// 4E 4F [00 01] 06
// 4E 54 [00 00]
// 4E 5B [00 04] 00 00 00 00
// 52 0B [00 04] 13 80 02 00
// 52 0F [00 14] 01 00 00 00 00 00 00 00 52 00 40 48 89 50 80 02 00 00 03 00
// 5D C2 [00 0C] 00 41 42 57 00 00 00 00 00 00 00 00
// 5D C8 [00 00]
// 65 97 [00 01] 07
// 69 9D [00 04] 00 00 00 00
// 69 A9 [00 00]
// 9D A5 [00 02] 00 01
// A4 91 [00 02] 00 00
// A4 93 [00 02] 00 01
// A4 94 [00 02] 00 00
// A4 9C [00 02] 00 00
// A4 B5 [00 02] 00 00
/*
00 01 00 76 E4 B8 DD 00 00 00 00 00 29
4E 22 00 0E 73 74 65 61 6D 63 68 69 6E 61 2E 66 75 6E 4E 25 00 06 34 33 33 31 30 30 4E 26 00 09 E4 B8 8D E7 9F A5 E9 81 93 4E 27 00 0A 31 33 38 2A 2A 2A 2A 2A 2A 2A 4E 29 00 01 01 4E 2A 00 00 4E 2B 00 00 4E 2D 00 23 68 74 74 70 3A 2F 2F 77 77 77 2E 34 33 39 39 2E 63 6F 6D 2F 66 6C 61 73 68 2F 33 32 39 37 39 2E 68 74 6D 4E 2E 00 02 31 00 4E 2F 00 04 36 30 33 00 4E 30 00 00 4E 31 00 01 00 4E 33 00 00 4E 35 00 00 4E 36 00 01 0A 4E 37 00 01 06 4E 38 00 01 00 4E 3F 00 04 07 DD 0B 13 4E 40 00 0C 00 41 42 57 00 00 00 00 00 00 00 00 4E 41 00 02 08 04 4E 42 00 02 00 00 4E 43 00 02 0C 04 4E 45 00 01 05 4E 49 00 04 00 00 00 00 4E 4B 00 04 00 00 00 00 4E 4F 00 01 06 4E 54 00 00 4E 5B 00 04 00 00 00 00 52 0B 00 04 13 80 02 00 52 0F 00 14 01 00 00 00 00 00 00 00 52 00 40 48 89 50 80 02 00 00 03 00 5D C2 00 0C 00 41 42 57 00 00 00 00 00 00 00 00 5D C8 00 00 65 97 00 01 07 69 9D 00 04 00 00 00 00 69 A9 00 00 9D A5 00 02 00 01 A4 91 00 02 00 00 A4 93 00 02 00 01 A4 94 00 02 00 00 A4 9C 00 02 00 00 A4 B5 00 02 00 00
*/
override fun decode() = with(input) {
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): RequestProfileDetailsResponse =
RequestProfileDetailsResponse().apply {
discardExact(3)
qq = readUInt()
discardExact(6)
@ -125,7 +66,69 @@ object RequestProfileDetailsPacket : OutgoingPacketBuilder {
)
map.clear()
}
}
}
@AnnotatedId(KnownPacketId.REQUEST_PROFILE_DETAILS)
class RequestProfileDetailsResponse : Packet {
var qq: UInt by Delegates.notNull()
lateinit var profile: Profile
//00 01 00 99 6B F8 D2 00 00 00 00 00 29
// 4E 22 00 0F E4 B8 8B E9 9B A8 E6 97 B6 E6 B5 81 E6 B3 AA 4E 25 00 00 4E 26 00 0C E4 B8 AD E5 9B BD E6 B2 B3 E5 8C 97 4E 27 00 0B 30 33 31 39 39 39 39 39 39 39 39
// 4E 29 [00 01] 01 4E 2A 00 00 4E 2B 00 17 6D 61 69 6C 2E 71 71 32 35 37 33 39 39 30 30 39 38 2E 40 2E 63 6F 6D 4E 2D 00 00 4E 2E 00 02 31 00 4E 2F 00 04 36 37 38 00 4E 30 00 00 4E 31 00 01 00 4E 33 00 00 4E 35 00 00 4E 36 00 01 00 4E 37 00 01 00 4E 38 00 01 00 4E 3F 00 04 07 C1 01 01 4E 40 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 4E 41 00 02 00 00 4E 42 00 02 00 00 4E 43 00 02 00 00 4E 45 00 01 22 4E 49 00 04 00 00 00 00 4E 4B 00 04 00 00 00 00 4E 4F 00 01 00 4E 54 00 00 4E 5B 00 00 52 0B 00 04 00 C0 00 01 52 0F 00 14 00 00 00 00 00 00 00 00 12 00 00 48 09 10 00 00 00 00 00 00 5D C2 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 5D C8 00 00 65 97 00 01 08 69 9D 00 04 00 00 00 00 69 A9 00 00 9D A5 00 02 00 01 A4 91 00 02 00 00 A4 93 00 02 00 00 A4 94 00 02 00 00 A4 9C 00 02 00 00 A4 B5 00 02 00 00
//00 01 00 87 73 86 9D 00 00 00 00 00 29 4E 22 00 15 E6 98 AF E6 9C 9D E8 8F 8C E4 B8 8D E7 9F A5 E6 99 A6 E6 9C 94 4E 25 00 00 4E 26 00 00 4E 27 00 00
// 4E 29 [00 01] 01 4E 2A 00 00 4E 2B 00 00 4E 2D 00 00 4E 2E 00 02 31 00 4E 2F 00 04 37 32 30 00 4E 30 00 00 4E 31 00 01 01 4E 33 00 00 4E 35 00 00 4E 36 00 01 00 4E 37 00 01 04 4E 38 00 01 00 4E 3F 00 04 07 CF 00 00 4E 40 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 4E 41 00 02 00 00 4E 42 00 02 00 00 4E 43 00 02 00 00 4E 45 00 01 13 4E 49 00 04 00 00 00 00 4E 4B 00 04 00 00 00 00 4E 4F 00 01 00 4E 54 00 00 4E 5B 00 04 00 00 00 00 52 0B 00 04 13 80 02 00 52 0F 00 14 00 04 02 00 00 00 00 00 12 04 10 58 89 50 C0 00 22 00 00 00 5D C2 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 5D C8 00 00 65 97 00 01 08 69 9D 00 04 00 00 00 00 69 A9 00 00 9D A5 00 02 00 01 A4 91 00 02 00 00 A4 93 00 02 00 01 A4 94 00 02 00 00 A4 9C 00 02 00 00 A4 B5 00 02 00 00
//00 01 00 76 E4 B8 DD
// 00 00 00 00 00 29
// 4E 22 [00 0E] 73 74 65 61 6D 63 68 69 6E 61 2E 66 75 6E //昵称
// 4E 25 [00 06] 34 33 33 31 30 30 //邮编
// 4E 26 [00 09] E4 B8 8D E7 9F A5 E9 81 93 //?
// 4E 27 [00 0A] 31 33 38 2A 2A 2A 2A 2A 2A 2A // 手机号
// 4E 29 [00 01] 02 性别, 女02, 男01
// 4E 2A [00 00]
// 4E 2B [00 00]
// 4E 2D [00 23] 68 74 74 70 3A 2F 2F 77 77 77 2E 34 33 39 39 2E 63 6F 6D 2F 66 6C 61 73 68 2F 33 32 39 37 39 2E 68 74 6D //http://www.4399.com/flash/32979.htm //???
// 4E 2E [00 02] 31 00
// 4E 2F [00 04] 36 30 33 00
// 4E 30 [00 00]
// 4E 31 [00 01] 00
// 4E 33 [00 00]
// 4E 35 [00 00]
// 4E 36 [00 01] 0A
// 4E 37 [00 01] 06
// 4E 38 [00 01] 00
// 4E 3F [00 04] 07 DD 0B 13 生日 short byte byte
// 4E 40 [00 0C] 00 41 42 57 0// 0 00 00 00 00 00 00 00
// 4E 41 [00 02] 08 04
// 4E 42 [00 02] 00 00
// 4E 43 [00 02] 0C 04
// 4E 45 [00 01] 05
// 4E 49 [00 04] 00 00 00 00
// 4E 4B [00 04] 00 00 00 00
// 4E 4F [00 01] 06
// 4E 54 [00 00]
// 4E 5B [00 04] 00 00 00 00
// 52 0B [00 04] 13 80 02 00
// 52 0F [00 14] 01 00 00 00 00 00 00 00 52 00 40 48 89 50 80 02 00 00 03 00
// 5D C2 [00 0C] 00 41 42 57 00 00 00 00 00 00 00 00
// 5D C8 [00 00]
// 65 97 [00 01] 07
// 69 9D [00 04] 00 00 00 00
// 69 A9 [00 00]
// 9D A5 [00 02] 00 01
// A4 91 [00 02] 00 00
// A4 93 [00 02] 00 01
// A4 94 [00 02] 00 00
// A4 9C [00 02] 00 00
// A4 B5 [00 02] 00 00
/*
00 01 00 76 E4 B8 DD 00 00 00 00 00 29
4E 22 00 0E 73 74 65 61 6D 63 68 69 6E 61 2E 66 75 6E 4E 25 00 06 34 33 33 31 30 30 4E 26 00 09 E4 B8 8D E7 9F A5 E9 81 93 4E 27 00 0A 31 33 38 2A 2A 2A 2A 2A 2A 2A 4E 29 00 01 01 4E 2A 00 00 4E 2B 00 00 4E 2D 00 23 68 74 74 70 3A 2F 2F 77 77 77 2E 34 33 39 39 2E 63 6F 6D 2F 66 6C 61 73 68 2F 33 32 39 37 39 2E 68 74 6D 4E 2E 00 02 31 00 4E 2F 00 04 36 30 33 00 4E 30 00 00 4E 31 00 01 00 4E 33 00 00 4E 35 00 00 4E 36 00 01 0A 4E 37 00 01 06 4E 38 00 01 00 4E 3F 00 04 07 DD 0B 13 4E 40 00 0C 00 41 42 57 00 00 00 00 00 00 00 00 4E 41 00 02 08 04 4E 42 00 02 00 00 4E 43 00 02 0C 04 4E 45 00 01 05 4E 49 00 04 00 00 00 00 4E 4B 00 04 00 00 00 00 4E 4F 00 01 06 4E 54 00 00 4E 5B 00 04 00 00 00 00 52 0B 00 04 13 80 02 00 52 0F 00 14 01 00 00 00 00 00 00 00 52 00 40 48 89 50 80 02 00 00 03 00 5D C2 00 0C 00 41 42 57 00 00 00 00 00 00 00 00 5D C8 00 00 65 97 00 01 07 69 9D 00 04 00 00 00 00 69 A9 00 00 9D A5 00 02 00 01 A4 91 00 02 00 00 A4 93 00 02 00 01 A4 94 00 02 00 00 A4 9C 00 02 00 00 A4 B5 00 02 00 00
*/
}
fun main() {

View File

@ -5,6 +5,7 @@ package net.mamoe.mirai.network.protocol.tim.packet.action
import kotlinx.io.core.*
import net.mamoe.mirai.message.MessageChain
import net.mamoe.mirai.message.internal.toPacket
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.utils.io.*
@ -12,62 +13,58 @@ import net.mamoe.mirai.utils.md5
@AnnotatedId(KnownPacketId.SEND_FRIEND_MESSAGE)
@PacketVersion(date = "2019.10.19", timVersion = "2.3.2.21173")
object SendFriendMessagePacket : OutgoingPacketBuilder {
object SendFriendMessagePacket : SessionPacketFactory<SendFriendMessagePacket.Response>() {
operator fun invoke(
botQQ: UInt,
targetQQ: UInt,
sessionKey: ByteArray,
sessionKey: SessionKey,
message: MessageChain
): OutgoingPacket = buildOutgoingPacket {
): OutgoingPacket = buildSessionPacket(botQQ, sessionKey) {
writeQQ(botQQ)
writeHex(TIMProtocol.version0x02)
writeQQ(targetQQ)
writeHex("00 00 00 08 00 01 00 04 00 00 00 00")
writeHex("38 03")
writeQQ(botQQ)
writeQQ(targetQQ)
writeFully(md5(buildPacket { writeQQ(targetQQ); writeFully(sessionKey) }.readBytes()))
writeHex("00 0B")
writeRandom(2)
writeTime()
writeHex(
"01 1D" +
" 00 00 00 00"
)
encryptAndWrite(sessionKey) {
writeQQ(botQQ)
writeQQ(targetQQ)
writeHex("00 00 00 08 00 01 00 04 00 00 00 00")
writeHex("38 03")
writeQQ(botQQ)
writeQQ(targetQQ)
writeFully(md5(buildPacket { writeQQ(targetQQ); writeFully(sessionKey) }.readBytes()))
writeHex("00 0B")
writeRandom(2)
writeTime()
writeHex(
"01 1D" +
" 00 00 00 00"
)
//消息过多要分包发送
//如果只有一个
writeByte(0x01)
writeByte(0)//第几个包
writeUByte(0x00u)
//如果大于一个,
//writeByte(0x02)//数量
//writeByte(0)//第几个包
//writeByte(0x91)//why?
//消息过多要分包发送
//如果只有一个
writeByte(0x01)
writeByte(0)//第几个包
writeUByte(0x00u)
//如果大于一个,
//writeByte(0x02)//数量
//writeByte(0)//第几个包
//writeByte(0x91)//why?
writeHex("00 01 4D 53 47 00 00 00 00 00")
writeTime()
writeRandom(4)
writeHex("00 00 00 00 0C 00 86")
writeHex(TIMProtocol.messageConstNewest)
writeZero(2)
writeHex("00 01 4D 53 47 00 00 00 00 00")
writeTime()
writeRandom(4)
writeHex("00 00 00 00 0C 00 86")
writeHex(TIMProtocol.messageConstNewest)
writeZero(2)
writePacket(message.toPacket(false))
writePacket(message.toPacket(false))
/*
//Plain text
val bytes = event.toPacket()
it.writeByte(0x01)
it.writeShort(bytes.size + 3)
it.writeByte(0x01)
it.writeShort(bytes.size)
it.write(bytes)*/
}
/*
//Plain text
val bytes = event.toPacket()
it.writeByte(0x01)
it.writeShort(bytes.size + 3)
it.writeByte(0x01)
it.writeShort(bytes.size)
it.write(bytes)*/
}
@AnnotatedId(KnownPacketId.SEND_FRIEND_MESSAGE)
class Response(input: ByteReadPacket) : ResponsePacket(input)
object Response : Packet
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): Response = Response
}

View File

@ -6,17 +6,16 @@ import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.contact.GroupInternalId
import net.mamoe.mirai.message.MessageChain
import net.mamoe.mirai.message.internal.toPacket
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.utils.io.*
@AnnotatedId(KnownPacketId.SEND_GROUP_MESSAGE)
object SendGroupMessagePacket : OutgoingPacketBuilder {
object SendGroupMessagePacket : SessionPacketFactory<SendGroupMessagePacket.Response>() {
operator fun invoke(
botQQ: UInt,
groupInternalId: GroupInternalId,
sessionKey: ByteArray,
sessionKey: SessionKey,
message: MessageChain
): OutgoingPacket = buildOutgoingPacket {
writeQQ(botQQ)
@ -46,6 +45,7 @@ object SendGroupMessagePacket : OutgoingPacketBuilder {
}
}
@AnnotatedId(KnownPacketId.SEND_GROUP_MESSAGE)
class Response(input: ByteReadPacket) : ResponsePacket(input)
object Response : Packet
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): Response = Response
}

View File

@ -12,7 +12,7 @@ import kotlinx.io.core.*
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.event.events.FriendImageIdObtainedEvent
import net.mamoe.mirai.message.ImageId
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.network.protocol.tim.packet.action.FriendImageIdRequestPacket.Response.State.*
import net.mamoe.mirai.network.qqAccount
@ -126,18 +126,19 @@ internal suspend inline fun HttpClient.postImage(
imageInput.close()
}
/*
/**
* 似乎没有必要. 服务器的返回永远都是 01 00 00 00 02 00 00
*/
@Deprecated("Useless packet")
@AnnotatedId(KnownPacketId.SUBMIT_IMAGE_FILE_NAME)
@PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
object SubmitImageFilenamePacket : OutgoingPacketBuilder {
object SubmitImageFilenamePacket : PacketFactory {
operator fun invoke(
bot: UInt,
target: UInt,
filename: String,
sessionKey: ByteArray
sessionKey: SessionKey
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
writeHex(TIMProtocol.fixVer2)//?
@ -166,9 +167,8 @@ object SubmitImageFilenamePacket : OutgoingPacketBuilder {
//解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1C 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 29 37 42 53 4B 48 32 44 35 54 51 28 5A 35 7D 35 24 56 5D 32 35 49 4E 2E 6A 70 67 00 00 03 73 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
}
@AnnotatedId(KnownPacketId.SUBMIT_IMAGE_FILE_NAME)
@PacketVersion(date = "2019.10.19", timVersion = "2.3.2.21173")
class Response(input: ByteReadPacket) : ResponsePacket(input) {
class Response {
override fun decode() = with(input) {
require(readBytes().contentEquals(expecting))
}
@ -177,7 +177,7 @@ object SubmitImageFilenamePacket : OutgoingPacketBuilder {
private val expecting = byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00)
}
}
}
}*/
/**
@ -188,10 +188,10 @@ object SubmitImageFilenamePacket : OutgoingPacketBuilder {
*/
@AnnotatedId(KnownPacketId.FRIEND_IMAGE_ID)
@PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
object FriendImageIdRequestPacket : OutgoingPacketBuilder {
object FriendImageIdRequestPacket : SessionPacketFactory<FriendImageIdRequestPacket.Response>() {
operator fun invoke(
bot: UInt,
sessionKey: ByteArray,
sessionKey: SessionKey,
target: UInt,
image: ExternalImage
) = buildOutgoingPacket {
@ -256,9 +256,8 @@ object FriendImageIdRequestPacket : OutgoingPacketBuilder {
}
@CorrespondingEvent(FriendImageIdObtainedEvent::class)
@AnnotatedId(KnownPacketId.FRIEND_IMAGE_ID)
@PacketVersion(date = "2019.11.1", timVersion = "2.3.2.21173")
class Response(input: ByteReadPacket) : ResponsePacket(input) {
class Response : Packet {
/**
* 访问 HTTP API 时需要使用的一个 key. 128
*/
@ -285,29 +284,29 @@ object FriendImageIdRequestPacket : OutgoingPacketBuilder {
*/
OVER_FILE_SIZE_MAX,
}
}
override fun decode() = with(input) {
discardExact(6)
if (readUByte() != UByte.MIN_VALUE) {
discardExact(60)
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): Response = Response().apply {
discardExact(6)
if (readUByte() != UByte.MIN_VALUE) {
discardExact(60)
@Suppress("ControlFlowWithEmptyBody")
while (readUByte().toUInt() != 0x4Au);
@Suppress("ControlFlowWithEmptyBody")
while (readUByte().toUInt() != 0x4Au);
uKey = readBytes(readUnsignedVarInt().toInt())//128
uKey = readBytes(readUnsignedVarInt().toInt())//128
discardExact(1)//52, id
imageId = ImageId(readString(readUnsignedVarInt().toInt()))//37
state = REQUIRE_UPLOAD
discardExact(1)//52, id
imageId = ImageId(readString(readUnsignedVarInt().toInt()))//37
state = REQUIRE_UPLOAD
} else {
val toDiscard = readUByte().toInt() - 37
if (toDiscard < 0) {
state = OVER_FILE_SIZE_MAX
} else {
val toDiscard = readUByte().toInt() - 37
if (toDiscard < 0) {
state = OVER_FILE_SIZE_MAX
} else {
discardExact(toDiscard)
imageId = ImageId(readString(37))
state = ALREADY_EXISTS
}
discardExact(toDiscard)
imageId = ImageId(readString(37))
state = ALREADY_EXISTS
}
}
}
@ -319,12 +318,12 @@ object FriendImageIdRequestPacket : OutgoingPacketBuilder {
*/
@AnnotatedId(KnownPacketId.GROUP_IMAGE_ID)
@PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
object GroupImageIdRequestPacket : OutgoingPacketBuilder {
object GroupImageIdRequestPacket : SessionPacketFactory<GroupImageIdRequestPacket.Response>() {
operator fun invoke(
bot: UInt,
groupInternalId: GroupInternalId,
image: ExternalImage,
sessionKey: ByteArray
sessionKey: SessionKey
) = buildOutgoingPacket {
writeQQ(bot)
writeHex("04 00 00 00 01 01 01 00 00 68 20 00 00 00 00 00 00 00 00")
@ -379,9 +378,8 @@ object GroupImageIdRequestPacket : OutgoingPacketBuilder {
private val value0x6A: UByteArray = ubyteArrayOf(0x05u, 0x32u, 0x36u, 0x36u, 0x35u, 0x36u)
@AnnotatedId(KnownPacketId.GROUP_IMAGE_ID)
@PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
class Response(input: ByteReadPacket) : ResponsePacket(input) {
class Response : Packet {
lateinit var state: State
/**
@ -403,19 +401,19 @@ object GroupImageIdRequestPacket : OutgoingPacketBuilder {
*/
OVER_FILE_SIZE_MAX,
}
}
override fun decode(): Unit = with(input) {
discardExact(6)//00 00 00 05 00 00
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): Response = Response().apply {
discardExact(6)//00 00 00 05 00 00
val length = remaining - 128 - 14
if (length < 0) {
state = if (readUShort().toUInt() == 0x0025u) State.OVER_FILE_SIZE_MAX else State.ALREADY_EXISTS
return@with
}
discardExact(length)
uKey = readBytes(128)
state = State.REQUIRE_UPLOAD
val length = remaining - 128 - 14
if (length < 0) {
state = if (readUShort().toUInt() == 0x0025u) Response.State.OVER_FILE_SIZE_MAX else Response.State.ALREADY_EXISTS
return@apply
}
discardExact(length)
uKey = readBytes(128)
state = Response.State.REQUIRE_UPLOAD
}
}

View File

@ -1,14 +0,0 @@
package net.mamoe.mirai.network.protocol.tim.packet.event
import kotlinx.io.core.ByteReadPacket
/**
* Android 客户端上线
*/
class ServerAndroidOnlineEventPacket(input: ByteReadPacket, eventIdentity: EventPacketIdentity) : ServerEventPacket(input, eventIdentity)
/**
* Android 客户端下线
*/
class ServerAndroidOfflineEventPacket(input: ByteReadPacket, eventIdentity: EventPacketIdentity) : ServerEventPacket(input, eventIdentity)

View File

@ -1,94 +0,0 @@
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.network.protocol.tim.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUInt
import net.mamoe.mirai.event.events.FriendConversationInitializedEvent
import net.mamoe.mirai.event.events.MemberPermissionChangedEvent
import net.mamoe.mirai.network.protocol.tim.packet.CorrespondingEvent
import net.mamoe.mirai.network.protocol.tim.packet.PacketVersion
import net.mamoe.mirai.utils.io.readString
import kotlin.properties.Delegates
/**
* 群文件上传
*/
class ServerGroupUploadFileEventPacket(input: ByteReadPacket, eventIdentity: EventPacketIdentity) :
ServerEventPacket(input, eventIdentity) {
private lateinit var xmlMessage: String
override fun decode() {
this.input.discardExact(60)
val size = this.input.readShort().toInt()
this.input.discardExact(3)
xmlMessage = this.input.readString(size)
}//todo test
}
class GroupMemberNickChangedEventPacket(input: ByteReadPacket, eventIdentity: EventPacketIdentity) :
ServerEventPacket(input, eventIdentity) {
private val groupId: UInt get() = eventIdentity.from
private val group: UInt get() = eventIdentity.from
override fun decode() {
// GroupId VarInt
// 00 00 00 08 00 0A 00 04 01 00 00 00 22 96 29 7B 01 01 00 00 F3 66 00 00 00 05 00 00 00 EE 00 00 00 05
// TODO ? 数据中没有哪个人的昵称改变了
}
}
/**
* 好友发起会话, 即在输入框输入了任意内容.
*/
@CorrespondingEvent(FriendConversationInitializedEvent::class)
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2.21173")
class FriendConversationInitializedEventPacket(input: ByteReadPacket, eventIdentity: EventPacketIdentity) :
ServerEventPacket(input, eventIdentity) {
var qq: UInt by Delegates.notNull()
// 00 00 00 00 3E 03 3F A2 00
override fun decode() = with(input) {
discardExact(4)// 00 00 00 00
qq = readUInt()
}
}
@CorrespondingEvent(MemberPermissionChangedEvent::class)
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2.21173")
class GroupMemberPermissionChangedEventPacket internal constructor(
input: ByteReadPacket,
eventIdentity: EventPacketIdentity
) :
ServerEventPacket(input, eventIdentity) {
val groupId: UInt get() = eventIdentity.from
var qq: UInt = 0u
lateinit var kind: MemberPermissionChangedEvent.Kind
override fun decode(): Unit = with(input) {
// 群里一个人变成管理员:
// 00 00 00 08 00 0A 00 04 01 00 00 00 22 96 29 7B 01 01 76 E4 B8 DD 01
// 取消管理员
// 00 00 00 08 00 0A 00 04 01 00 00 00 22 96 29 7B 01 00 76 E4 B8 DD 00
discardExact(remaining - 5)
qq = readUInt()
kind = when (readByte().toInt()) {
0x00 -> MemberPermissionChangedEvent.Kind.NO_LONGER_OPERATOR
0x01 -> MemberPermissionChangedEvent.Kind.BECOME_OPERATOR
else -> {
error("Could not determine permission change kind")
}
}
}
}
class ServerGroupUnknownChangedEventPacket(input: ByteReadPacket, eventIdentity: EventPacketIdentity) :
ServerEventPacket(input, eventIdentity) {
override fun decode() = with(input) {
//00 00 00 08 00 0A 00 04 01 00 00 00 22 96 29 7B 01 01 00 00 F3 55 00 00 00 05 00 00 00 E9 00 00 00 05
//00 00 00 08 00 0A 00 04 01 00 00 00 22 96 29 7B 01 01 00 00 F3 56 00 00 00 05 00 00 00 EA 00 00 00 05
}
}

View File

@ -1,133 +0,0 @@
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.network.protocol.tim.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUInt
import net.mamoe.mirai.message.MessageChain
import net.mamoe.mirai.message.NullMessageChain
import net.mamoe.mirai.message.internal.readMessageChain
import net.mamoe.mirai.network.protocol.tim.packet.PacketVersion
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.io.printTLVMap
import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.readTLVMap
import net.mamoe.mirai.utils.io.readUShortLVByteArray
import kotlin.properties.Delegates
enum class SenderPermission {
OWNER,
OPERATOR,
MEMBER;
}
@Suppress("EXPERIMENTAL_API_USAGE")
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2.21173")
class GroupMessageEventPacket(input: ByteReadPacket, eventIdentity: EventPacketIdentity) :
ServerEventPacket(input, eventIdentity) {
var groupNumber: UInt by Delegates.notNull()
var qq: UInt by Delegates.notNull()
lateinit var senderName: String
/**
* 发送方权限.
*/
lateinit var senderPermission: SenderPermission
var message: MessageChain = NullMessageChain
override fun decode() = with(input) {
discardExact(31)
groupNumber = readUInt()
discardExact(1)
qq = readUInt()
discardExact(48)
readUShortLVByteArray()
discardExact(2)//2个0x00
message = readMessageChain()
val map = readTLVMap(true)
if (map.containsKey(18u)) {
map.getValue(18u).read {
val tlv = readTLVMap(true)
//tlv.printTLVMap("消息结尾 tag=18 的 TLV")
////群主的18: 05 00 04 00 00 00 03 08 00 04 00 00 00 04 01 00 09 48 69 6D 31 38 38 6D 6F 65 03 00 01 04 04 00 04 00 00 00 08
//群主的 子map= {5=00 00 00 03, 8=00 00 00 04, 1=48 69 6D 31 38 38 6D 6F 65, 3=04, 4=00 00 00 08}
//管理员 子map= {5=00 00 00 03, 8=00 00 00 04, 2=65 6F 6D 38 38 31 6D 69 48, 3=02, 4=00 00 00 10}
//群成员 子map= {5=00 00 00 03, 8=00 00 00 04, 2=65 6F 6D 38 38 31 6D 69 48, 3=02}
// 4=08, 群主
// 没有4, 群员
// 4=10, 管理员
senderPermission = when (tlv.takeIf { it.containsKey(0x04u) }?.get(0x04u)?.getOrNull(3)?.toUInt()) {
null -> SenderPermission.MEMBER
0x08u -> SenderPermission.OWNER
0x10u -> SenderPermission.OPERATOR
else -> {
tlv.printTLVMap("TLV(tag=18) Map")
MiraiLogger.warning("Could not determine member permission, default permission MEMBER is being used")
SenderPermission.MEMBER
}
}
senderName = when {
tlv.containsKey(0x01u) -> kotlinx.io.core.String(tlv.getValue(0x01u))//这个人的qq昵称
tlv.containsKey(0x02u) -> kotlinx.io.core.String(tlv.getValue(0x02u))//这个人的群名片
else -> {
tlv.printTLVMap("TLV(tag=18) Map")
MiraiLogger.warning("Could not determine senderName")
"null"
}
}
}
}
}
}
//
//以前的消息: 00 00 00 25 00 08 00 02 00 01 00 09 00 06 00 01 00 00 00 01 00 0A 00 04 01 00 00 00 00 01 00 04 00 00 00 00 00 03 00 01 01 38 03 3E 03 3F A2 76 E4 B8 DD 58 2C 60 86 35 3A 30 B3 C7 63 4A 80 E7 CD 5B 64 00 0B 78 16 5D A3 0A FD 01 1D 00 00 00 00 01 00 00 00 01 4D 53 47 00 00 00 00 00 5D A3 0A FD AB 77 16 02 00 00 00 00 0C 00 86 22 00 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 00 00 01 00 04 01 00 01 36 0E 00 07 01 00 04 00 00 00 09 19 00 18 01 00 15 AA 02 12 9A 01 0F 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00
//刚刚的消息: 00 00 00 2D 00 05 00 02 00 01 00 06 00 04 00 01 2E 01 00 09 00 06 00 01 00 00 00 01 00 0A 00 04 01 00 00 00 00 01 00 04 00 00 00 00 00 03 00 01 01 38 03 3E 03 3F A2 76 E4 B8 DD 11 F4 B2 F2 1A E7 1F C4 F1 3F 23 FB 74 80 42 64 00 0B 78 1A 5D A3 26 C1 01 1D 00 00 00 00 01 00 00 00 01 4D 53 47 00 00 00 00 00 5D A3 26 C1 AA 34 08 42 00 00 00 00 0C 00 86 22 00 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 00 00 01 00 09 01 00 06 E4 BD A0 E5 A5 BD 0E 00 07 01 00 04 00 00 00 09 19 00 18 01 00 15 AA 02 12 9A 01 0F 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00
class FriendMessageEventPacket(input: ByteReadPacket, eventIdentity: EventPacketIdentity) :
ServerEventPacket(input, eventIdentity) {
val qq: UInt get() = eventIdentity.from
/**
* 是否是在这次登录之前的消息, 即消息记录
*/
var isPrevious: Boolean = false
var message: MessageChain by Delegates.notNull()
//来自自己发送给自己
//00 00 00 20 00 05 00 02 00 06 00 06 00 04 00 01 01 07 00 09 00 06 03 E9 20 02 EB 94 00 0A 00 04 01 00 00 00 0C 17 76 E4 B8 DD 76 E4 B8 DD 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0B A6 D2 5D A3 2A 3F 00 00 5D A3 2A 3F 01 00 00 00 00 4D 53 47 00 00 00 00 00 5D A3 2A 3F 0C 8A 59 3D 00 00 00 00 0A 00 86 02 00 06 E5 AE 8B E4 BD 93 00 00 01 00 06 01 00 03 31 32 33 19 00 1F 01 00 1C AA 02 19 08 00 88 01 00 9A 01 11 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 0E 00 0E 01 00 04 00 00 00 00 0A 00 04 00 00 00 00
override fun decode() = with(input) {
input.discardExact(2)
val l1 = readShort()
discardExact(1)//0x00
isPrevious = readByte().toInt() == 0x08
discardExact(l1.toInt() - 2)
//java.io.EOFException: Only 49 bytes were discarded of 69 requested
//抖动窗口消息
discardExact(69)
readUShortLVByteArray()//font
discardExact(2)//2个0x00
message = readMessageChain()
//val map: Map<Int, ByteArray> = readTLVMap(true).withDefault { byteArrayOf() }
//map.printTLVMap("readTLVMap")
//println("map.getValue(18)=" + map.getValue(18).toUHexString())
//19 00 38 01 00 35 AA 02 32 50 03 60 00 68 00 9A 01 29 08 09 20 BF 02 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 B8 03 00 C0 03 00 D0 03 00 E8 03 00 12 00 25 01 00 09 48 69 6D 31 38 38 6D 6F 65 03 00 01 04 04 00 04 00 00 00 08 05 00 04 00 00 00 01 08 00 04 00 00 00 01
/*
val offset = unknownLength0 + fontLength//57
event = MessageChain(PlainText(let {
val length = input.readShortAt(101 + offset)//
input.goto(103 + offset).readString(length.toInt())
}))*/
}
}

View File

@ -3,15 +3,20 @@
package net.mamoe.mirai.network.protocol.tim.packet.event
import kotlinx.io.core.*
import net.mamoe.mirai.event.events.MemberPermissionChangedEvent
import net.mamoe.mirai.message.internal.readMessageChain
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.network.sessionKey
import net.mamoe.mirai.qqAccount
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.io.*
/**
* 事件的识别 ID. [事件确认包][ServerEventPacket.EventResponse] 中被使用.
*/
data class EventPacketIdentity(
class EventPacketIdentity(
val from: UInt,//对于好友消息, 这个是发送人
val to: UInt,//对于好友消息, 这个是bot
internal val uniqueId: IoBuffer//8
@ -19,121 +24,214 @@ data class EventPacketIdentity(
override fun toString(): String = "($from->$to)"
}
enum class SenderPermission {
OWNER,
OPERATOR,
MEMBER;
}
object IgnoredEventPacket : Packet
class UnknownEventPacket(
val id: UnknownEventId
// TODO: 2019/11/5 补充包数据 , 用于输出
) : Packet
/**
* 事件包, 它将会分析 [事件ID][KnownEventId] 并解析事件为 [Packet]
*/
@NoLog
@Suppress("FunctionName")
object EventPacketFactory : PacketFactory<Packet, SessionKey>(SessionKey) {
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): Packet {
val eventIdentity = EventPacketIdentity(
from = readUInt(),
to = readUInt(),
uniqueId = readIoBuffer(8)
)
handler.sendPacket(EventPacketFactory(id, sequenceId, handler.bot.qqAccount, handler.sessionKey, eventIdentity))
discardExact(2)
return when (val type = EventId(readUShort())) {
is KnownEventId -> type.parser(this, eventIdentity)
is UnknownEventId -> UnknownEventPacket(type)
is IgnoredEventId -> IgnoredEventPacket
else -> throw AssertionError("Unknown EventId type")
}
}
operator fun invoke(
id: PacketId,
sequenceId: UShort,
bot: UInt,
sessionKey: SessionKey,
identity: EventPacketIdentity
): OutgoingPacket = buildOutgoingPacket(name = "EventPacket", id = id, sequenceId = sequenceId) {
writeQQ(bot)
writeHex(TIMProtocol.fixVer2)
encryptAndWrite(sessionKey) {
writeEventPacketIdentity(identity)
}
}
}
fun BytePacketBuilder.writeEventPacketIdentity(identity: EventPacketIdentity) = with(identity) {
writeUInt(from)
writeUInt(to)
writeFully(uniqueId)
}
fun <S : ServerEventPacket> S.applyId(id: PacketId): S {
this.packetId = id
return this
typealias EventPacketParser = ByteReadPacket.(EventPacketIdentity) -> Packet
interface EventId {
val value: UShort
val parser: EventPacketParser?
}
// TODO: 2019/11/5 整理文件
@Suppress("FunctionName")
fun EventId(value: UShort): EventId =
KnownEventId.ofValueOrNull(value) ?: IgnoredEventIds.firstOrNull { it.value == value } ?: UnknownEventId(value)
object IgnoredEventIds : List<IgnoredEventId> by {
listOf(
0x0021u
).map { IgnoredEventId(it.toUShort()) }
}()
inline class IgnoredEventId(override val value: UShort) : EventId {
override val parser: EventPacketParser get() = { IgnoredPacket }
}
inline class UnknownEventId(override val value: UShort) : EventId {
override val parser: EventPacketParser
get() = {
MiraiLogger.debug("UnknownEventPacket type = ${value.toUHexString()}")
MiraiLogger.debug("UnknownEventPacket data = ${readBytes().toUHexString()}")
UnknownEventPacket(UnknownEventId(value))
}
}
/**
* Packet id: `00 CE` or `00 17`
*
* @author Him188moe
*/
abstract class ServerEventPacket(input: ByteReadPacket, val eventIdentity: EventPacketIdentity) : ServerPacket(input) {
override var packetId: PacketId = NullPacketId
class Raw(input: ByteReadPacket, override val packetId: PacketId) : ServerPacket(input) {
fun distribute(): ServerEventPacket = with(input) {
val eventIdentity = EventPacketIdentity(
from = readUInt(),
to = readUInt(),
uniqueId = readIoBuffer(8)
)
discardExact(2)
val type = readUShort()
//DebugLogger.warning("unknown2Byte+byte = ${unknown2Byte.toUHexString()} ${type.toUHexString()}")
return when (type.toUInt()) {
0x00C4u -> {
discardExact(13)
if (readBoolean()) {
ServerAndroidOfflineEventPacket(input, eventIdentity)
} else {
ServerAndroidOnlineEventPacket(input, eventIdentity)
}
}
0x002Du -> ServerGroupUploadFileEventPacket(input, eventIdentity)
0x002Cu -> GroupMemberPermissionChangedEventPacket(input, eventIdentity)
/*
*
inline GROUP_MEMBER_NICK_CHANGED(0x002Fu, null),
inline GROUP_MEMBER_PERMISSION_CHANGED(0x002Cu, null),
*/
0x0052u -> GroupMessageEventPacket(input, eventIdentity)
0x00A6u -> FriendMessageEventPacket(input, eventIdentity)
0x0079u -> FriendConversationInitializedEventPacket(input, eventIdentity)
0x0210u // "对方正在输入..."
-> IgnoredServerEventPacket(input, eventIdentity)
else -> {
UnknownServerEventPacket(type.toByteArray(), true, input, eventIdentity)
}
}.applyId(packetId).applySequence(sequenceId)
}
class Encrypted(input: ByteReadPacket, override var packetId: PacketId, override var sequenceId: UShort) :
ServerPacket(input) {
fun decrypt(sessionKey: ByteArray): Raw =
Raw(this.decryptBy(sessionKey), packetId).applySequence(sequenceId)
}
}
@Suppress("FunctionName")
fun ResponsePacket(
bot: UInt,
sessionKey: ByteArray
): OutgoingPacket = EventResponse(this.packetId, this.sequenceId, bot, sessionKey, this.eventIdentity)
@NoLog
@Suppress("FunctionName")
object EventResponse : OutgoingPacketBuilder {
operator fun invoke(
id: PacketId,
sequenceId: UShort,
bot: UInt,
sessionKey: ByteArray,
identity: EventPacketIdentity
): OutgoingPacket = buildOutgoingPacket(name = "EventResponse", id = id, sequenceId = sequenceId) {
writeQQ(bot)
writeHex(TIMProtocol.fixVer2)
encryptAndWrite(sessionKey) {
writeEventPacketIdentity(identity)
}
}
}
}
/**
* 忽略的事件
* @param parser 解析器. 解析 [数据包][ByteReadPacket] [Packet]
*/
@Suppress("unused")
class IgnoredServerEventPacket(input: ByteReadPacket, eventIdentity: EventPacketIdentity) :
ServerEventPacket(input, eventIdentity)
enum class KnownEventId(override inline val value: UShort, override val parser: EventPacketParser) : EventId {
/**
* Android 客户端在线状态改变
*/
ANDROID_DEVICE_ONLINE_STATUS_CHANGE(0x00C4u, {
discardExact(13)
EventPacket.AndroidDeviceStatusChange(
if (readBoolean()) EventPacket.AndroidDeviceStatusChange.Kind.OFFLINE else EventPacket.AndroidDeviceStatusChange.Kind.ONLINE
)
}),
/**
* Unknown event
*/
class UnknownServerEventPacket(
@Suppress("MemberVisibilityCanBePrivate")// 让它能被日志记录
val eventId: ByteArray,
private val showData: Boolean = false,
input: ByteReadPacket,
eventIdentity: EventPacketIdentity
) :
ServerEventPacket(input, eventIdentity) {
override fun decode() {
MiraiLogger.debug("UnknownEvent type = ${eventId.toUHexString()}")
if (showData) {
MiraiLogger.debug("UnknownServerEventPacket data: " + this.input.readBytes().toUHexString())
} else {
this.input.discard()
GROUP_FILE_UPLOAD(0x002Du, {
discardExact(60)
val size = readShort().toInt()
discardExact(3)
EventPacket.GroupFileUpload(xmlMessage = readString(size))
}),
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2.21173")
GROUP_MEMBER_PERMISSION_CHANGE(0x002Cu, {
// 群里一个人变成管理员:
// 00 00 00 08 00 0A 00 04 01 00 00 00 22 96 29 7B 01 01 76 E4 B8 DD 01
// 取消管理员
// 00 00 00 08 00 0A 00 04 01 00 00 00 22 96 29 7B 01 00 76 E4 B8 DD 00
discardExact(remaining - 5)
EventPacket.MemberPermissionChange().apply {
groupId = it.from
qq = readUInt()
kind = when (readByte().toInt()) {
0x00 -> MemberPermissionChangedEvent.Kind.NO_LONGER_OPERATOR
0x01 -> MemberPermissionChangedEvent.Kind.BECOME_OPERATOR
else -> error("Could not determine permission change kind")
}
}
}),
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2.21173")
GROUP_MESSAGE(0x0052u, {
EventPacket.GroupMessage().apply {
discardExact(31)
groupNumber = readUInt()
discardExact(1)
qq = readUInt()
discardExact(48)
readUShortLVByteArray()
discardExact(2)//2个0x00
message = readMessageChain()
val map = readTLVMap(true)
if (map.containsKey(18u)) {
map.getValue(18u).read {
val tlv = readTLVMap(true)
senderPermission = when (tlv.takeIf { it.containsKey(0x04u) }?.get(0x04u)?.getOrNull(3)?.toUInt()) {
null -> SenderPermission.MEMBER
0x08u -> SenderPermission.OWNER
0x10u -> SenderPermission.OPERATOR
else -> {
tlv.printTLVMap("TLV(tag=18) Map")
MiraiLogger.warning("Could not determine member permission, default permission MEMBER is being used")
SenderPermission.MEMBER
}
}
senderName = when {
tlv.containsKey(0x01u) -> String(tlv.getValue(0x01u))//这个人的qq昵称
tlv.containsKey(0x02u) -> String(tlv.getValue(0x02u))//这个人的群名片
else -> {
tlv.printTLVMap("TLV(tag=18) Map")
MiraiLogger.warning("Could not determine senderName")
"null"
}
}
}
}
}
}),
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2.21173")
FRIEND_MESSAGE(0x00A6u, {
discardExact(2)
val l1 = readShort()
discardExact(1)//0x00
val previous = readByte().toInt() == 0x08
discardExact(l1.toInt() - 2)
//java.io.EOFException: Only 49 bytes were discarded of 69 requested
//抖动窗口消息
discardExact(69)
readUShortLVByteArray()//font
discardExact(2)//2个0x00
EventPacket.FriendMessage(
isPrevious = previous,
qq = it.from,
message = readMessageChain()
)
}),
FRIEND_CONVERSATION_INITIALIZE(0x0079u, {
discardExact(4)// 00 00 00 00
EventPacket.FriendConversationInitialize().apply {
qq = readUInt()
}
}),
;
companion object {
fun ofValueOrNull(value: UShort): KnownEventId? = values().firstOrNull { it.value == value }
}
}

View File

@ -1,23 +1,28 @@
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "FunctionName")
package net.mamoe.mirai.network.protocol.tim.packet.login
import kotlinx.io.core.*
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.utils.io.*
/**
* 客户端请求验证码图片数据的第几部分
*/
object CaptchaKey : DecrypterByteArray, DecrypterType<CaptchaKey> {
override val value: ByteArray = TIMProtocol.key00BA.hexToBytes(withCache = false)
}
@AnnotatedId(KnownPacketId.CAPTCHA)
object RequestCaptchaTransmissionPacket : OutgoingPacketBuilder {
operator fun invoke(
object CaptchaPacket : PacketFactory<CaptchaPacket.CaptchaResponse, CaptchaKey>(CaptchaKey) {
/**
* 请求验证码传输
*/
fun RequestTransmission(
bot: UInt,
token0825: ByteArray,
captchaSequence: Int,
token00BA: ByteArray
) = buildOutgoingPacket {
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
writeHex(TIMProtocol.fixVer)
writeHex(TIMProtocol.key00BA)
@ -36,19 +41,38 @@ object RequestCaptchaTransmissionPacket : OutgoingPacketBuilder {
writeHex(TIMProtocol.key00BAFix)
}
}
}
/**
* 提交验证码
*/
@AnnotatedId(KnownPacketId.CAPTCHA)
object SubmitCaptchaPacket : OutgoingPacketBuilder {
operator fun invoke(
/**
* 刷新验证码
*/
fun Refresh(
bot: UInt,
token0825: ByteArray
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
writeHex(TIMProtocol.fixVer)
writeHex(TIMProtocol.key00BA)
encryptAndWrite(TIMProtocol.key00BA) {
writeHex("00 02 00 00 08 04 01 E0")
writeHex(TIMProtocol.constantData2)
writeHex("00 00 38")
writeFully(token0825)
writeHex("01 03 00 19")
writeHex(TIMProtocol.publicKey)
writeHex("13 00 05 00 00 00 00 00 00 00 00 10")
writeHex(TIMProtocol.key00BAFix)
}
}
/**
* 提交验证码
*/
fun Submit(
bot: UInt,
token0825: ByteArray,
captcha: String,
captchaToken: IoBuffer
) = buildOutgoingPacket {
): OutgoingPacket = buildOutgoingPacket {
require(captcha.length == 4) { "captcha.length must == 4" }
writeQQ(bot)
writeHex(TIMProtocol.fixVer)
@ -72,99 +96,52 @@ object SubmitCaptchaPacket : OutgoingPacketBuilder {
writeHex(TIMProtocol.key00BAFix)//16
}
}
}
/**
* 刷新验证码
*/
@AnnotatedId(KnownPacketId.CAPTCHA)
object OutgoingCaptchaRefreshPacket : OutgoingPacketBuilder {
operator fun invoke(
qq: UInt,
token0825: ByteArray
) = buildOutgoingPacket {
writeQQ(qq)
writeHex(TIMProtocol.fixVer)
writeHex(TIMProtocol.key00BA)
encryptAndWrite(TIMProtocol.key00BA) {
writeHex("00 02 00 00 08 04 01 E0")
writeHex(TIMProtocol.constantData2)
writeHex("00 00 38")
writeFully(token0825)
writeHex("01 03 00 19")
writeHex(TIMProtocol.publicKey)
writeHex("13 00 05 00 00 00 00 00 00 00 00 10")
writeHex(TIMProtocol.key00BAFix)
sealed class CaptchaResponse : Packet {
lateinit var token00BA: ByteArray//56 bytes
class Correct : CaptchaResponse()
class Transmission : CaptchaResponse() {
lateinit var captchaSectionN: IoBuffer
lateinit var captchaToken: IoBuffer//56bytes
var transmissionCompleted: Boolean = false//验证码是否已经传输完成
}
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): CaptchaResponse =
when (val id = readByte().toUInt()) {
0x14u -> {//00 05 00 00 00 00 00 00 38
CaptchaResponse.Correct().apply {
discardExact(9)
token00BA = readBytes(56)
}
}
0x13u -> {
CaptchaResponse.Transmission().apply {
discardExact(9)
captchaToken = readIoBuffer(56)
val length = readShort()
captchaSectionN = readIoBuffer(length)
discardExact(1)
val byte = readByte().toInt()
transmissionCompleted = byte == 0
discardExact(remaining - 56 - 2)
token00BA = readBytes(40)
}
}
else -> error("Unable to analyze RequestCaptchaTransmissionPacket, unknown id: $id")
}
}
/**
* 服务器发送验证码图片文件一部分过来. 当验证码输入错误时, 服务器的返回也会是这个包.
*
* @author Him188moe
*/
@AnnotatedId(KnownPacketId.CAPTCHA)
open class CaptchaTransmissionResponsePacket(input: ByteReadPacket) : ServerCaptchaPacket(input) {
lateinit var captchaSectionN: IoBuffer
lateinit var captchaToken: IoBuffer//56bytes
var transmissionCompleted: Boolean = false//验证码是否已经传输完成
lateinit var token00BA: ByteArray//40 bytes
override fun decode() = with(input) {
input.discardExact(10)//13 00 05 01 00 00 01 23 00 38
captchaToken = readIoBuffer(56)
val length = readShort()
captchaSectionN = readIoBuffer(length)
discardExact(1)
val byte = readByte().toInt()
transmissionCompleted = byte == 0
discardExact(remaining - 56 - 2)
token00BA = readBytes(40)
}
}
/*
fun main() {
val data = "13 00 05 01 00 00 01 23 00 38 59 32 29 5A 3E 3D 2D FC F5 22 EB 9E 2D FB 9C 4F AA 06 C8 32 3D F0 3C 2C 2B BA 8D 05 C4 9B C1 74 3B 70 F1 99 90 BB 6E 3E 6F 74 48 97 D3 61 B7 04 C0 A3 F1 DF 40 A4 DC 2B 00 A2 01 2D BB BB E8 FE B8 AF B3 6F 39 7C EA E2 5B 91 BE DB 59 38 CF 58 BC F2 88 F1 09 CF 92 E9 F7 FB 13 76 C5 68 29 23 3F 8E 43 16 2E 50 D7 FA 4D C1 F7 67 EF 27 FB C6 F1 A7 25 A4 BC 45 39 3A EA B2 A5 38 02 FF 4B C9 FF EB BD 89 E5 5D B9 4A 2A BE 5F 52 F1 EB 09 29 CB 3E 66 CF EF 97 89 47 BB 6B E0 7B 4A 3E A1 BC 3F FB F2 0A 83 CB E3 EA B9 43 E1 26 88 03 0B A7 E0 B2 AD 7F 83 CC DA 74 85 83 72 08 EC D2 F9 95 05 15 05 96 F7 1C FF 00 82 C3 90 22 A4 BA 90 D5 00 00 00 00 49 45 4E 44 AE 42 60 82 03 00 00 28 EA 32 5A 85 C8 D2 73 B3 40 39 77 85 65 98 00 FE 03 A2 A5 95 B4 2F E6 79 7A DE 5A 03 10 C8 3D BF 6D 3D 8B 51 84 C2 6D 49 00 10 92 AA 69 FB C6 3D 60 5A 7A A4 AC 7A B0 71 00 36".hexToBytes()
CaptchaTransmissionResponsePacket(data.toReadPacket(), data.size, "00 BA 31 01".hexToBytes()).let {
ServerCaptchaTransmissionResponsePacket(data.toReadPacket(), data.size, "00 BA 31 01".hexToBytes()).let {
it.dataDecode()
println(it.toString())
}
}*/
/**
* 验证码正确
*
* @author Him188moe
*/
@AnnotatedId(KnownPacketId.CAPTCHA)
class CaptchaCorrectPacket(input: ByteReadPacket) : ServerCaptchaPacket(input) {
lateinit var token00BA: ByteArray//56 bytes
override fun decode() = with(input) {
discardExact(10)//14 00 05 00 00 00 00 00 00 38
token00BA = readBytes(56)
}
}
@AnnotatedId(KnownPacketId.CAPTCHA)
abstract class ServerCaptchaPacket(input: ByteReadPacket) : ServerPacket(input) {
@AnnotatedId(KnownPacketId.CAPTCHA)
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
fun decrypt(): ServerCaptchaPacket {
return this.decryptAsByteArray(TIMProtocol.key00BA) { data ->
when (data.size) {
66,
95 -> CaptchaCorrectPacket(data.toReadPacket())
//66 -> ServerCaptchaUnknownPacket(data.toReadPacket())
else -> CaptchaTransmissionResponsePacket(data.toReadPacket())
}.applySequence(sequenceId)
}
}
}
}
}*/

View File

@ -2,7 +2,9 @@
package net.mamoe.mirai.network.protocol.tim.packet.login
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.writeUByte
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.utils.OnlineStatus
@ -14,10 +16,10 @@ import net.mamoe.mirai.utils.io.writeQQ
* 改变在线状态: "我在线上", "隐身"
*/
@AnnotatedId(KnownPacketId.CHANGE_ONLINE_STATUS)
object ChangeOnlineStatusPacket : OutgoingPacketBuilder {
object ChangeOnlineStatusPacket : PacketFactory<ChangeOnlineStatusPacket.ChangeOnlineStatusResponse, NoDecrypter>(NoDecrypter) {
operator fun invoke(
bot: UInt,
sessionKey: ByteArray,
sessionKey: SessionKey,
loginStatus: OnlineStatus
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
@ -28,4 +30,9 @@ object ChangeOnlineStatusPacket : OutgoingPacketBuilder {
writeHex("00 01 00 01 00 04 00 00 00 00")
}
}
object ChangeOnlineStatusResponse : Packet
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): ChangeOnlineStatusResponse =
ChangeOnlineStatusResponse
}

View File

@ -2,26 +2,52 @@
package net.mamoe.mirai.network.protocol.tim.packet.login
import kotlinx.io.core.BytePacketBuilder
import kotlinx.io.core.IoBuffer
import kotlinx.io.core.writeFully
import kotlinx.io.core.*
import net.mamoe.mirai.contact.Gender
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.decryptBy
import net.mamoe.mirai.utils.encryptBy
import net.mamoe.mirai.utils.io.*
import net.mamoe.mirai.utils.writeCRC32
import kotlin.properties.Delegates
object ShareKey : DecrypterByteArray, DecrypterType<ShareKey> {
override val value: ByteArray = TIMProtocol.shareKey.hexToBytes(withCache = false)
}
inline class PrivateKey(override val value: ByteArray) : DecrypterByteArray {
companion object Type : DecrypterType<PrivateKey>
}
inline class SubmitPasswordResponseDecrypter(private val privateKey: PrivateKey) : Decrypter {
override fun decrypt(packet: ByteReadPacket): ByteReadPacket {
var decrypted = ShareKey.decrypt(packet)
(decrypted.remaining).let {
if (it.toInt() % 8 == 0 && it >= 16) {
decrypted = privateKey.decrypt(decrypted)
}
} // TODO: 2019/11/5 优化: 某些情况下并不需要这次解密. 根据长度判断会导致一些问题
return decrypted
}
companion object Type : DecrypterType<SubmitPasswordResponseDecrypter>
}
/**
* 提交密码
*/
@AnnotatedId(KnownPacketId.LOGIN)
object SubmitPasswordPacket : OutgoingPacketBuilder {
object SubmitPasswordPacket : PacketFactory<SubmitPasswordPacket.LoginResponse, SubmitPasswordResponseDecrypter>(SubmitPasswordResponseDecrypter) {
operator fun invoke(
bot: UInt,
password: String,
loginTime: Int,
loginIP: String,
privateKey: ByteArray,
privateKey: PrivateKey,
token0825: ByteArray,
token00BA: ByteArray? = null,
randomDeviceName: Boolean = false,
@ -29,30 +55,160 @@ object SubmitPasswordPacket : OutgoingPacketBuilder {
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
writeHex(TIMProtocol.passwordSubmissionTLV1)
writeShort(25)
writeHex(TIMProtocol.publicKey)//=25
writeShort(25); writeHex(TIMProtocol.publicKey)//=25
writeZero(2)
writeShort(16)
writeHex(TIMProtocol.key0836)//=16
writeShort(16); writeHex(TIMProtocol.key0836)//=16
//TODO shareKey 极大可能为 publicKey, key0836 计算得到
encryptAndWrite(TIMProtocol.shareKey) {
writePart1(bot, password, loginTime, loginIP, privateKey, token0825, randomDeviceName, tlv0006)
if (token00BA != null) {
writeHex("01 10")
writeHex("00 3C")
writeHex("00 01")
writeHex("00 38")
writeFully(token00BA)
writeHex("00 38"); writeFully(token00BA)
}
writePart2()
}
}
sealed class LoginResponse : Packet {
class KeyExchange : LoginResponse() {
lateinit var tlv0006: IoBuffer//120bytes
var tokenUnknown: ByteArray? = null
var privateKeyUpdate: PrivateKey? = null//16bytes
}
class CaptchaInit : LoginResponse() {
lateinit var captchaPart1: IoBuffer
lateinit var token00BA: ByteArray
var unknownBoolean: Boolean by Delegates.notNull()
}
class Success : LoginResponse() {
var sessionResponseDecryptionKey: SessionResponseDecryptionKey by Delegates.notNull()//16 bytes|
lateinit var token38: IoBuffer//56
lateinit var token88: IoBuffer//136
lateinit var encryptionKey: IoBuffer//16
lateinit var nickname: String
var age: Short by Delegates.notNull()
lateinit var gender: Gender
}
class Failed(val result: LoginResult) : LoginResponse()
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): LoginResponse {
val size = remaining.toInt()
return when {
size == 229 || size == 271 || size == 207 -> LoginResponse.KeyExchange().apply {
discardExact(5)//01 00 1E 00 10
privateKeyUpdate = PrivateKey(readBytes(0x10))
discardExact(4)//00 06 00 78
tlv0006 = readIoBuffer(0x78)
try {
discardExact(8)//01 10 00 3C 00 01 00 38
tokenUnknown = readBytes(56)
} catch (e: EOFException) {
//什么都不做. 因为有的包就是没有这个数据.
}
}
size == 844 || size == 871 -> LoginResponse.CaptchaInit().apply {
discardExact(78)
//println(readRemainingBytes().toUHexString())
val captchaLength = readShort()//2bytes
this.captchaPart1 = readIoBuffer(captchaLength)
discardExact(1)
this.unknownBoolean = readByte().toInt() == 1
discardExact(remaining - 60)
this.token00BA = readBytes(40)
}
size > 650 -> LoginResponse.Success().apply {
discardExact(7)//00 01 09 00 70 00 01
//FB 01 04 03 33
encryptionKey = readIoBuffer(16)//C6 72 C7 73 70 01 46 A2 11 88 AC E4 92 7B BF 90
discardExact(2)//00 38
token38 = readIoBuffer(56)
discardExact(60)//00 20 01 60 C5 A1 39 7A 12 8E BC 34 C3 56 70 E3 1A ED 20 67 ED A9 DB 06 C1 70 81 3C 01 69 0D FF 63 DA 00 00 01 03 00 14 00 01 00 10 60 C9 5D A7 45 70 04 7F 21 7D 84 50 5C 66 A5 C6
discardExact(
when (readUByte().toUInt()) {
0x00u -> when (readUByte().toUInt()) {
0x33u -> 28
else -> null
}
0x01u -> when (readUByte().toUInt()) {
0x07u -> 0
0x10u -> 64
else -> null
}
else -> null
} ?: error("Unknown length flag")
)
discardExact(23 + 3)//01 D3 00 01 00 16 00 00 00 01 00 00 00 64 00 00 0D DE 00 09 3A 80 00
discardExact(2)//00 02
sessionResponseDecryptionKey = SessionResponseDecryptionKey(readIoBuffer(16))
discardExact(2)
token88 = readIoBuffer(136)
discardExact(299)//2E 72 7A 50 41 54 5B 62 7D 47 5D 37 41 53 47 51 00 78 00 01 5D A2 DB 79 00 70 72 E7 D3 4E 6F D8 D1 DD F2 67 04 1D 23 4D E9 A7 AB 89 7A B7 E6 4B C0 79 60 3B 4F AA 31 C5 24 51 C1 4B 4F A4 32 74 BA FE 8E 06 DB 54 25 A2 56 91 E8 66 BB 23 29 EB F7 13 7B 94 1E AF B2 40 4E 69 5C 8C 35 04 D1 25 1F 60 93 F3 40 71 0B 61 60 F1 B6 A9 7A E8 B1 DA 0E 16 A2 F1 2D 69 5A 01 20 7A AB A7 37 68 D2 1A B0 4D 35 D1 E1 35 64 F6 90 2B 00 83 01 24 5B 4E 69 3D 45 54 6B 29 5E 73 23 2D 4E 42 3F 00 70 00 01 5D A2 DB 79 00 68 FD 10 8A 39 51 09 C6 69 CE 09 A4 52 8C 53 D3 B6 87 E1 7B 7E 4E 52 6D BA 9C C4 6E 6D DE 09 99 67 B4 BD 56 71 14 5A 54 01 68 1C 3C AA 0D 76 0B 86 5A C1 F1 BC 5E 0A ED E3 8C 57 86 35 D8 A5 F8 16 01 24 8B 57 56 8C A6 31 6F 65 73 03 DA ED 21 FA 6B 79 32 2B 09 01 E8 D2 D8 F0 7B F1 60 C2 7F 53 5D F6 53 50 8A 43 E2 23 2E 52 7B 60 39 56 67 2D 6A 23 43 4B 60 55 68 35 01 08 00 23 00 01 00 1F 00 17 02 5B
val nickLength = readUByte().toInt()
nickname = readString(nickLength)
//后文
//00 05 00 04 00 00 00 01 01 15 00 10 49 83 5C D9 93 6C 8D FE 09 18 99 37 99 80 68 92
discardExact(4)//02 13 80 02
age = readShort()//00 05
discardExact(4)//00 04 00 00
discardExact(2)//00 01
gender = if (readBoolean()) Gender.FEMALE else Gender.MALE
}
else -> LoginResponse.Failed(when (size) {
135 -> {//包数据错误. 目前怀疑是 tlv0006
this.readRemainingBytes().cutTail(1).decryptBy(TIMProtocol.shareKey).read {
discardExact(51)
MiraiLogger.error("Internal error: " + readUShortLVString())//抱歉,请重新输入密码。
}
LoginResult.INTERNAL_ERROR
}
240, 319, 320, 351 -> LoginResult.WRONG_PASSWORD
//135 -> LoginState.RETYPE_PASSWORD
63 -> LoginResult.BLOCKED
263 -> LoginResult.UNKNOWN_QQ_NUMBER
279, 495, 551, 487 -> LoginResult.DEVICE_LOCK
343, 359 -> LoginResult.TAKEN_BACK
else -> {
MiraiLogger.error("login response packet size = $size, data=${this.readRemainingBytes().toUHexString()}")
LoginResult.UNKNOWN
}
})
}
}
}
inline class SessionResponseDecryptionKey(private val delegate: IoBuffer) : Decrypter {
override fun decrypt(packet: ByteReadPacket): ByteReadPacket = packet.decryptBy(delegate)
companion object Type : DecrypterType<SessionResponseDecryptionKey>
}
private fun BytePacketBuilder.writePart1(
@ -60,7 +216,7 @@ private fun BytePacketBuilder.writePart1(
password: String,
loginTime: Int,
loginIP: String,
privateKey: ByteArray,
privateKey: PrivateKey,
token0825: ByteArray,
randomDeviceName: Boolean,
tlv0006: IoBuffer? = null

View File

@ -4,6 +4,7 @@ package net.mamoe.mirai.network.protocol.tim.packet.login
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.BotSession
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
@ -17,10 +18,10 @@ fun BotSession.RequestSKeyPacket(): OutgoingPacket = RequestSKeyPacket(qqAccount
* SKey 用于 http api
*/
@AnnotatedId(KnownPacketId.S_KEY)
object RequestSKeyPacket : OutgoingPacketBuilder {
object RequestSKeyPacket : SessionPacketFactory<SKey>() {
operator fun invoke(
bot: UInt,
sessionKey: ByteArray
sessionKey: SessionKey
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
writeHex(TIMProtocol.fixVer2)
@ -29,16 +30,15 @@ object RequestSKeyPacket : OutgoingPacketBuilder {
}
}
@AnnotatedId(KnownPacketId.S_KEY)
class Response(input: ByteReadPacket) : ResponsePacket(input) {
lateinit var sKey: String
override fun decode() = with(input) {
discardExact(4)
//debugDiscardExact(2)
sKey = this.readString(10)//16??
DebugLogger.warning("SKey=$sKey")
DebugLogger.warning("SKey 包后面${this.readRemainingBytes().toUHexString()}")
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): SKey {
discardExact(4)
// TODO: 2019/11/2 这里
return SKey(readString(10)).also {
DebugLogger.warning("SKey 包后面${readRemainingBytes().toUHexString()}")
}
}
}
}
inline class SKey(
val delegate: String
) : Packet

View File

@ -1,153 +0,0 @@
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.network.protocol.tim.packet.login
import kotlinx.io.core.*
import net.mamoe.mirai.contact.Gender
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.utils.Tested
import net.mamoe.mirai.utils.io.readBoolean
import net.mamoe.mirai.utils.io.readIoBuffer
import net.mamoe.mirai.utils.io.readString
import kotlin.properties.Delegates
@AnnotatedId(KnownPacketId.LOGIN)
sealed class ServerLoginResponsePacket(input: ByteReadPacket) : ServerPacket(input)
@AnnotatedId(KnownPacketId.LOGIN)
class LoginResponseFailedPacket(val loginResult: LoginResult, input: ByteReadPacket) : ServerLoginResponsePacket(input)
/**
* 服务器进行加密后返回 privateKey
*/
@AnnotatedId(KnownPacketId.LOGIN)
class LoginResponseKeyExchangeResponsePacket(input: ByteReadPacket) : ServerLoginResponsePacket(input) {
lateinit var tlv0006: IoBuffer//120bytes
var tokenUnknown: ByteArray? = null
lateinit var privateKeyUpdate: ByteArray//16bytes
@Tested
override fun decode() {
this.input.discardExact(5)//01 00 1E 00 10
privateKeyUpdate = this.input.readBytes(0x10)
this.input.discardExact(4)//00 06 00 78
tlv0006 = this.input.readIoBuffer(0x78)
try {
this.input.discardExact(8)//01 10 00 3C 00 01 00 38
tokenUnknown = this.input.readBytes(56)
} catch (e: EOFException) {
//什么都不做. 因为有的包就是没有这个数据.
}
}
@AnnotatedId(KnownPacketId.LOGIN)
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
@Tested
fun decrypt(privateKey: ByteArray): LoginResponseKeyExchangeResponsePacket =
LoginResponseKeyExchangeResponsePacket(this.decryptBy(TIMProtocol.shareKey, privateKey)).applySequence(sequenceId)
}
}
@AnnotatedId(KnownPacketId.LOGIN)
class LoginResponseSuccessPacket(input: ByteReadPacket) : ServerLoginResponsePacket(input) {
lateinit var sessionResponseDecryptionKey: IoBuffer//16 bytes|
lateinit var token38: IoBuffer//56
lateinit var token88: IoBuffer//136
lateinit var encryptionKey: IoBuffer//16
lateinit var nickname: String
var age: Short by Delegates.notNull()
lateinit var gender: Gender
@Tested
override fun decode() = with(input) {
discardExact(7)//00 01 09 00 70 00 01
encryptionKey = readIoBuffer(16)//C6 72 C7 73 70 01 46 A2 11 88 AC E4 92 7B BF 90
discardExact(2)//00 38
token38 = readIoBuffer(56)
discardExact(60)//00 20 01 60 C5 A1 39 7A 12 8E BC 34 C3 56 70 E3 1A ED 20 67 ED A9 DB 06 C1 70 81 3C 01 69 0D FF 63 DA 00 00 01 03 00 14 00 01 00 10 60 C9 5D A7 45 70 04 7F 21 7D 84 50 5C 66 A5 C6
discardExact(
when (readUByte().toUInt()) {
0x00u -> when (readUByte().toUInt()) {
0x33u -> 28
else -> null
}
0x01u -> when (readUByte().toUInt()) {
0x07u -> 0
0x10u -> 64
else -> null
}
else -> null
} ?: error("Unknown length flag")
)
discardExact(23 + 3)//01 D3 00 01 00 16 00 00 00 01 00 00 00 64 00 00 0D DE 00 09 3A 80 00
discardExact(2)//00 02
sessionResponseDecryptionKey = readIoBuffer(16)
discardExact(2)
token88 = readIoBuffer(136)
discardExact(299)//2E 72 7A 50 41 54 5B 62 7D 47 5D 37 41 53 47 51 00 78 00 01 5D A2 DB 79 00 70 72 E7 D3 4E 6F D8 D1 DD F2 67 04 1D 23 4D E9 A7 AB 89 7A B7 E6 4B C0 79 60 3B 4F AA 31 C5 24 51 C1 4B 4F A4 32 74 BA FE 8E 06 DB 54 25 A2 56 91 E8 66 BB 23 29 EB F7 13 7B 94 1E AF B2 40 4E 69 5C 8C 35 04 D1 25 1F 60 93 F3 40 71 0B 61 60 F1 B6 A9 7A E8 B1 DA 0E 16 A2 F1 2D 69 5A 01 20 7A AB A7 37 68 D2 1A B0 4D 35 D1 E1 35 64 F6 90 2B 00 83 01 24 5B 4E 69 3D 45 54 6B 29 5E 73 23 2D 4E 42 3F 00 70 00 01 5D A2 DB 79 00 68 FD 10 8A 39 51 09 C6 69 CE 09 A4 52 8C 53 D3 B6 87 E1 7B 7E 4E 52 6D BA 9C C4 6E 6D DE 09 99 67 B4 BD 56 71 14 5A 54 01 68 1C 3C AA 0D 76 0B 86 5A C1 F1 BC 5E 0A ED E3 8C 57 86 35 D8 A5 F8 16 01 24 8B 57 56 8C A6 31 6F 65 73 03 DA ED 21 FA 6B 79 32 2B 09 01 E8 D2 D8 F0 7B F1 60 C2 7F 53 5D F6 53 50 8A 43 E2 23 2E 52 7B 60 39 56 67 2D 6A 23 43 4B 60 55 68 35 01 08 00 23 00 01 00 1F 00 17 02 5B
val nickLength = readUByte().toInt()
nickname = readString(nickLength)
//后文
//00 05 00 04 00 00 00 01 01 15 00 10 49 83 5C D9 93 6C 8D FE 09 18 99 37 99 80 68 92
discardExact(4)//02 13 80 02
age = readShort()//00 05
discardExact(4)//00 04 00 00
discardExact(2)//00 01
gender = if (readBoolean()) Gender.FEMALE else Gender.MALE
}
@AnnotatedId(KnownPacketId.LOGIN)
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
fun decrypt(privateKey: ByteArray): LoginResponseSuccessPacket = LoginResponseSuccessPacket(this.decryptBy(TIMProtocol.shareKey, privateKey)).applySequence(sequenceId)
}
}
/**
* 收到这个包意味着需要验证码登录, 并且能得到验证码图片文件的一部分
*
* @author Him188moe
*/
@AnnotatedId(KnownPacketId.LOGIN)
class LoginResponseCaptchaInitPacket(input: ByteReadPacket) : ServerLoginResponsePacket(input) {
lateinit var captchaPart1: IoBuffer
lateinit var token00BA: ByteArray
var unknownBoolean: Boolean by Delegates.notNull()
@Tested
override fun decode() {
this.input.discardExact(78)
//println(this.input.readRemainingBytes().toUHexString())
val captchaLength = this.input.readShort()//2bytes
this.captchaPart1 = this.input.readIoBuffer(captchaLength)
this.input.discardExact(1)
this.unknownBoolean = this.input.readByte().toInt() == 1
this.input.discardExact(this.input.remaining - 60)
this.token00BA = this.input.readBytes(40)
}
@AnnotatedId(KnownPacketId.LOGIN)
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
fun decrypt(): LoginResponseCaptchaInitPacket = LoginResponseCaptchaInitPacket(decryptBy(TIMProtocol.shareKey)).applySequence(sequenceId)
}
}

View File

@ -1,16 +0,0 @@
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.network.protocol.tim.packet.login
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.network.protocol.tim.packet.AnnotatedId
import net.mamoe.mirai.network.protocol.tim.packet.KnownPacketId
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
/**
* Congratulations!
*
* @author Him188moe
*/
@AnnotatedId(KnownPacketId.CHANGE_ONLINE_STATUS)
class ServerLoginSuccessPacket(input: ByteReadPacket) : ServerPacket(input)//TODO 可能只是 login status change 的返回包

View File

@ -3,14 +3,14 @@
package net.mamoe.mirai.network.protocol.tim.packet.login
import kotlinx.io.core.*
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.utils.Tested
import net.mamoe.mirai.utils.io.*
import net.mamoe.mirai.utils.localIpAddress
@AnnotatedId(KnownPacketId.SESSION_KEY)
object RequestSessionPacket : OutgoingPacketBuilder {
object RequestSessionPacket : PacketFactory<RequestSessionPacket.SessionKeyResponse, SessionResponseDecryptionKey>(SessionResponseDecryptionKey) {
operator fun invoke(
bot: UInt,
serverIp: String,
@ -58,61 +58,49 @@ object RequestSessionPacket : OutgoingPacketBuilder {
writeIP(localIpAddress())//todo random to avoid being banned? or that may cause errors?
}
}
}
class SessionKeyResponse : Packet {
var sessionKey: SessionKey? = null
lateinit var tlv0105: ByteReadPacket
}
@AnnotatedId(KnownPacketId.SESSION_KEY)
class SessionKeyResponsePacket(input: ByteReadPacket) : ServerPacket(input) {
lateinit var sessionKey: ByteArray
lateinit var tlv0105: ByteReadPacket
@Tested
override fun decode() = with(input) {
when (val dataLength = remaining) {
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): SessionKeyResponse =
SessionKeyResponse().apply {
when (remaining) {
407L -> {
input.discardExact(25)//todo test
sessionKey = input.readBytes(16)
discardExact(25)//todo test
sessionKey = SessionKey(readBytes(16))
}
439L -> {
input.discardExact(63)
sessionKey = input.readBytes(16)
discardExact(63)
sessionKey = SessionKey(readBytes(16))
}
502L,//?
512L,
527L -> {
input.discardExact(63)//00 00 0D 00 06 00 01 00 00 00 00 00 1F 00 22 00 01 D7 EC FC 38 1B 74 6F 91 42 00 B9 DB 69 32 43 EC 8C 02 DC E0 07 35 58 8C 6C FE 43 5D AA 6A 88 E0 00 14 00 04 00 01 00 3C 01 0C 00 73 00 01
sessionKey = input.readBytes(16)
discardExact(63)//00 00 0D 00 06 00 01 00 00 00 00 00 1F 00 22 00 01 D7 EC FC 38 1B 74 6F 91 42 00 B9 DB 69 32 43 EC 8C 02 DC E0 07 35 58 8C 6C FE 43 5D AA 6A 88 E0 00 14 00 04 00 01 00 3C 01 0C 00 73 00 01
sessionKey = SessionKey(readBytes(16))
tlv0105 = buildPacket {
writeHex("01 05 00 88 00 01 01 02 00 40 02 01 03 3C 01 03 00 00")
input.discardExact(input.remaining - 122 - 1)
writeFully(input.readIoBuffer(56))
discardExact(remaining - 122 - 1)
writeFully(readIoBuffer(56))
writeHex("00 40 02 02 03 3C 01 03 00 00")
input.discardExact(11)
writeFully(input.readIoBuffer(56))
discardExact(11)
writeFully(readIoBuffer(56))
} //todo 这个 tlv0105似乎可以保存起来然后下次登录时使用.
/*
Discarded(63) =00 00 0D 00 06 00 01 00 00 00 00 00 1F 00 22 00 01 F7 AB 01 4B 23 B5 47 FC 79 02 09 E0 19 EF 61 91 14 AD 8F 38 2E 8B D7 47 39 DE FE 84 A7 E5 6E 3D 00 14 00 04 00 01 00 3C 01 0C 00 73 00 01
sessionKey=7E 8C 1D AC 52 64 B8 D0 9A 55 3A A6 DF 53 88 C8
Discarded(301) =76 E4 B8 DD AB 53 02 2B 53 F1 5D A2 DA CB 00 00 00 B4 03 3D 97 B4 D1 3D 97 B4 C7 00 00 00 07 00 30 D4 E2 53 73 2E 00 F6 3F 8E 45 9F 2E 74 63 39 99 B4 AC 3B 40 C8 9A EE B0 62 A8 E1 39 FE 8E 75 EC 28 6C 03 E6 3B 5F F5 6D 50 7D 1E 29 EC 3D 47 85 08 02 04 08 08 08 08 08 04 00 05 01 0E 12 AC F6 01 0E 00 56 00 01 00 52 13 80 42 00 00 02 02 00 00 18 AB 52 CF 5B E8 CD 95 CC 3F 5C A7 BA C9 C1 5D DD F8 E2 6E 0D A3 DF F8 76 00 20 D3 87 6B 1F F2 2B C7 53 38 60 F3 AD 07 82 8B F6 62 3C E0 DB 66 BC AD D0 68 D0 30 9D 8A 41 E7 75 00 0C 00 00 00 01 00 00 00 00 00 00 00 40 00 2F 00 2A 00 01 8F FE 4F BB B2 63 C7 69 C3 F1 3C DC A1 E8 77 A3 DD 97 FA 00 36 04 40 EF 11 7A 31 02 4E 10 13 94 02 28 00 00 00 00 00 00 01 0D 00 2C 00 01 00 28 EF CB 22 58 6F AE DC F5 CC CE 45 EE 6D CA E7 EF 06 3F 60 B5 8A 22 D5 9E 37 FA 92 9F A9 11 68 F0 2A 25 4A 45 C3 D4 56 CF 01 05 00 8A 00 01 02 02 00 41 01 00 01 03 3C 01 03 00 00 FB
56长度=39 89 04 81 64 6B C0 71 B5 6E B0 DF 7D D4 C0 7E 97 83 BC 9F 31 39 39 C3 95 93 D9 CD 48 00 1D 0D 18 52 87 21 B2 C1 B1 AD EF 96 82 D6 D4 57 EA 48 5A 27 8C 14 6F E2 83 00
Discarded(11) =41 01 00 02 03 3C 01 03 00 00 86
Discarded(63) =00 00 0D 00 06 00 01 00 00 00 00 00 1F 00 22 00 01 F7 AB 01 4B 23 B5 47 FC 79 02 09 E0 19 EF 61 91 14 AD 8F 38 2E 8B D7 47 39 DE FE 84 A7 E5 6E 3D 00 14 00 04 00 01 00 3C 01 0C 00 73 00 01
sessionKey=7E 8C 1D AC 52 64 B8 D0 9A 55 3A A6 DF 53 88 C8
Discarded(301) =76 E4 B8 DD AB 53 02 2B 53 F1 5D A2 DA CB 00 00 00 B4 03 3D 97 B4 D1 3D 97 B4 C7 00 00 00 07 00 30 D4 E2 53 73 2E 00 F6 3F 8E 45 9F 2E 74 63 39 99 B4 AC 3B 40 C8 9A EE B0 62 A8 E1 39 FE 8E 75 EC 28 6C 03 E6 3B 5F F5 6D 50 7D 1E 29 EC 3D 47 85 08 02 04 08 08 08 08 08 04 00 05 01 0E 12 AC F6 01 0E 00 56 00 01 00 52 13 80 42 00 00 02 02 00 00 18 AB 52 CF 5B E8 CD 95 CC 3F 5C A7 BA C9 C1 5D DD F8 E2 6E 0D A3 DF F8 76 00 20 D3 87 6B 1F F2 2B C7 53 38 60 F3 AD 07 82 8B F6 62 3C E0 DB 66 BC AD D0 68 D0 30 9D 8A 41 E7 75 00 0C 00 00 00 01 00 00 00 00 00 00 00 40 00 2F 00 2A 00 01 8F FE 4F BB B2 63 C7 69 C3 F1 3C DC A1 E8 77 A3 DD 97 FA 00 36 04 40 EF 11 7A 31 02 4E 10 13 94 02 28 00 00 00 00 00 00 01 0D 00 2C 00 01 00 28 EF CB 22 58 6F AE DC F5 CC CE 45 EE 6D CA E7 EF 06 3F 60 B5 8A 22 D5 9E 37 FA 92 9F A9 11 68 F0 2A 25 4A 45 C3 D4 56 CF 01 05 00 8A 00 01 02 02 00 41 01 00 01 03 3C 01 03 00 00 FB
56长度=39 89 04 81 64 6B C0 71 B5 6E B0 DF 7D D4 C0 7E 97 83 BC 9F 31 39 39 C3 95 93 D9 CD 48 00 1D 0D 18 52 87 21 B2 C1 B1 AD EF 96 82 D6 D4 57 EA 48 5A 27 8C 14 6F E2 83 00
Discarded(11) =41 01 00 02 03 3C 01 03 00 00 86
*/
}
else -> throw IllegalArgumentException(dataLength.toString())
else -> throw IllegalArgumentException(remaining.toString())
}
//tlv0105 = "01 05 00 88 00 01 01 02 00 40 02 01 03 3C 01 03 00 00" + 取文本中间(data, 取文本长度(data) 367, 167) “00 40 02 02 03 3C 01 03 00 00 ” 取文本中间 (data, 取文本长度 (data) 166, 167)
}
@AnnotatedId(KnownPacketId.SESSION_KEY)
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
fun decrypt(sessionResponseDecryptionKey: IoBuffer): SessionKeyResponsePacket =
SessionKeyResponsePacket(decryptBy(sessionResponseDecryptionKey)).applySequence(sequenceId)
}
}

View File

@ -5,28 +5,58 @@ package net.mamoe.mirai.network.protocol.tim.packet.login
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readBytes
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.utils.io.*
object TouchKey : DecrypterByteArray, DecrypterType<TouchKey> {
override val value: ByteArray = TIMProtocol.touchKey.hexToBytes(withCache = false)
}
/**
* The packet received when logging in, used to redirect server address
*
* @see RedirectionPacket
* @see SubmitPasswordPacket
* The packet to sendTouch server, that is, to start the connection to the server.
*
* @author Him188moe
*/
@Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
@AnnotatedId(KnownPacketId.TOUCH)
class TouchResponsePacket(input: ByteReadPacket) : ServerPacket(input) {
var serverIP: String? = null
object TouchPacket : PacketFactory<TouchPacket.TouchResponse, TouchKey>(TouchKey) {
operator fun invoke(
bot: UInt,
serverIp: String,
isRedirect: Boolean
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
writeHex(TIMProtocol.fixVer)
writeHex(TIMProtocol.touchKey)
var loginTime: Int = 0
lateinit var loginIP: String
lateinit var token0825: ByteArray//56
encryptAndWrite(TIMProtocol.touchKey) {
writeHex(TIMProtocol.constantData1)
writeHex(TIMProtocol.constantData2)
writeQQ(bot)
writeHex(if (isRedirect) "00 01 00 00 03 09 00 0C 00 01" else "00 00 00 00 03 09 00 08 00 01")
writeIP(serverIp)
writeHex(
if (isRedirect) "01 6F A1 58 22 01 00 36 00 12 00 02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 14 00 1D 01 03 00 19"
else "00 02 00 36 00 12 00 02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 14 00 1D 01 02 00 19"
)
writeHex(TIMProtocol.publicKey)
}
}
override fun decode() = with(input) {
class TouchResponse : Packet {
var serverIP: String? = null
internal set
var loginTime: Int = 0
internal set
lateinit var loginIP: String
internal set
lateinit var token0825: ByteArray
internal set
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): TouchResponse = TouchResponse().apply {
when (val id = readByte().toUByte().toInt()) {
0xFE -> {
discardExact(94)
@ -41,69 +71,7 @@ class TouchResponsePacket(input: ByteReadPacket) : ServerPacket(input) {
loginIP = readIP()
}
else -> {
throw IllegalStateException(id.toByte().toUHexString())
}
}
}
@AnnotatedId(KnownPacketId.TOUCH)
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
fun decrypt(): TouchResponsePacket =
TouchResponsePacket(decryptBy(TIMProtocol.touchKey.hexToBytes())).applySequence(sequenceId)
}
}
/**
* The packet to sendTouch server, that is, to start the connection to the server.
*
* @author Him188moe
*/
@AnnotatedId(KnownPacketId.TOUCH)
object TouchPacket : OutgoingPacketBuilder {
operator fun invoke(
bot: UInt,
serverIp: String
) = buildOutgoingPacket {
writeQQ(bot)
writeHex(TIMProtocol.fixVer)
writeHex(TIMProtocol.touchKey)
encryptAndWrite(TIMProtocol.touchKey) {
writeHex(TIMProtocol.constantData1)
writeHex(TIMProtocol.constantData2)
writeQQ(bot)
writeHex("00 00 00 00 03 09 00 08 00 01")
writeIP(serverIp)
writeHex("00 02 00 36 00 12 00 02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 14 00 1D 01 02 00 19")
writeHex(TIMProtocol.publicKey)
}
}
}
/**
* Server redirection (0825 response)
*
* @author Him188moe
*/
@AnnotatedId(KnownPacketId.TOUCH)
object RedirectionPacket : OutgoingPacketBuilder {
operator fun invoke(
bot: UInt,
serverIP: String
) = buildOutgoingPacket {
writeQQ(bot)
writeHex(TIMProtocol.fixVer)
writeHex(TIMProtocol.touchKey)//redirection key
encryptAndWrite(TIMProtocol.touchKey) {
writeHex(TIMProtocol.constantData1)
writeHex(TIMProtocol.constantData2)
writeQQ(bot)
writeHex("00 01 00 00 03 09 00 0C 00 01")
writeIP(serverIP)
writeHex("01 6F A1 58 22 01 00 36 00 12 00 02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 14 00 1D 01 03 00 19")
writeHex(TIMProtocol.publicKey)
else -> throw IllegalStateException(id.toByte().toUHexString())
}
}
}

View File

@ -2,7 +2,6 @@ package net.mamoe.mirai.utils
import com.soywiz.klock.TimeSpan
import com.soywiz.klock.seconds
import net.mamoe.mirai.network.protocol.tim.packet.login.TouchResponsePacket
import kotlin.jvm.JvmField
/**
@ -10,7 +9,7 @@ import kotlin.jvm.JvmField
*/
class BotNetworkConfiguration {
/**
* 等待 [TouchResponsePacket] 的时间
* 等待 [TouchRespnose] 的时间
*/
var touchTimeout: TimeSpan = 2.seconds
/**

View File

@ -21,7 +21,7 @@ fun ExternalImage(
md5: ByteArray,
format: String,
data: ByteReadPacket
) = ExternalImage(width, height, md5, format, data, data.remaining)
): ExternalImage = ExternalImage(width, height, md5, format, data, data.remaining)
/**
* 外部图片. 图片数据还没有读取到内存.

View File

@ -19,11 +19,20 @@ enum class OnlineStatus(
/**
* 忙碌
*/
BUSY(0x32u);
BUSY(0x32u),
/**
* 离线 ? 也可能是被删好友 TODO confirm that
*/
OFFLINE(0x02u),
/**
* ?
*/
UNKNOWN(0x20u)
;
// TODO: 2019/10/29 what is 0x20u
companion object {
fun ofId(id: UByte): OnlineStatus? = values().firstOrNull { it.id == id }
}

View File

@ -3,6 +3,7 @@ package net.mamoe.mirai.utils
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.IoBuffer
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.network.protocol.tim.packet.DecrypterByteArray
import net.mamoe.mirai.utils.io.*
import kotlin.experimental.and
import kotlin.experimental.xor
@ -26,6 +27,8 @@ class DecryptionFailedException : Exception()
*/
fun ByteArray.encryptBy(key: ByteArray, length: Int = this.size): ByteArray = TEA.encrypt(this, key, sourceLength = length)
fun ByteArray.encryptBy(key: DecrypterByteArray, length: Int = this.size): ByteArray = TEA.encrypt(this, key.value, sourceLength = length)
/**
* 通过 [String.hexToBytes] [keyHex] 转为 [ByteArray] 后用它解密 [this].
* 将会使用 [HexCache]

View File

@ -3,14 +3,6 @@
package net.mamoe.mirai.utils.io
import kotlinx.io.core.*
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.network.protocol.tim.packet.KnownPacketId.*
import net.mamoe.mirai.network.protocol.tim.packet.action.*
import net.mamoe.mirai.network.protocol.tim.packet.event.ServerEventPacket
import net.mamoe.mirai.network.protocol.tim.packet.login.*
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.decryptBy
fun ByteReadPacket.readRemainingBytes(
@ -23,66 +15,6 @@ fun ByteReadPacket.readIoBuffer(
fun ByteReadPacket.readIoBuffer(n: Number) = this.readIoBuffer(n.toInt())
//必须消耗完 packet
fun ByteReadPacket.parseServerPacket(size: Int): ServerPacket {
discardExact(3)
val id = readUShort()
val sequenceId = readUShort()
discardExact(7)//4 for qq number, 3 for 0x00 0x00 0x00. 但更可能是应该 discard 8
return when (PacketId(id)) {
TOUCH -> TouchResponsePacket.Encrypted(this)
LOGIN ->
//todo 不要用size分析
when {
size == 271 || size == 207 -> LoginResponseKeyExchangeResponsePacket.Encrypted(this)
size == 871 -> LoginResponseCaptchaInitPacket.Encrypted(this)
size > 700 -> LoginResponseSuccessPacket.Encrypted(this)
else -> LoginResponseFailedPacket(
when (size) {
135 -> {//包数据错误. 目前怀疑是 tlv0006
this.readRemainingBytes().cutTail(1).decryptBy(TIMProtocol.shareKey).read {
discardExact(51)
MiraiLogger.error("Internal error: " + readUShortLVString())//抱歉,请重新输入密码。
}
LoginResult.INTERNAL_ERROR
}
319, 351 -> LoginResult.WRONG_PASSWORD
//135 -> LoginState.RETYPE_PASSWORD
63 -> LoginResult.BLOCKED
263 -> LoginResult.UNKNOWN_QQ_NUMBER
279, 495, 551, 487 -> LoginResult.DEVICE_LOCK
343, 359 -> LoginResult.TAKEN_BACK
else -> LoginResult.UNKNOWN
}, this)
}
SESSION_KEY -> SessionKeyResponsePacket.Encrypted(this)
CHANGE_ONLINE_STATUS -> ServerLoginSuccessPacket(this)
CAPTCHA -> ServerCaptchaPacket.Encrypted(this)
SERVER_EVENT_1, SERVER_EVENT_2 -> ServerEventPacket.Raw.Encrypted(this, PacketId(id), sequenceId)
FRIEND_ONLINE_STATUS_CHANGE -> FriendOnlineStatusChangedPacket.Encrypted(this)
S_KEY -> ResponsePacket.Encrypted<RequestSKeyPacket.Response>(this)
ACCOUNT_INFO -> ResponsePacket.Encrypted<RequestAccountInfoPacket.Response>(this)
SEND_GROUP_MESSAGE -> ResponsePacket.Encrypted<SendGroupMessagePacket.Response>(this)
SEND_FRIEND_MESSAGE -> ResponsePacket.Encrypted<SendFriendMessagePacket.Response>(this)
CAN_ADD_FRIEND -> ResponsePacket.Encrypted<CanAddFriendPacket.Response>(this)
HEARTBEAT -> ResponsePacket.Encrypted<HeartbeatPacket.Response>(this)
GROUP_IMAGE_ID -> ResponsePacket.Encrypted<GroupImageIdRequestPacket.Response>(this)
FRIEND_IMAGE_ID -> ResponsePacket.Encrypted<FriendImageIdRequestPacket.Response>(this)
REQUEST_PROFILE_DETAILS -> ResponsePacket.Encrypted<RequestProfileDetailsPacket.Response>(this)
// 0x01_BDu -> EventResponse.Encrypted<SubmitImageFilenamePacket.Response>(this)
else -> UnknownServerPacket.Encrypted(this, PacketId(id), sequenceId)
}.applySequence(sequenceId)
}
fun Input.readIP(): String = buildString(4 + 3) {
repeat(4) {
val byte = readUByte()

View File

@ -7,6 +7,8 @@ import kotlinx.io.pool.useInstance
import net.mamoe.mirai.contact.GroupId
import net.mamoe.mirai.contact.GroupInternalId
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.DecrypterByteArray
import net.mamoe.mirai.network.protocol.tim.packet.login.PrivateKey
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.internal.coerceAtMostOrFail
import kotlin.random.Random
@ -20,6 +22,7 @@ fun BytePacketBuilder.writeQQ(qq: Long) = this.writeUInt(qq.toUInt())
fun BytePacketBuilder.writeQQ(qq: UInt) = this.writeUInt(qq)
fun BytePacketBuilder.writeGroup(groupId: GroupId) = this.writeUInt(groupId.value)
fun BytePacketBuilder.writeGroup(groupInternalId: GroupInternalId) = this.writeUInt(groupInternalId.value)
fun BytePacketBuilder.writeFully(value: DecrypterByteArray) = this.writeFully(value.value)
fun BytePacketBuilder.writeShortLVByteArray(byteArray: ByteArray) {
this.writeShort(byteArray.size.toShort())
@ -109,9 +112,11 @@ fun BytePacketBuilder.encryptAndWrite(key: IoBuffer, encoder: BytePacketBuilder.
encryptAndWrite(it, encoder)
}
fun BytePacketBuilder.encryptAndWrite(key: DecrypterByteArray, encoder: BytePacketBuilder.() -> Unit) = encryptAndWrite(key.value, encoder)
fun BytePacketBuilder.encryptAndWrite(keyHex: String, encoder: BytePacketBuilder.() -> Unit) = encryptAndWrite(keyHex.hexToBytes(), encoder)
fun BytePacketBuilder.writeTLV0006(qq: UInt, password: String, loginTime: Int, loginIP: String, privateKey: ByteArray) {
fun BytePacketBuilder.writeTLV0006(qq: UInt, password: String, loginTime: Int, loginIP: String, privateKey: PrivateKey) {
val firstMD5 = md5(password)
val secondMD5 = md5(firstMD5 + byteArrayOf(0, 0, 0, 0) + qq.toUInt().toByteArray())
@ -130,7 +135,7 @@ fun BytePacketBuilder.writeTLV0006(qq: UInt, password: String, loginTime: Int, l
writeZero(8)
writeHex("00 10")//这两个hex是passwordSubmissionTLV2的末尾
writeHex("15 74 C4 89 85 7A 19 F5 5E A9 C9 A3 5E 8A 5A 9B")//16
writeFully(privateKey)
writeFully(privateKey.value)
}
}

View File

@ -1,6 +1,7 @@
package net.mamoe.mirai.event
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
internal actual val EventDispatcher: CoroutineDispatcher get() = Dispatchers.Default
internal actual val EventDispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()

View File

@ -1,5 +1,6 @@
package net.mamoe.mirai.utils
import io.ktor.util.KtorExperimentalAPI
import io.ktor.util.cio.writeChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
@ -19,14 +20,15 @@ import kotlin.math.min
*
* @return 用户输入得到的验证码
*/
@KtorExperimentalAPI
internal actual suspend fun solveCaptcha(captchaBuffer: IoBuffer): String? = captchaLock.withLock {
val tempFile: File = createTempFile(suffix = ".png").apply { deleteOnExit() }
withContext(Dispatchers.IO) {
tempFile.createNewFile()
@Suppress("EXPERIMENTAL_API_USAGE")
MiraiLogger.info("需要验证码登录, 验证码为 4 字母")
try {
tempFile.writeChannel().writeFully(captchaBuffer)
MiraiLogger.info("需要验证码登录, 验证码为 4 字母")
MiraiLogger.info("若看不清字符图片, 请查看 ${tempFile.absolutePath}")
} catch (e: Exception) {
MiraiLogger.info("无法写出验证码文件(${e.message}), 请尝试查看以上字符图片")

View File

@ -2,12 +2,11 @@ apply plugin: "kotlin"
apply plugin: "java"
dependencies {
implementation project(":mirai-core")
api project(":mirai-core")
runtime files("../../mirai-core/build/classes/kotlin/jvm/main") // mpp targeting android limitation
api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlin_version
api group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: coroutines_version
compile group: 'com.alibaba', name: 'fastjson', version: '1.2.62'
implementation 'org.jsoup:jsoup:1.12.1'
implementation files('./lib/ExImageGallery.jar')
}

View File

@ -33,8 +33,8 @@ private fun readTestAccount(): BotAccount? {
suspend fun main() {
val bot = Bot(
readTestAccount() ?: BotAccount(
id = 1994701121u,
password = "123456"
id = 913366033u,
password = "a18260132383"
)
).apply { login().requireSuccess() }