mirror of
https://github.com/mamoe/mirai.git
synced 2025-03-09 19:50:27 +08:00
Redesign packets
This commit is contained in:
parent
6f498554b7
commit
bd9cf3bfd1
@ -0,0 +1,3 @@
|
||||
package net.mamoe.mirai
|
||||
|
||||
actual object MiraiEnvironment
|
@ -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()
|
@ -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()
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
@ -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")
|
||||
}
|
@ -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
|
10
mirai-core/src/androidMain/res/AndroidManifest.xml
Normal file
10
mirai-core/src/androidMain/res/AndroidManifest.xml
Normal 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>
|
@ -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()) {
|
||||
|
@ -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)
|
@ -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
|
@ -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].
|
||||
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 -> {
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
/**
|
||||
* 发送一个数据包(非异步).
|
||||
*
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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>
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 =
|
||||
|
@ -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>
|
||||
)
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +0,0 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.action
|
||||
|
||||
/**
|
||||
* 添加好友结果
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
enum class AddFriendResult {
|
||||
/**
|
||||
* 等待对方处理
|
||||
*/
|
||||
WAITING_FOR_AGREEMENT,
|
||||
|
||||
/**
|
||||
* 和对方已经是好友了
|
||||
*/
|
||||
ALREADY_ADDED,
|
||||
|
||||
/**
|
||||
* 对方设置为不添加好友等
|
||||
*/
|
||||
FAILED,
|
||||
}
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
@ -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
|
||||
}
|
||||
}
|
@ -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())
|
||||
}))*/
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 的返回包
|
@ -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)
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
/**
|
||||
|
@ -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)
|
||||
|
||||
/**
|
||||
* 外部图片. 图片数据还没有读取到内存.
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
@ -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}), 请尝试查看以上字符图片")
|
||||
|
@ -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')
|
||||
}
|
||||
|
Binary file not shown.
@ -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() }
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user