mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-30 19:00:33 +08:00
Multiplatform
This commit is contained in:
parent
c09d44f45a
commit
d89f8acc43
@ -4,6 +4,7 @@ ext {
|
||||
// kotlin
|
||||
kotlinJvm = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
kotlinCommon = "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version"
|
||||
kotlinNative = "org.jetbrains.kotlin:kotlin-stdlib-native:$kotlin_version"
|
||||
|
||||
// coroutine
|
||||
coroutine_version = "1.3.0"
|
||||
@ -13,6 +14,8 @@ ext {
|
||||
coroutineAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
|
||||
coroutineJs = "org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$coroutine_version"
|
||||
|
||||
coroutineIo = "org.jetbrains.kotlinx:kotlinx-coroutines-io:0.24.0"
|
||||
|
||||
// reflect
|
||||
reflect = "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
|
||||
@ -25,5 +28,6 @@ ext {
|
||||
kotlinxIOJvm = "org.jetbrains.kotlinx:kotlinx-io-jvm:$kotlinx_io_version"
|
||||
kotlinxIOCommon = "org.jetbrains.kotlinx:kotlinx-io:$kotlinx_io_version"
|
||||
kotlinxIOJS = "org.jetbrains.kotlinx:kotlinx-io-js:$kotlinx_io_version"
|
||||
kotlinxIONative = "org.jetbrains.kotlinx:kotlinx-io-native:$kotlinx_io_version"
|
||||
|
||||
}
|
||||
|
@ -1,17 +1,16 @@
|
||||
package net.mamoe.mirai
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.LoginState
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
|
||||
import net.mamoe.mirai.utils.BotAccount
|
||||
import net.mamoe.mirai.utils.LoggerTextFormat
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.config.MiraiConfig
|
||||
import net.mamoe.mirai.utils.setting.MiraiSettings
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mirai 服务器.
|
||||
@ -40,11 +39,11 @@ object MiraiServer {
|
||||
|
||||
this.logger = MiraiLogger
|
||||
|
||||
logger.info("About to run Mirai (" + Mirai.VERSION + ") under " + if (isUnix) "unix" else "windows")
|
||||
logger.info("Loading data under " + LoggerTextFormat.GREEN + this.parentFolder)
|
||||
logger.logInfo("About to run Mirai (" + Mirai.VERSION + ") under " + if (isUnix) "unix" else "windows")
|
||||
logger.logInfo("Loading data under " + LoggerTextFormat.GREEN + this.parentFolder)
|
||||
|
||||
val setting = this.parentFolder + "/Mirai.ini"
|
||||
logger.info("Selecting setting from " + LoggerTextFormat.GREEN + setting)
|
||||
val setting = File(this.parentFolder, "/Mirai.ini")
|
||||
logger.logInfo("Selecting setting from " + LoggerTextFormat.GREEN + setting)
|
||||
|
||||
/*
|
||||
if (!setting.exists()) {
|
||||
@ -54,7 +53,7 @@ object MiraiServer {
|
||||
}
|
||||
|
||||
File qqs = new File(this.parentFolder + "/QQ.yml");
|
||||
getLogger().info("Reading QQ accounts from " + LoggerTextFormat.GREEN + qqs);
|
||||
getLogger().logInfo("Reading QQ accounts from " + LoggerTextFormat.GREEN + qqs);
|
||||
if (!qqs.exists()) {
|
||||
this.initQQConfig(qqs);
|
||||
} else {
|
||||
@ -67,10 +66,10 @@ object MiraiServer {
|
||||
/*
|
||||
MiraiSettingMapSection qqs = this.setting.getMapSection("qq");
|
||||
qqs.forEach((a,p) -> {
|
||||
this.getLogger().info("Finding available ports between " + "1-65536");
|
||||
this.getLogger().logInfo("Finding available ports between " + "1-65536");
|
||||
try {
|
||||
int port = MiraiNetwork.getAvailablePort();
|
||||
this.getLogger().info("Listening on port " + port);
|
||||
this.getLogger().logInfo("Listening on port " + port);
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
@ -84,18 +83,18 @@ object MiraiServer {
|
||||
|
||||
fun shutdown() {
|
||||
if (this.enabled) {
|
||||
logger.info("About to shutdown Mirai")
|
||||
logger.info("Data have been saved")
|
||||
logger.logInfo("About to shutdown Mirai")
|
||||
logger.logInfo("Data have been saved")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun initSetting(setting: File) {
|
||||
logger.info("Thanks for using Mirai")
|
||||
logger.info("initializing Settings")
|
||||
logger.logInfo("Thanks for using Mirai")
|
||||
logger.logInfo("initializing Settings")
|
||||
try {
|
||||
if (setting.createNewFile()) {
|
||||
logger.info("Mirai Config Created")
|
||||
logger.logInfo("Mirai Config Created")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
@ -113,24 +112,24 @@ object MiraiServer {
|
||||
worker["core_task_pool_worker_amount"] = 5
|
||||
|
||||
val plugin = this.settings.getMapSection("plugin")
|
||||
plugin["debug"] = false
|
||||
plugin["logDebug"] = false
|
||||
|
||||
this.settings.save()
|
||||
logger.info("initialized; changing can be made in setting file: $setting")
|
||||
logger.logInfo("initialized; changing can be made in setting file: $setting")
|
||||
}
|
||||
|
||||
private fun initQQConfig(qqConfig: File) {
|
||||
this.qqs = MiraiConfig(qqConfig)
|
||||
MiraiLogger.info("QQ account initialized; changing can be made in Config file: $qqConfig")
|
||||
logger.info("QQ 账户管理初始化完毕")
|
||||
MiraiLogger.logInfo("QQ account initialized; changing can be made in Config file: $qqConfig")
|
||||
logger.logInfo("QQ 账户管理初始化完毕")
|
||||
}
|
||||
|
||||
private fun reload() {
|
||||
this.enabled = true
|
||||
MiraiLogger.info(LoggerTextFormat.GREEN.toString() + "Server enabled; Welcome to Mirai")
|
||||
MiraiLogger.info("Mirai Version=" + Mirai.VERSION)
|
||||
MiraiLogger.logInfo(LoggerTextFormat.GREEN.toString() + "Server enabled; Welcome to Mirai")
|
||||
MiraiLogger.logInfo("Mirai Version=" + Mirai.VERSION)
|
||||
|
||||
MiraiLogger.info("Initializing [Bot]s")
|
||||
MiraiLogger.logInfo("Initializing [Bot]s")
|
||||
|
||||
try {
|
||||
availableBot
|
||||
@ -142,23 +141,23 @@ object MiraiServer {
|
||||
|
||||
/*
|
||||
this.qqs.keySet().stream().map(key -> this.qqs.getSection(key)).forEach(section -> {
|
||||
getLogger().info("Initializing [Bot] " + section.getString("account"));
|
||||
getLogger().logInfo("Initializing [Bot] " + section.getString("account"));
|
||||
try {
|
||||
Bot bot = new Bot(section);
|
||||
var state = bot.network.login$mirai_core().of();
|
||||
//bot.network.login$mirai_core().whenComplete((state, e) -> {
|
||||
if (state == LoginState.SUCCESS) {
|
||||
Bot.instances.add(bot);
|
||||
getLogger().green(" Login Succeed");
|
||||
getLogger().logGreen(" Login Succeed");
|
||||
} else {
|
||||
getLogger().error(" Login Failed with error " + state);
|
||||
getLogger().logError(" Login Failed with logError " + state);
|
||||
bot.close();
|
||||
}
|
||||
// }).of();
|
||||
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
getLogger().error("Could not load QQ bots config!");
|
||||
getLogger().logError("Could not load QQ bots config!");
|
||||
System.exit(1);
|
||||
}
|
||||
});*/
|
||||
@ -173,9 +172,9 @@ object MiraiServer {
|
||||
get() {
|
||||
for (it in qqList.split("\n").dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
||||
val strings = it.split("----").dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
val bot = Bot(BotAccount(strings[0].toLong(), strings[1]), Console())
|
||||
val bot = Bot(BotAccount(strings[0].toLong(), strings[1]), MiraiLogger)
|
||||
|
||||
if (runBlocking { bot.login() } === LoginState.SUCCESS) {
|
||||
if (runBlocking { bot.login() } === LoginResult.SUCCESS) {
|
||||
bot.green("Login succeed")
|
||||
return bot
|
||||
}
|
||||
|
@ -4,22 +4,40 @@ apply plugin: "kotlin-multiplatform"
|
||||
kotlin {
|
||||
targets {
|
||||
fromPreset(presets.jvm, "jvm")
|
||||
//fromPreset(presets.mingwX64, "mingwX64")
|
||||
}
|
||||
jvm()
|
||||
|
||||
/*
|
||||
mingwX64("mingwX64") {
|
||||
binaries {
|
||||
executable {
|
||||
// Change to specify fully qualified name of your application's entry point:
|
||||
entryPoint = 'main'
|
||||
// Specify command-line arguments, if necessary:
|
||||
runTask?.args('')
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect
|
||||
implementation rootProject.ext.kotlinCommon
|
||||
|
||||
implementation rootProject.ext.reflect
|
||||
implementation rootProject.ext.coroutine
|
||||
implementation rootProject.ext.kotlinJvm
|
||||
|
||||
//implementation rootProject.ext.coroutine
|
||||
implementation rootProject.ext.coroutineCommon
|
||||
implementation rootProject.ext.coroutineIo
|
||||
|
||||
implementation rootProject.ext.atomicFUCommon
|
||||
|
||||
implementation rootProject.ext.kotlinxIOCommon
|
||||
}
|
||||
}
|
||||
|
||||
jvmMain {
|
||||
apply plugin: 'java'
|
||||
|
||||
@ -35,6 +53,23 @@ kotlin {
|
||||
implementation 'org.ini4j:ini4j:0.5.2'
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
mingwX64Main {
|
||||
|
||||
dependencies {
|
||||
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect
|
||||
implementation rootProject.ext.kotlinCommon
|
||||
implementation rootProject.ext.coroutine
|
||||
implementation rootProject.ext.coroutineNative
|
||||
implementation rootProject.ext.kotlinNative
|
||||
implementation rootProject.ext.reflect
|
||||
|
||||
//implementation rootProject.ext.coroutine
|
||||
implementation rootProject.ext.kotlinxIONative
|
||||
}
|
||||
}*/
|
||||
|
||||
jvmTest {
|
||||
}
|
||||
|
||||
@ -46,17 +81,4 @@ kotlin {
|
||||
|
||||
compileKotlinJvm {
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
dependencies {
|
||||
compile 'com.google.protobuf:protobuf-java:3.5.0'
|
||||
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M2'
|
||||
compile 'io.netty:netty-all:4.1.38.Final'
|
||||
compile 'net.java.dev.jna:jna:5.4.0'
|
||||
compile 'org.apache.logging.log4j:log4j-core:2.12.1'
|
||||
compile 'org.yaml:snakeyaml:1.18'
|
||||
compile 'org.jetbrains.kotlin:kotlin-reflect:1.3.41'
|
||||
compile 'org.jsoup:jsoup:1.12.1'
|
||||
compile 'org.ini4j:ini4j:0.5.2'
|
||||
}*/
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package net.mamoe.mirai
|
||||
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import net.mamoe.mirai.Bot.ContactSystem
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.QQ
|
||||
@ -12,12 +14,12 @@ import net.mamoe.mirai.utils.MiraiLogger
|
||||
/**
|
||||
* Mirai 的机器人. 一个机器人实例登录一个 QQ 账号.
|
||||
* Mirai 为多账号设计, 可同时维护多个机器人.
|
||||
* <br></br>
|
||||
*
|
||||
* [Bot] 由 3 个模块组成.
|
||||
* [联系人管理][ContactSystem]: 可通过 [Bot.contacts] 访问
|
||||
* [网络处理器][TIMBotNetworkHandler]: 可通过 [Bot.network] 访问
|
||||
* [机器人账号信息][BotAccount]: 可通过 [Bot.account] 访问
|
||||
* <br></br>
|
||||
*
|
||||
* 若你需要得到机器人的 QQ 账号, 请访问 [Bot.account]
|
||||
* 若你需要得到服务器上所有机器人列表, 请访问 [Bot.instances]
|
||||
*
|
||||
@ -28,7 +30,7 @@ import net.mamoe.mirai.utils.MiraiLogger
|
||||
* a [ContactSystem], which manage contacts such as [QQ] and [Group];
|
||||
* a [TIMBotNetworkHandler], which manages the connection to the server;
|
||||
* a [BotAccount], which stores the account information(e.g. qq number the bot)
|
||||
* <br></br>
|
||||
*
|
||||
* To of all the QQ contacts, access [Bot.account]
|
||||
* To of all the Robot instance, access [Bot.instances]
|
||||
*
|
||||
@ -42,7 +44,7 @@ class Bot(val account: BotAccount, val logger: MiraiLogger) {
|
||||
|
||||
val contacts = ContactSystem()
|
||||
|
||||
val network: BotNetworkHandler = TIMBotNetworkHandler(this)
|
||||
val network: BotNetworkHandler<*> = TIMBotNetworkHandler(this)
|
||||
|
||||
init {
|
||||
instances.add(this)
|
||||
@ -59,25 +61,22 @@ class Bot(val account: BotAccount, val logger: MiraiLogger) {
|
||||
*/
|
||||
inner class ContactSystem internal constructor() {
|
||||
val groups = ContactList<Group>()
|
||||
private val groupsLock = Mutex()
|
||||
val qqs = ContactList<QQ>()
|
||||
private val qqsLock = Mutex()
|
||||
|
||||
fun getQQ(qqNumber: Long): QQ {
|
||||
synchronized(this.qqs) {
|
||||
if (!this.qqs.containsKey(qqNumber)) {
|
||||
this.qqs[qqNumber] = QQ(this@Bot, qqNumber)
|
||||
}
|
||||
return this.qqs[qqNumber]!!
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 通过群号码获取群对象.
|
||||
* 注意: 在并发调用时, 这个方法并不是原子的.
|
||||
*/
|
||||
fun getQQ(qqNumber: Long): QQ = qqs.getOrPut(qqNumber) { QQ(this@Bot, qqNumber) }
|
||||
|
||||
/**
|
||||
* 通过群号码获取群对象.
|
||||
* 注意: 在并发调用时, 这个方法并不是原子的.
|
||||
*/
|
||||
fun getGroupByNumber(groupNumber: Long): Group = groups.getOrPut(groupNumber) { Group(this@Bot, groupNumber) }
|
||||
|
||||
fun getGroupByNumber(groupNumber: Long): Group {
|
||||
synchronized(this.groups) {
|
||||
if (!this.groups.containsKey(groupNumber)) {
|
||||
this.groups[groupNumber] = Group(this@Bot, groupNumber)
|
||||
}
|
||||
return this.groups[groupNumber]!!
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupById(groupId: Long): Group {
|
||||
return getGroupByNumber(Group.groupIdToNumber(groupId))
|
||||
@ -86,7 +85,6 @@ class Bot(val account: BotAccount, val logger: MiraiLogger) {
|
||||
|
||||
fun close() {
|
||||
this.network.close()
|
||||
this.contacts.groups.values.forEach { it.close() }
|
||||
this.contacts.groups.clear()
|
||||
this.contacts.qqs.clear()
|
||||
}
|
||||
@ -94,8 +92,7 @@ class Bot(val account: BotAccount, val logger: MiraiLogger) {
|
||||
companion object {
|
||||
val instances: MutableList<Bot> = mutableListOf()
|
||||
|
||||
private var id = 0
|
||||
private val idLock = Any()
|
||||
fun nextId(): Int = synchronized(idLock) { id++ }
|
||||
private val id = atomic(0)
|
||||
fun nextId(): Int = id.addAndGet(1)
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
@file:Suppress("unused", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai
|
||||
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.QQ
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
|
||||
import net.mamoe.mirai.utils.ContactList
|
||||
import net.mamoe.mirai.utils.LoginConfiguration
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
|
||||
/**
|
||||
* The mirror of functions in inner classes of [Bot]
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
//Contacts
|
||||
fun Bot.getQQ(number: Long): QQ = this.contacts.getQQ(number)
|
||||
|
||||
fun Bot.getQQ(number: UInt): QQ = getQQ(number.toLong())
|
||||
|
||||
fun Bot.getGroupByNumber(number: Long): Group = this.contacts.getGroupByNumber(number)
|
||||
fun Bot.getGroupByNumber(number: UInt): Group = getGroupByNumber(number.toLong())
|
||||
|
||||
fun Bot.getGroupById(number: Long): Group = this.contacts.getGroupById(number)
|
||||
|
||||
val Bot.groups: ContactList<Group> get() = this.contacts.groups
|
||||
|
||||
val Bot.qqs: ContactList<QQ> get() = this.contacts.qqs
|
||||
|
||||
|
||||
//NetworkHandler
|
||||
suspend fun Bot.sendPacket(packet: ClientPacket) = this.network.sendPacket(packet)
|
||||
|
||||
suspend fun Bot.login(configuration: LoginConfiguration.() -> Unit): LoginResult = this.network.login(LoginConfiguration().apply(configuration))
|
||||
|
||||
suspend fun Bot.login(): LoginResult = this.network.login(LoginConfiguration.Default)
|
||||
|
||||
//BotAccount
|
||||
val Bot.qqNumber: Long get() = this.account.qqNumber
|
||||
|
||||
|
||||
//logging
|
||||
fun Bot.log(o: Any?) = info(o)
|
||||
|
||||
fun Bot.println(o: Any?) = info(o)
|
||||
fun Bot.info(o: Any?) = this.logger.logInfo(o)
|
||||
|
||||
fun Bot.error(o: Any?) = this.logger.logError(o)
|
||||
|
||||
fun Bot.purple(o: Any?) = this.logger.logPurple(o)
|
||||
|
||||
fun Bot.cyan(o: Any?) = this.logger.logCyan(o)
|
||||
fun Bot.green(o: Any?) = this.logger.logGreen(o)
|
||||
|
||||
fun Bot.debug(o: Any?) = this.logger.logDebug(o)
|
||||
|
||||
fun Bot.printPacketDebugging(packet: ServerPacket) {
|
||||
debug("Packet=$packet")
|
||||
debug("Packet size=" + packet.input.readBytes().size)
|
||||
debug("Packet data=" + packet.input.readBytes().toUHexString())
|
||||
}
|
@ -1,9 +1,5 @@
|
||||
package net.mamoe.mirai
|
||||
|
||||
//expect fun s(): String
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
object Mirai {
|
||||
const val VERSION: String = "1.0.0"
|
||||
}
|
@ -4,7 +4,7 @@ import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.Group.Companion.groupNumberToId
|
||||
import net.mamoe.mirai.message.MessageChain
|
||||
import net.mamoe.mirai.utils.ContactList
|
||||
import java.io.Closeable
|
||||
import kotlin.jvm.JvmStatic
|
||||
|
||||
/**
|
||||
* 群.
|
||||
@ -21,22 +21,20 @@ import java.io.Closeable
|
||||
* Java 调用 [groupNumberToId] : `Group.groupNumberToId(number)`
|
||||
* @author Him188moe
|
||||
*/
|
||||
class Group(bot: Bot, number: Long) : Contact(bot, number), Closeable {
|
||||
class Group(bot: Bot, number: Long) : Contact(bot, number) {
|
||||
val groupId = groupNumberToId(number)
|
||||
val members = ContactList<QQ>()//todo members
|
||||
val members: ContactList<QQ>
|
||||
//todo members
|
||||
get() = throw UnsupportedOperationException("Not yet supported")
|
||||
|
||||
override suspend fun sendMessage(message: MessageChain) {
|
||||
bot.network.message.sendGroupMessage(this, message)
|
||||
bot.network.event.sendGroupMessage(this, message)
|
||||
}
|
||||
|
||||
override suspend fun sendXMLMessage(message: String) {
|
||||
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
this.members.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun groupNumberToId(number: Long): Long {//求你别出错
|
@ -12,34 +12,26 @@ import net.mamoe.mirai.message.MessageChain
|
||||
* Java 获取 qq 号: `qq.getNumber()`
|
||||
* Java 获取所属 bot: `qq.getBot()`
|
||||
*
|
||||
* A QQ instance helps you to receive message from or sendPacket message to.
|
||||
* A QQ instance helps you to receive event from or sendPacket event to.
|
||||
* Notice that, one QQ instance belong to one [Bot], that is, QQ instances from different [Bot] are NOT the same.
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
class QQ(bot: Bot, number: Long) : Contact(bot, number) {
|
||||
override suspend fun sendMessage(message: MessageChain) {
|
||||
bot.network.message.sendFriendMessage(this, message)
|
||||
bot.network.event.sendFriendMessage(this, message)
|
||||
}
|
||||
|
||||
override suspend fun sendXMLMessage(message: String) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* At(@) this account.
|
||||
*
|
||||
* @return an instance of [Message].
|
||||
*/
|
||||
fun at(): At {
|
||||
return At(this)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Make that we can use (QQ + QQ2 + QQ3).sendMessage( )
|
||||
|
||||
operator fun plus(qq: QQ): QQCombination {
|
||||
|
||||
}*/
|
||||
}
|
||||
|
||||
/**
|
||||
* At(@) this account.
|
||||
*
|
||||
* @return an instance of [Message].
|
||||
*/
|
||||
fun QQ.at(): At {
|
||||
return At(this)
|
||||
}
|
@ -2,10 +2,16 @@
|
||||
|
||||
package net.mamoe.mirai.event
|
||||
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.event.internal.broadcastInternal
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.utils.log
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
/**
|
||||
* 所有事件的基类.
|
||||
@ -13,7 +19,6 @@ import net.mamoe.mirai.event.internal.broadcastInternal
|
||||
*
|
||||
* @see [broadcast] 广播事件
|
||||
* @see [subscribe] 监听事件
|
||||
* @author Him188moe
|
||||
*/
|
||||
abstract class Event {
|
||||
|
||||
@ -31,7 +36,6 @@ abstract class Event {
|
||||
*
|
||||
* @throws UnsupportedOperationException 如果事件没有实现 [Cancellable]
|
||||
*/
|
||||
@Throws(UnsupportedOperationException::class)
|
||||
fun cancel() {
|
||||
cancelled = true
|
||||
}
|
||||
@ -39,8 +43,6 @@ abstract class Event {
|
||||
|
||||
/**
|
||||
* 实现这个接口的事件可以被取消.
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
interface Cancellable {
|
||||
val cancelled: Boolean
|
||||
@ -51,8 +53,23 @@ interface Cancellable {
|
||||
/**
|
||||
* 广播一个事件的唯一途径
|
||||
*/
|
||||
@Synchronized
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
suspend fun <E : Event> E.broadcast(): E = withContext(EventScope.coroutineContext) { this@broadcast.broadcastInternal() }
|
||||
@JvmOverloads
|
||||
suspend fun <E : Event> E.broadcast(context: CoroutineContext? = null): E {
|
||||
var ctx = EventScope.coroutineContext
|
||||
if (context != null) {
|
||||
if (context[CoroutineExceptionHandler] == null)
|
||||
ctx += CoroutineExceptionHandler { _, e -> e.log() }
|
||||
ctx += context
|
||||
}
|
||||
return withContext(ctx) { this@broadcast.broadcastInternal() }
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件协程作用域.
|
||||
* 所有的事件 [broadcast] 过程均在此作用域下运行.
|
||||
*
|
||||
* 然而, 若在事件处理过程中使用到 [Contact.sendMessage] 等会 [发送数据包][BotNetworkHandler.sendPacket] 的方法,
|
||||
* 发送过程将会通过 [withContext] 将协程切换到 [BotNetworkHandler.NetworkScope]
|
||||
*/
|
||||
object EventScope : CoroutineScope by CoroutineScope(Dispatchers.Default)//todo may change
|
@ -5,6 +5,7 @@ package net.mamoe.mirai.event
|
||||
import net.mamoe.mirai.event.internal.Handler
|
||||
import net.mamoe.mirai.event.internal.listeners
|
||||
import net.mamoe.mirai.event.internal.subscribeInternal
|
||||
import kotlin.jvm.Synchronized
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
enum class ListeningStatus {
|
||||
@ -76,8 +77,8 @@ inline fun <reified E : Event> subscribeAll(noinline listeners: ListenerBuilder<
|
||||
* }
|
||||
*
|
||||
* untilFalse {
|
||||
* it.reply("你发送了 ${it.message}")
|
||||
* it.message eq "停止"
|
||||
* it.reply("你发送了 ${it.event}")
|
||||
* it.event eq "停止"
|
||||
* }
|
||||
* }
|
||||
* ```
|
@ -3,9 +3,7 @@ package net.mamoe.mirai.event.events
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.event.Event
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
abstract class BotEvent(val bot: Bot) : Event()
|
||||
|
||||
class BotLoginSucceedEvent(bot: Bot) : BotEvent(bot)
|
@ -5,9 +5,7 @@ import net.mamoe.mirai.contact.QQ
|
||||
import net.mamoe.mirai.message.Message
|
||||
import net.mamoe.mirai.message.MessageChain
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
abstract class FriendEvent(bot: Bot, val sender: QQ) : BotEvent(bot)
|
||||
|
||||
/**
|
||||
@ -16,15 +14,11 @@ abstract class FriendEvent(bot: Bot, val sender: QQ) : BotEvent(bot)
|
||||
* @author Him188moe
|
||||
*/
|
||||
class FriendMessageEvent(bot: Bot, sender: QQ, val message: MessageChain) : FriendEvent(bot, sender) {
|
||||
@JvmSynthetic
|
||||
suspend inline fun reply(message: Message) = sender.sendMessage(message)
|
||||
|
||||
@JvmSynthetic
|
||||
suspend inline fun reply(message: String) = sender.sendMessage(message)
|
||||
|
||||
@JvmSynthetic
|
||||
suspend inline fun reply(message: List<Message>) = sender.sendMessage(message)
|
||||
|
||||
@JvmSynthetic
|
||||
suspend inline fun reply(message: MessageChain) = sender.sendMessage(message)//shortcut
|
||||
}
|
@ -6,24 +6,16 @@ import net.mamoe.mirai.contact.QQ
|
||||
import net.mamoe.mirai.message.Message
|
||||
import net.mamoe.mirai.message.MessageChain
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
abstract class GroupEvent(bot: Bot, val group: Group) : BotEvent(bot)
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
class GroupMessageEvent(bot: Bot, group: Group, val sender: QQ, val message: MessageChain) : GroupEvent(bot, group) {
|
||||
@JvmSynthetic
|
||||
suspend inline fun reply(message: Message) = group.sendMessage(message)
|
||||
|
||||
@JvmSynthetic
|
||||
suspend inline fun reply(message: String) = group.sendMessage(message)
|
||||
|
||||
@JvmSynthetic
|
||||
suspend inline fun reply(message: List<Message>) = group.sendMessage(message)
|
||||
|
||||
@JvmSynthetic
|
||||
suspend inline fun reply(message: MessageChain) = group.sendMessage(message)
|
||||
}
|
@ -8,9 +8,7 @@ import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
|
||||
/* Abstract */
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
sealed class PacketEvent<out P : Packet>(bot: Bot, open val packet: P) : BotEvent(bot)
|
||||
|
||||
|
@ -1,19 +1,10 @@
|
||||
package net.mamoe.mirai.event.internal
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import net.mamoe.mirai.Bot
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.mamoe.mirai.event.Event
|
||||
import net.mamoe.mirai.event.ListeningStatus
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.ServerPacketReceivedEvent
|
||||
import net.mamoe.mirai.event.subscribeAlways
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.dataInputStream
|
||||
import net.mamoe.mirai.utils.BotAccount
|
||||
import net.mamoe.mirai.utils.Console
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.allSuperclasses
|
||||
import kotlin.reflect.full.isSuperclassOf
|
||||
|
||||
/**
|
||||
* 监听和广播实现
|
||||
@ -42,29 +33,31 @@ class Handler<E : Event>(val handler: suspend (E) -> ListeningStatus) : Listener
|
||||
|
||||
internal val <E : Event> KClass<E>.listeners: EventListeners<E> get() = EventListenerManger.get(this)
|
||||
|
||||
internal class EventListeners<E : Event> : MutableList<Listener<E>> by mutableListOf()
|
||||
internal class EventListeners<E : Event> : MutableList<Listener<E>> by mutableListOf() {
|
||||
val lock = Mutex()
|
||||
}
|
||||
|
||||
internal object EventListenerManger {
|
||||
private val registries: MutableMap<KClass<out Event>, EventListeners<out Event>> = mutableMapOf()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal fun <E : Event> get(clazz: KClass<E>): EventListeners<E> {
|
||||
synchronized(clazz) {
|
||||
if (registries.containsKey(clazz)) {
|
||||
return registries[clazz] as EventListeners<E>
|
||||
} else {
|
||||
EventListeners<E>().let {
|
||||
registries[clazz] = it
|
||||
return it
|
||||
}
|
||||
//synchronized(clazz) {
|
||||
if (registries.containsKey(clazz)) {
|
||||
return registries[clazz] as EventListeners<E>
|
||||
} else {
|
||||
EventListeners<E>().let {
|
||||
registries[clazz] = it
|
||||
return it
|
||||
}
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal suspend fun <E : Event> E.broadcastInternal(): E {
|
||||
suspend fun callListeners(listeners: EventListeners<in E>) {
|
||||
suspend fun callListeners(listeners: EventListeners<in E>) = listeners.lock.withLock {
|
||||
val iterator = listeners.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().onEvent(this) == ListeningStatus.STOPPED) {
|
||||
@ -74,24 +67,9 @@ internal suspend fun <E : Event> E.broadcastInternal(): E {
|
||||
}
|
||||
|
||||
callListeners(this::class.listeners as EventListeners<in E>)
|
||||
this::class.allSuperclasses.forEach {
|
||||
//println("super: " + it.simpleName)
|
||||
|
||||
if (Event::class.isSuperclassOf(it)) {
|
||||
callListeners((it as KClass<out Event>).listeners as EventListeners<in E>)
|
||||
}
|
||||
}
|
||||
|
||||
loopAllListeners(this::class) { callListeners(it as EventListeners<in E>) }
|
||||
return this
|
||||
}
|
||||
|
||||
suspend fun main() {
|
||||
ServerPacketReceivedEvent::class.subscribeAlways {
|
||||
println("got it")
|
||||
}
|
||||
|
||||
println(ServerPacketReceivedEvent::class.listeners.size)
|
||||
|
||||
ServerPacketReceivedEvent(Bot(BotAccount(1, ""), Console()), object : ServerPacket(byteArrayOf().dataInputStream()) {}).broadcast()
|
||||
delay(1000)
|
||||
}
|
||||
internal expect inline fun <E : Event> loopAllListeners(clazz: KClass<E>, consumer: (EventListeners<in E>) -> Unit)
|
@ -0,0 +1,168 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.message
|
||||
|
||||
/**
|
||||
* @author LamGC
|
||||
*/
|
||||
@Suppress("EnumEntryName", "unused", "SpellCheckingInspection")
|
||||
enum class FaceID constructor(val id: UByte) {
|
||||
unknown(0xffu),
|
||||
// TODO: 2019/9/1 添加更多表情
|
||||
jingya(0u),
|
||||
piezui(1u),
|
||||
se(2u),
|
||||
fadai(3u),
|
||||
deyi(4u),
|
||||
liulei(5u),
|
||||
haixiu(6u),
|
||||
bizui(7u),
|
||||
shui(8u),
|
||||
daku(9u),
|
||||
ganga(10u),
|
||||
fanu(11u),
|
||||
tiaopi(12u),
|
||||
ciya(13u),
|
||||
weixiao(14u),
|
||||
nanguo(15u),
|
||||
ku(16u),
|
||||
zhuakuang(18u),
|
||||
tu(19u),
|
||||
touxiao(20u),
|
||||
keai(21u),
|
||||
baiyan(22u),
|
||||
aoman(23u),
|
||||
ji_e(24u),
|
||||
kun(25u),
|
||||
jingkong(26u),
|
||||
liuhan(27u),
|
||||
hanxiao(28u),
|
||||
dabing(29u),
|
||||
fendou(30u),
|
||||
zhouma(31u),
|
||||
yiwen(32u),
|
||||
yun(34u),
|
||||
zhemo(35u),
|
||||
shuai(36u),
|
||||
kulou(37u),
|
||||
qiaoda(38u),
|
||||
zaijian(39u),
|
||||
fadou(41u),
|
||||
aiqing(42u),
|
||||
tiaotiao(43u),
|
||||
zhutou(46u),
|
||||
yongbao(49u),
|
||||
dan_gao(53u),
|
||||
shandian(54u),
|
||||
zhadan(55u),
|
||||
dao(56u),
|
||||
zuqiu(57u),
|
||||
bianbian(59u),
|
||||
kafei(60u),
|
||||
fan(61u),
|
||||
meigui(63u),
|
||||
diaoxie(64u),
|
||||
aixin(66u),
|
||||
xinsui(67u),
|
||||
liwu(69u),
|
||||
taiyang(74u),
|
||||
yueliang(75u),
|
||||
qiang(76u),
|
||||
ruo(77u),
|
||||
woshou(78u),
|
||||
shengli(79u),
|
||||
feiwen(85u),
|
||||
naohuo(86u),
|
||||
xigua(89u),
|
||||
lenghan(96u),
|
||||
cahan(97u),
|
||||
koubi(98u),
|
||||
guzhang(99u),
|
||||
qiudale(100u),
|
||||
huaixiao(101u),
|
||||
zuohengheng(102u),
|
||||
youhengheng(103u),
|
||||
haqian(104u),
|
||||
bishi(105u),
|
||||
weiqu(106u),
|
||||
kuaikule(107u),
|
||||
yinxian(108u),
|
||||
qinqin(109u),
|
||||
xia(110u),
|
||||
kelian(111u),
|
||||
caidao(112u),
|
||||
pijiu(113u),
|
||||
lanqiu(114u),
|
||||
pingpang(115u),
|
||||
shiai(116u),
|
||||
piaochong(117u),
|
||||
baoquan(118u),
|
||||
gouyin(119u),
|
||||
quantou(120u),
|
||||
chajin(121u),
|
||||
aini(122u),
|
||||
bu(123u),
|
||||
hao(124u),
|
||||
zhuanquan(125u),
|
||||
ketou(126u),
|
||||
huitou(127u),
|
||||
tiaosheng(128u),
|
||||
huishou(129u),
|
||||
jidong(130u),
|
||||
jiewu(131u),
|
||||
xianwen(132u),
|
||||
zuotaiji(133u),
|
||||
youtaiji(134u),
|
||||
shuangxi(136u),
|
||||
bianpao(137u),
|
||||
denglong(138u),
|
||||
facai(139u),
|
||||
K_ge(140u),
|
||||
gouwu(141u),
|
||||
youjian(142u),
|
||||
shuai_qi(143u),
|
||||
hecai(144u),
|
||||
qidao(145u),
|
||||
baojin(146u),
|
||||
bangbangtang(147u),
|
||||
he_nai(148u),
|
||||
xiamian(149u),
|
||||
xiangjiao(150u),
|
||||
feiji(151u),
|
||||
kaiche(152u),
|
||||
gaotiezuochetou(153u),
|
||||
chexiang(154u),
|
||||
gaotieyouchetou(155u),
|
||||
duoyun(156u),
|
||||
xiayu(157u),
|
||||
chaopiao(158u),
|
||||
xiongmao(159u),
|
||||
dengpao(160u),
|
||||
fengche(161u),
|
||||
naozhong(162u),
|
||||
dasan(163u),
|
||||
caiqiu(164u),
|
||||
zuanjie(165u),
|
||||
shafa(166u),
|
||||
zhijin(167u),
|
||||
yao(168u),
|
||||
shouqiang(169u),
|
||||
qingwa(170u);
|
||||
|
||||
override fun toString(): String {
|
||||
return "$name($id)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun ofId(id: UByte): FaceID {
|
||||
for (value in values()) {
|
||||
if (value.id == id) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return unknown
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -13,12 +13,12 @@ import net.mamoe.mirai.contact.QQ
|
||||
* 这与使用 [String] 的使用非常类似.
|
||||
*
|
||||
* 比较 [Message] 与 [String] (使用 infix [Message.eq]):
|
||||
* `if(message eq "你好") qq.sendMessage(message)`
|
||||
* `if(event eq "你好") qq.sendMessage(event)`
|
||||
*
|
||||
* 连接 [Message] 与 [Message], [String], (使用 operator [Message.plus]):
|
||||
* ```
|
||||
* message = PlainText("Hello ")
|
||||
* qq.sendMessage(message + "world")
|
||||
* event = PlainText("Hello ")
|
||||
* qq.sendMessage(event + "world")
|
||||
* ```
|
||||
*
|
||||
* 但注意: 不能 `String + Message`. 只能 `Message + String`
|
@ -2,11 +2,9 @@
|
||||
|
||||
package net.mamoe.mirai.message
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
@Suppress("unused")
|
||||
enum class MessageType(private val value: UByte) {
|
||||
enum class MessageType(val value: UByte) {
|
||||
PLAIN_TEXT(0x03u),
|
||||
AT(0x06u),
|
||||
FACE(0x02u),
|
@ -9,8 +9,4 @@ fun String.toMessage(): PlainText = PlainText(this)
|
||||
/**
|
||||
* 用 `this` 构造 [MessageChain]
|
||||
*/
|
||||
fun Message.toChain(): MessageChain = if (this is MessageChain) this else MessageChain(this)
|
||||
|
||||
fun MessageChain.containsType(clazz: Class<out Message>): Boolean = list.any { clazz.isInstance(it) }
|
||||
|
||||
operator fun MessageChain.contains(sub: Class<out Message>): Boolean = containsType(sub)
|
||||
fun Message.toChain(): MessageChain = if (this is MessageChain) this else MessageChain(this)
|
@ -0,0 +1,172 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.message.internal
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.message.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
internal fun ByteArray.parseMessageFace(): Face = read {
|
||||
//00 01 AF 0B 00 08 00 01 00 04 52 CC F5 D0 FF 00 02 14 F0
|
||||
//00 01 0C 0B 00 08 00 01 00 04 52 CC F5 D0 FF 00 02 14 4D
|
||||
discardExact(1)
|
||||
|
||||
val id1 = FaceID.ofId(readLVNumber().toInt().toUByte())//可能这个是id, 也可能下面那个
|
||||
discardExact(readByte().toLong())
|
||||
readLVNumber()//某id?
|
||||
return@read Face(id1)
|
||||
}
|
||||
|
||||
internal fun ByteArray.parsePlainText(): PlainText = read {
|
||||
discardExact(1)
|
||||
PlainText(readLVString())
|
||||
}
|
||||
|
||||
internal fun ByteArray.parseMessageImage0x06(): Image = read {
|
||||
discardExact(1)
|
||||
MiraiLogger.logDebug("好友的图片")
|
||||
MiraiLogger.logDebug(this@parseMessageImage0x06.toUHexString())
|
||||
val filenameLength = readShort()
|
||||
val suffix = readString(filenameLength).substringAfter(".")
|
||||
discardExact(this@parseMessageImage0x06.size - 37 - 1 - filenameLength - 2)
|
||||
val imageId = readString(36)
|
||||
MiraiLogger.logDebug(imageId)
|
||||
discardExact(1)//0x41
|
||||
return@read Image("{$imageId}.$suffix")
|
||||
}
|
||||
|
||||
internal fun ByteArray.parseMessageImage0x03(): Image = read {
|
||||
discardExact(1)
|
||||
return@read Image(String(readLVByteArray()))
|
||||
/*
|
||||
println(String(readLVByteArray()))
|
||||
readTLVMap()
|
||||
return Image(String(readLVByteArray().cutTail(5).getRight(42)))
|
||||
/
|
||||
discardExact(data.size - 47)
|
||||
val imageId = String(readBytes(42))
|
||||
discardExact(1)//0x41
|
||||
discardExact(1)//0x42
|
||||
discardExact(1)//0x43
|
||||
discardExact(1)//0x41
|
||||
|
||||
return Image(imageId)*/
|
||||
}
|
||||
|
||||
internal fun ByteArray.parseMessageChain(): MessageChain = read {
|
||||
readMessageChain()
|
||||
}
|
||||
|
||||
internal fun ByteReadPacket.readMessage(): Message? {
|
||||
val messageType = this.readByte().toInt()
|
||||
val sectionLength = this.readShort().toLong()//sectionLength: short
|
||||
val sectionData = this.readBytes(sectionLength.toInt())//use buffer instead
|
||||
return when (messageType) {
|
||||
0x01 -> sectionData.parsePlainText()
|
||||
0x02 -> sectionData.parseMessageFace()
|
||||
0x03 -> sectionData.parseMessageImage0x03()
|
||||
0x06 -> sectionData.parseMessageImage0x06()
|
||||
|
||||
|
||||
0x19 -> {//长文本
|
||||
val value = readLVByteArray()
|
||||
//todo 未知压缩算法
|
||||
PlainText(String(value))
|
||||
|
||||
// PlainText(String(GZip.uncompress( value)))
|
||||
}
|
||||
|
||||
|
||||
0x14 -> {//长文本
|
||||
val value = readLVByteArray()
|
||||
println(value.size)
|
||||
println(value.toUHexString())
|
||||
//todo 未知压缩算法
|
||||
this.discardExact(7)//几个TLV
|
||||
return PlainText(String(value))
|
||||
}
|
||||
|
||||
0x0E -> {
|
||||
//null
|
||||
null
|
||||
}
|
||||
|
||||
else -> {
|
||||
println("未知的messageType=0x${messageType.toByte().toUHexString()}")
|
||||
println("后文=${this.readBytes().toUHexString()}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun ByteReadPacket.readMessageChain(): MessageChain {
|
||||
val chain = MessageChain()
|
||||
var got: Message? = null
|
||||
do {
|
||||
if (got != null) {
|
||||
chain.concat(got)
|
||||
}
|
||||
if (this.remaining == 0L) {
|
||||
return chain
|
||||
}
|
||||
got = this.readMessage()
|
||||
} while (got != null)
|
||||
return chain
|
||||
}
|
||||
|
||||
fun MessageChain.toPacket(): ByteReadPacket = buildPacket {
|
||||
this@toPacket.list.forEach { message ->
|
||||
writePacket(with(message) {
|
||||
when (this) {
|
||||
is Face -> buildPacket {
|
||||
writeUByte(MessageType.FACE.value)
|
||||
|
||||
writeLVPacket {
|
||||
writeShort(1)
|
||||
writeUByte(id.id)
|
||||
|
||||
writeHex("0B 00 08 00 01 00 04 52 CC F5 D0 FF")
|
||||
|
||||
writeShort(2)
|
||||
writeByte(0x14)//??
|
||||
writeUByte((id.id + 65u).toUByte())
|
||||
}
|
||||
}
|
||||
|
||||
is At -> throw UnsupportedOperationException("At is not supported now but is expecting to be supported")
|
||||
|
||||
is Image -> buildPacket {
|
||||
writeUByte(MessageType.IMAGE.value)
|
||||
|
||||
writeLVPacket {
|
||||
writeByte(0x02)
|
||||
writeLVString(imageId)
|
||||
writeHex("04 00 " +
|
||||
"04 9B 53 B0 08 " +
|
||||
"05 00 " +
|
||||
"04 D9 8A 5A 70 " +
|
||||
"06 00 " +
|
||||
"04 00 00 00 50 " +
|
||||
"07 00 " +
|
||||
"01 43 08 00 00 09 00 01 01 0B 00 00 14 00 04 11 00 00 00 15 00 04 00 00 02 BC 16 00 04 00 00 02 BC 18 00 04 00 00 7D 5E FF 00 5C 15 36 20 39 32 6B 41 31 43 39 62 35 33 62 30 30 38 64 39 38 61 35 61 37 30 20")
|
||||
writeHex("20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20")
|
||||
writeStringUtf8(imageId)
|
||||
writeByte(0x41)
|
||||
}
|
||||
}
|
||||
|
||||
is PlainText -> buildPacket {
|
||||
writeUByte(MessageType.PLAIN_TEXT.value)
|
||||
|
||||
writeLVPacket {
|
||||
writeByte(0x01)
|
||||
writeLVString(stringValue)
|
||||
}
|
||||
}
|
||||
|
||||
else -> throw UnsupportedOperationException("${this::class.simpleName} is not supported")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package net.mamoe.mirai.network
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.io.core.Closeable
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler.BotSocket
|
||||
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.*
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.ClientSKeyRefreshmentRequestPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
|
||||
import net.mamoe.mirai.utils.LoginConfiguration
|
||||
import net.mamoe.mirai.utils.MiraiDatagramChannel
|
||||
|
||||
/**
|
||||
* Mirai 的网络处理器, 它承担所有数据包([Packet])的处理任务.
|
||||
* [BotNetworkHandler] 是全异步和线程安全的.
|
||||
*
|
||||
* [BotNetworkHandler] 由 2 个模块构成:
|
||||
* - [BotSocket]: 处理数据包底层的发送([ByteArray])
|
||||
* - [PacketHandler]: 制作 [ClientPacket] 并传递给 [BotSocket] 发送; 分析 [ServerPacket] 并处理
|
||||
*
|
||||
* 其中, [PacketHandler] 由 3 个子模块构成:
|
||||
* - [LoginHandler] 处理 sendTouch/login/verification code 相关
|
||||
* - [EventPacketHandler] 处理消息相关(群消息/好友消息)([ServerEventPacket])
|
||||
* - [ActionPacketHandler] 处理动作相关(踢人/加入群/好友列表等)
|
||||
*
|
||||
* A BotNetworkHandler is used to connect with Tencent servers.
|
||||
*/
|
||||
interface BotNetworkHandler<Socket : DataPacketSocket> : Closeable {
|
||||
/**
|
||||
* [BotNetworkHandler] 的协程作用域.
|
||||
* 所有 [BotNetworkHandler] 的协程均启动在此作用域下.
|
||||
*
|
||||
* [BotNetworkHandler] 的协程包含:
|
||||
* - UDP 包接收: [MiraiDatagramChannel.read]
|
||||
* - 心跳 Job [ClientHeartbeatPacket]
|
||||
* - SKey 刷新 [ClientSKeyRefreshmentRequestPacket]
|
||||
* - 所有数据包处理和发送
|
||||
*
|
||||
* [BotNetworkHandler.close] 时将会 [取消][CoroutineScope.cancel] 所有此作用域下的协程
|
||||
*/
|
||||
val NetworkScope: CoroutineScope
|
||||
|
||||
var socket: Socket
|
||||
|
||||
/**
|
||||
* 事件处理. 如发送好友消息, 接受群消息并触发事件
|
||||
*/
|
||||
val event: EventPacketHandler
|
||||
|
||||
/**
|
||||
* 动作处理. 如发送好友请求, 处理别人发来的好友请求等
|
||||
*/
|
||||
val action: ActionPacketHandler
|
||||
|
||||
/**
|
||||
* [PacketHandler] 列表
|
||||
*/
|
||||
val packetHandlers: PacketHandlerList
|
||||
|
||||
/**
|
||||
* 尝试登录. 将会依次尝试登录到可用的服务器. 在任一服务器登录完成后返回登录结果
|
||||
*/
|
||||
suspend fun login(configuration: LoginConfiguration): LoginResult
|
||||
|
||||
/**
|
||||
* 添加一个临时包处理器
|
||||
*
|
||||
* @see [TemporaryPacketHandler]
|
||||
*/
|
||||
suspend fun addHandler(temporaryPacketHandler: TemporaryPacketHandler<*>)
|
||||
|
||||
/**
|
||||
* 发送数据包
|
||||
*/
|
||||
suspend fun sendPacket(packet: ClientPacket)
|
||||
|
||||
override fun close() {
|
||||
NetworkScope.cancel("handler closed", HandlerClosedException())
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package net.mamoe.mirai.network
|
||||
|
||||
class HandlerClosedException : Exception()
|
@ -1,12 +1,14 @@
|
||||
package net.mamoe.mirai.network
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.protocol.tim.handler.DataPacketSocket
|
||||
import net.mamoe.mirai.network.protocol.tim.handler.TemporaryPacketHandler
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.utils.getGTK
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
|
||||
/**
|
||||
* 登录会话. 当登录完成后, 客户端会拿到 sessionKey.
|
||||
@ -16,8 +18,9 @@ import net.mamoe.mirai.utils.getGTK
|
||||
*/
|
||||
class LoginSession(
|
||||
val bot: Bot,
|
||||
val sessionKey: ByteArray,
|
||||
val socket: DataPacketSocket
|
||||
val sessionKey: ByteArray,//TODO 协议抽象? 可能并不是所有协议均需要 sessionKey
|
||||
val socket: DataPacketSocket,
|
||||
val scope: CoroutineScope
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -28,6 +31,7 @@ class LoginSession(
|
||||
/**
|
||||
* Web api 使用
|
||||
*/
|
||||
@ExperimentalStdlibApi
|
||||
var sKey: String = ""
|
||||
set(value) {
|
||||
field = value
|
||||
@ -39,6 +43,7 @@ class LoginSession(
|
||||
*/
|
||||
var gtk: Int = 0
|
||||
|
||||
val isOpen: Boolean get() = socket.isOpen
|
||||
|
||||
/**
|
||||
* 发送一个数据包, 并期待接受一个特定的 [ServerPacket].
|
||||
@ -56,7 +61,7 @@ class LoginSession(
|
||||
* @param P 期待的包
|
||||
* @param handlerTemporary 处理器.
|
||||
*/
|
||||
@JvmSynthetic
|
||||
//@JvmSynthetic
|
||||
suspend inline fun <reified P : ServerPacket> expectPacket(handlerTemporary: TemporaryPacketHandler<P>.() -> Unit): CompletableDeferred<Unit> {
|
||||
val deferred = CompletableDeferred<Unit>()
|
||||
this.bot.network.addHandler(TemporaryPacketHandler(P::class, deferred, this).also(handlerTemporary))
|
||||
@ -88,4 +93,7 @@ class LoginSession(
|
||||
})
|
||||
return deferred
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun LoginSession.distributePacket(packet: ServerPacket) = this.socket.distributePacket(packet)
|
@ -0,0 +1,421 @@
|
||||
package net.mamoe.mirai.network.protocol.tim
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.*
|
||||
import net.mamoe.mirai.event.EventScope
|
||||
import net.mamoe.mirai.event.ListeningStatus
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.BeforePacketSendEvent
|
||||
import net.mamoe.mirai.event.events.BotLoginSucceedEvent
|
||||
import net.mamoe.mirai.event.events.PacketSentEvent
|
||||
import net.mamoe.mirai.event.events.ServerPacketReceivedEvent
|
||||
import net.mamoe.mirai.event.subscribe
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.LoginSession
|
||||
import net.mamoe.mirai.network.protocol.tim.handler.*
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
|
||||
/**
|
||||
* [BotNetworkHandler] 的 TIM PC 协议实现
|
||||
*
|
||||
* @see BotNetworkHandler
|
||||
*/
|
||||
internal class TIMBotNetworkHandler(private val bot: Bot) : BotNetworkHandler<TIMBotNetworkHandler.BotSocket> {
|
||||
override val NetworkScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
override lateinit var socket: BotSocket
|
||||
|
||||
override lateinit var event: EventPacketHandler
|
||||
|
||||
override lateinit var action: ActionPacketHandler
|
||||
|
||||
override val packetHandlers: PacketHandlerList = PacketHandlerList()
|
||||
|
||||
internal val temporaryPacketHandlers = mutableListOf<TemporaryPacketHandler<*>>()
|
||||
|
||||
private var heartbeatJob: Job? = null
|
||||
|
||||
override suspend fun addHandler(temporaryPacketHandler: TemporaryPacketHandler<*>) {
|
||||
temporaryPacketHandlers.add(temporaryPacketHandler)
|
||||
temporaryPacketHandler.send(action.session)
|
||||
}
|
||||
|
||||
override suspend fun login(configuration: LoginConfiguration): LoginResult {
|
||||
TIMProtocol.SERVER_IP.forEach {
|
||||
bot.logger.logInfo("Connecting server $it")
|
||||
this.socket = BotSocket(it, configuration)
|
||||
|
||||
loginResult = CompletableDeferred()
|
||||
|
||||
val state = socket.resendTouch()
|
||||
|
||||
if (state != LoginResult.TIMEOUT) {
|
||||
return state
|
||||
}
|
||||
bot.logger.logPurple("Timeout. Retrying next server")
|
||||
|
||||
socket.close()
|
||||
}
|
||||
return LoginResult.TIMEOUT
|
||||
}
|
||||
|
||||
internal var loginResult: CompletableDeferred<LoginResult> = CompletableDeferred()
|
||||
|
||||
|
||||
//private | internal
|
||||
private fun onLoggedIn(sessionKey: ByteArray) {
|
||||
require(packetHandlers.size == 0) { "Already logged in" }
|
||||
val session = LoginSession(bot, sessionKey, socket, NetworkScope)
|
||||
event = EventPacketHandler(session)
|
||||
action = ActionPacketHandler(session)
|
||||
|
||||
packetHandlers.add(event.asNode())
|
||||
packetHandlers.add(action.asNode())
|
||||
}
|
||||
|
||||
private lateinit var sessionKey: ByteArray
|
||||
|
||||
override fun close() {
|
||||
super.close()
|
||||
|
||||
this.heartbeatJob?.cancel(CancellationException("handler closed"))
|
||||
this.heartbeatJob = null
|
||||
|
||||
if (!this.loginResult.isCompleted && !this.loginResult.isCancelled) {
|
||||
this.loginResult.cancel(CancellationException("socket closed"))
|
||||
}
|
||||
|
||||
this.packetHandlers.forEach {
|
||||
it.instance.close()
|
||||
}
|
||||
|
||||
this.socket.close()
|
||||
}
|
||||
|
||||
override suspend fun sendPacket(packet: ClientPacket) = socket.sendPacket(packet)
|
||||
|
||||
internal inner class BotSocket(override val serverIp: String, val configuration: LoginConfiguration) : DataPacketSocket {
|
||||
override val channel: MiraiDatagramChannel = MiraiDatagramChannel(serverIp, 8000)
|
||||
|
||||
override val isOpen: Boolean get() = channel.isOpen
|
||||
|
||||
private lateinit var loginHandler: LoginHandler
|
||||
|
||||
private suspend fun processReceive() {
|
||||
while (channel.isOpen) {
|
||||
val buffer = IoBuffer.Pool.borrow()
|
||||
|
||||
try {
|
||||
channel.read(buffer)//JVM: withContext(IO)
|
||||
} catch (e: ClosedChannelException) {
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.log()
|
||||
continue
|
||||
}
|
||||
|
||||
if (!buffer.canRead() || buffer.readRemaining == 0) {//size==0
|
||||
buffer.release(IoBuffer.Pool)
|
||||
continue
|
||||
}
|
||||
|
||||
NetworkScope.launch {
|
||||
try {
|
||||
//Ensure the packet is consumed totally so that all buffers are released
|
||||
ByteReadPacket(buffer, IoBuffer.Pool).use {
|
||||
distributePacket(it.parseServerPacket(buffer.readRemaining))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.log()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun resendTouch(): LoginResult {
|
||||
if (::loginHandler.isInitialized) loginHandler.close()
|
||||
|
||||
loginHandler = LoginHandler()
|
||||
|
||||
|
||||
val expect = expectPacket<ServerTouchResponsePacket>()
|
||||
NetworkScope.launch { processReceive() }
|
||||
NetworkScope.launch {
|
||||
if (withTimeoutOrNull(configuration.touchTimeoutMillis) { expect.join() } == null) {
|
||||
loginResult.complete(LoginResult.TIMEOUT)
|
||||
}
|
||||
}
|
||||
sendPacket(ClientTouchPacket(bot.qqNumber, this.serverIp))
|
||||
|
||||
return loginResult.await()
|
||||
}
|
||||
|
||||
private inline fun <reified P : ServerPacket> expectPacket(): CompletableDeferred<P> {
|
||||
val receiving = CompletableDeferred<P>()
|
||||
subscribe<ServerPacketReceivedEvent> {
|
||||
if (it.packet is P && it.bot === bot) {
|
||||
receiving.complete(it.packet)
|
||||
ListeningStatus.STOPPED
|
||||
} else
|
||||
ListeningStatus.LISTENING
|
||||
}
|
||||
return receiving
|
||||
}
|
||||
|
||||
override suspend fun distributePacket(packet: ServerPacket) {
|
||||
try {
|
||||
packet.decode()
|
||||
} catch (e: Exception) {
|
||||
bot.printPacketDebugging(packet)
|
||||
packet.close()
|
||||
throw e
|
||||
}
|
||||
|
||||
packet.use {
|
||||
//coz removeIf is not inline
|
||||
with(temporaryPacketHandlers.iterator()) {
|
||||
while (hasNext()) {
|
||||
if (next().onPacketReceived(action.session, packet)) {
|
||||
remove()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//For logDebug
|
||||
{
|
||||
val name = packet::class.simpleName
|
||||
if (name != null && !name.endsWith("Encrypted") && !name.endsWith("Raw")) {
|
||||
bot.cyan("Packet received: $packet")
|
||||
}
|
||||
}()
|
||||
|
||||
if (packet is ServerEventPacket) {
|
||||
//no need to sync acknowledgement packets
|
||||
NetworkScope.launch {
|
||||
sendPacket(packet.ResponsePacket(bot.qqNumber, sessionKey))
|
||||
}
|
||||
}
|
||||
|
||||
if (ServerPacketReceivedEvent(bot, packet).broadcast().cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
loginHandler.onPacketReceived(packet)
|
||||
packetHandlers.forEach {
|
||||
it.instance.onPacketReceived(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendPacket(packet: ClientPacket) = withContext(NetworkScope.coroutineContext) {
|
||||
check(channel.isOpen) { "channel is not open" }
|
||||
|
||||
if (BeforePacketSendEvent(bot, packet).broadcast().cancelled) {
|
||||
return@withContext
|
||||
}
|
||||
|
||||
packet.packet.use { build ->
|
||||
val buffer = IoBuffer.Pool.borrow()
|
||||
try {
|
||||
build.readAvailable(buffer)
|
||||
channel.send(buffer)//JVM: withContext(IO)
|
||||
} catch (e: Exception) {
|
||||
e.log()
|
||||
return@withContext
|
||||
} finally {
|
||||
buffer.release(IoBuffer.Pool)
|
||||
}
|
||||
}
|
||||
|
||||
bot.green("Packet sent: $packet")
|
||||
|
||||
EventScope.launch { PacketSentEvent(bot, packet).broadcast() }
|
||||
}
|
||||
|
||||
override val owner: Bot get() = this@TIMBotNetworkHandler.bot
|
||||
|
||||
override fun close() {
|
||||
if (::loginHandler.isInitialized) loginHandler.close()
|
||||
this.channel.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理登录过程
|
||||
*/
|
||||
inner class LoginHandler {
|
||||
private lateinit var token00BA: ByteArray
|
||||
private lateinit var token0825: ByteArray//56
|
||||
private var loginTime: Int = 0
|
||||
private lateinit var loginIP: String
|
||||
private var privateKey: ByteArray = getRandomByteArray(16)
|
||||
|
||||
/**
|
||||
* 0828_decr_key
|
||||
*/
|
||||
private lateinit var sessionResponseDecryptionKey: IoBuffer
|
||||
|
||||
private var captchaSectionId: Int = 1
|
||||
private var captchaCache: IoBuffer? = null
|
||||
get() {
|
||||
if (field == null) field = IoBuffer.Pool.borrow()
|
||||
return field
|
||||
}
|
||||
set(value) {
|
||||
if (value == null) {
|
||||
field?.release(IoBuffer.Pool)
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
suspend fun onPacketReceived(packet: ServerPacket) {
|
||||
when (packet) {
|
||||
is ServerTouchResponsePacket -> {
|
||||
if (packet.serverIP != null) {//redirection
|
||||
socket.close()
|
||||
socket = BotSocket(packet.serverIP!!, socket.configuration)
|
||||
bot.logger.logPurple("Redirecting to ${packet.serverIP}")
|
||||
loginResult.complete(socket.resendTouch())
|
||||
} else {//password submission
|
||||
this.loginIP = packet.loginIP
|
||||
this.loginTime = packet.loginTime
|
||||
this.token0825 = packet.token0825
|
||||
socket.sendPacket(ClientPasswordSubmissionPacket(bot.qqNumber, bot.account.password, packet.loginTime, packet.loginIP, this.privateKey, packet.token0825, socket.configuration.randomDeviceName))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerLoginResponseFailedPacket -> {
|
||||
loginResult.complete(packet.loginResult)
|
||||
bot.close()
|
||||
return
|
||||
}
|
||||
|
||||
is ServerCaptchaCorrectPacket -> {
|
||||
this.privateKey = getRandomByteArray(16)//似乎是必须的
|
||||
this.token00BA = packet.token00BA
|
||||
|
||||
socket.sendPacket(ClientLoginResendPacket3105(bot.qqNumber, bot.account.password, this.loginTime, this.loginIP, this.privateKey, this.token0825, packet.token00BA, socket.configuration.randomDeviceName))
|
||||
}
|
||||
|
||||
is ServerLoginResponseVerificationCodeInitPacket -> {
|
||||
//[token00BA]来源之一: 验证码
|
||||
this.token00BA = packet.token00BA
|
||||
this.captchaCache = packet.verifyCodePart1
|
||||
|
||||
if (packet.unknownBoolean == true) {
|
||||
this.captchaSectionId = 1
|
||||
socket.sendPacket(ClientVerificationCodeTransmissionRequestPacket(1, bot.qqNumber, this.token0825, this.captchaSectionId++, packet.token00BA))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerCaptchaTransmissionPacket -> {
|
||||
if (packet is ServerCaptchaWrongPacket) {
|
||||
bot.error("验证码错误, 请重新输入")
|
||||
captchaSectionId = 1
|
||||
this.captchaCache = null
|
||||
}
|
||||
|
||||
this.captchaCache!!.writeFully(packet.captchaSectionN)
|
||||
this.token00BA = packet.token00BA
|
||||
|
||||
if (packet.transmissionCompleted) {
|
||||
val code = solveCaptcha(captchaCache!!)
|
||||
if (code == null) {
|
||||
this.captchaCache = null
|
||||
this.captchaSectionId = 1
|
||||
socket.sendPacket(ClientVerificationCodeRefreshPacket(packet.packetIdLast + 1, bot.qqNumber, token0825))
|
||||
} else {
|
||||
socket.sendPacket(ClientVerificationCodeSubmitPacket(packet.packetIdLast + 1, bot.qqNumber, token0825, code, packet.verificationToken))
|
||||
}
|
||||
} else {
|
||||
socket.sendPacket(ClientVerificationCodeTransmissionRequestPacket(packet.packetIdLast + 1, bot.qqNumber, token0825, captchaSectionId++, packet.token00BA))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerLoginResponseSuccessPacket -> {
|
||||
this.sessionResponseDecryptionKey = packet.sessionResponseDecryptionKey
|
||||
socket.sendPacket(ClientSessionRequestPacket(bot.qqNumber, socket.serverIp, packet.token38, packet.token88, packet.encryptionKey))
|
||||
}
|
||||
|
||||
//是ClientPasswordSubmissionPacket之后服务器回复的
|
||||
is ServerLoginResponseKeyExchangePacket -> {
|
||||
//if (packet.tokenUnknown != null) {
|
||||
//this.token00BA = packet.token00BA!!
|
||||
//println("token00BA changed!!! to " + token00BA.toUByteArray())
|
||||
//}
|
||||
if (packet.flag == ServerLoginResponseKeyExchangePacket.Flag.`08 36 31 03`) {
|
||||
this.privateKey = packet.privateKeyUpdate
|
||||
socket.sendPacket(ClientLoginResendPacket3104(bot.qqNumber, bot.account.password, loginTime, loginIP, privateKey, token0825, packet.tokenUnknown
|
||||
?: token00BA, socket.configuration.randomDeviceName, packet.tlv0006))
|
||||
} else {
|
||||
socket.sendPacket(ClientLoginResendPacket3106(bot.qqNumber, bot.account.password, loginTime, loginIP, privateKey, token0825, packet.tokenUnknown
|
||||
?: token00BA, socket.configuration.randomDeviceName, packet.tlv0006))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerSessionKeyResponsePacket -> {
|
||||
sessionKey = packet.sessionKey
|
||||
|
||||
heartbeatJob = NetworkScope.launch {
|
||||
while (socket.isOpen) {
|
||||
delay(90000)
|
||||
socket.sendPacket(ClientHeartbeatPacket(bot.qqNumber, sessionKey))
|
||||
}
|
||||
}
|
||||
|
||||
loginResult.complete(LoginResult.SUCCESS)
|
||||
|
||||
setOnlineStatus(OnlineStatus.ONLINE)//required
|
||||
}
|
||||
|
||||
is ServerLoginSuccessPacket -> {
|
||||
BotLoginSucceedEvent(bot).broadcast()
|
||||
|
||||
//登录成功后会收到大量上次的消息, 忽略掉 todo 优化
|
||||
NetworkScope.launch {
|
||||
delay(3000)
|
||||
event.ignoreMessage = false
|
||||
}
|
||||
|
||||
onLoggedIn(sessionKey)
|
||||
|
||||
this.close()//The LoginHandler is useless since then
|
||||
}
|
||||
|
||||
|
||||
is ServerCaptchaPacket.Encrypted -> socket.distributePacket(packet.decrypt())
|
||||
is ServerLoginResponseVerificationCodeInitPacket.Encrypted -> socket.distributePacket(packet.decrypt())
|
||||
is ServerLoginResponseKeyExchangePacket.Encrypted -> socket.distributePacket(packet.decrypt(this.privateKey))
|
||||
is ServerLoginResponseSuccessPacket.Encrypted -> socket.distributePacket(packet.decrypt(this.privateKey))
|
||||
is ServerSessionKeyResponsePacket.Encrypted -> socket.distributePacket(packet.decrypt(this.sessionResponseDecryptionKey))
|
||||
is ServerTouchResponsePacket.Encrypted -> socket.distributePacket(packet.decrypt())
|
||||
|
||||
|
||||
is ServerHeartbeatResponsePacket -> {
|
||||
|
||||
}
|
||||
|
||||
is UnknownServerPacket.Encrypted -> socket.distributePacket(packet.decrypt(sessionKey))
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
suspend fun setOnlineStatus(status: OnlineStatus) {
|
||||
socket.sendPacket(ClientChangeOnlineStatusPacket(bot.qqNumber, sessionKey, status))
|
||||
}
|
||||
|
||||
fun close() {
|
||||
this.captchaCache = null
|
||||
|
||||
if (::sessionResponseDecryptionKey.isInitialized) this.sessionResponseDecryptionKey.release(IoBuffer.Pool)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,17 +2,9 @@
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim
|
||||
|
||||
import net.mamoe.mirai.utils.TEA
|
||||
import net.mamoe.mirai.utils.hexToBytes
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
import java.net.InetAddress
|
||||
import java.util.*
|
||||
import java.util.stream.Collectors
|
||||
import net.mamoe.mirai.utils.solveIpAddress
|
||||
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
object TIMProtocol {
|
||||
val SERVER_IP: List<String> = {
|
||||
//add("183.60.56.29")
|
||||
@ -25,7 +17,7 @@ object TIMProtocol {
|
||||
"sz8.tencent.com",
|
||||
"sz9.tencent.com",
|
||||
"sz2.tencent.com"
|
||||
).forEach { list.add(InetAddress.getByName(it).hostAddress) }
|
||||
).forEach { list.add(solveIpAddress(it)) }
|
||||
|
||||
list.toList()
|
||||
}()
|
||||
@ -45,7 +37,7 @@ object TIMProtocol {
|
||||
const val constantData2 = "00 00 04 53 00 00 00 01 00 00 15 85 "
|
||||
|
||||
/**
|
||||
* Touch 发出时写入, 并用于加密, 接受 touch response 时解密.
|
||||
* Touch 发出时写入, 并用于加密, 接受 sendTouch response 时解密.
|
||||
*/
|
||||
const val touchKey = "A4 F1 91 88 C9 82 14 99 0C 9E 56 55 91 23 C8 3D"//16
|
||||
|
||||
@ -66,11 +58,6 @@ object TIMProtocol {
|
||||
*/
|
||||
const val key0836 = "EF 4A 36 6A 16 A8 E6 3D 2E EA BD 1F 98 C1 3C DA"//16
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
println(TEA.decrypt(publicKey.hexToBytes(), key0836).toUHexString())
|
||||
}
|
||||
|
||||
/**
|
||||
* 并非常量. 是 publicKey 与 key0836 的算法计算结果
|
||||
*/
|
||||
@ -100,27 +87,4 @@ object TIMProtocol {
|
||||
*/
|
||||
const val messageConst1 = "00 00 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91"
|
||||
// TIM最新 22 00 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91
|
||||
|
||||
private val hexToByteArrayCacheMap: MutableMap<Int, ByteArray> = mutableMapOf()
|
||||
|
||||
|
||||
fun hexToBytes(hex: String): ByteArray {
|
||||
hex.hashCode().let { id ->
|
||||
if (hexToByteArrayCacheMap.containsKey(id)) {
|
||||
return hexToByteArrayCacheMap[id]!!.clone()
|
||||
} else {
|
||||
hexToUBytes(hex).toByteArray().let {
|
||||
hexToByteArrayCacheMap[id] = it.clone()
|
||||
return it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun hexToUBytes(hex: String): UByteArray = Arrays
|
||||
.stream(hex.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray())
|
||||
.map { value -> value.trim { it <= ' ' } }
|
||||
.map { s -> s.toUByte(16) }
|
||||
.collect(Collectors.toList()).toUByteArray()
|
||||
}
|
@ -1,29 +1,20 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.handler
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import net.mamoe.mirai.network.LoginSession
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientAccountInfoRequestPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerAccountInfoResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerEventPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.AddFriendResult
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ClientAddFriendPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ClientCanAddFriendPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ServerCanAddFriendResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.image.ServerTryGetImageIDFailedPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.image.ServerTryGetImageIDResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.image.ServerTryGetImageIDSuccessPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.ClientSKeyRefreshmentRequestPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.ClientSKeyRequestPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.ServerSKeyResponsePacket
|
||||
import net.mamoe.mirai.task.MiraiThreadPool
|
||||
import net.mamoe.mirai.utils.getGTK
|
||||
import java.awt.image.BufferedImage
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Supplier
|
||||
import net.mamoe.mirai.utils.hexToBytes
|
||||
|
||||
/**
|
||||
* 动作: 获取好友列表, 点赞, 踢人等.
|
||||
@ -32,12 +23,13 @@ import java.util.function.Supplier
|
||||
* @author Him188moe
|
||||
*/
|
||||
class ActionPacketHandler(session: LoginSession) : PacketHandler(session) {
|
||||
private val addFriendSessions = Collections.synchronizedCollection(mutableListOf<AddFriendSession>())
|
||||
private val uploadImageSessions = Collections.synchronizedCollection(mutableListOf<UploadImageSession>())
|
||||
private val addFriendSessions = mutableListOf<AddFriendSession>()
|
||||
private val uploadImageSessions = mutableListOf<UploadImageSession>()
|
||||
|
||||
private var sKeyRefresherFuture: ScheduledFuture<*>? = null
|
||||
private var sKeyRefresherJob: Job? = null
|
||||
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
override suspend fun onPacketReceived(packet: ServerPacket) {
|
||||
when (packet) {
|
||||
is ServerCanAddFriendResponsePacket -> {
|
||||
@ -65,11 +57,13 @@ class ActionPacketHandler(session: LoginSession) : PacketHandler(session) {
|
||||
session.sKey = packet.sKey
|
||||
session.cookies = "uin=o" + session.bot.account.qqNumber + ";skey=" + session.sKey + ";"
|
||||
|
||||
sKeyRefresherFuture = MiraiThreadPool.instance.scheduleWithFixedDelay({
|
||||
runBlocking {
|
||||
|
||||
sKeyRefresherJob = session.scope.launch {
|
||||
while (session.isOpen) {
|
||||
delay(1800000)
|
||||
session.socket.sendPacket(ClientSKeyRefreshmentRequestPacket(session.bot.account.qqNumber, session.sessionKey))
|
||||
}
|
||||
}, 1800000, 1800000, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
|
||||
session.gtk = getGTK(session.sKey)
|
||||
}
|
||||
@ -82,15 +76,9 @@ class ActionPacketHandler(session: LoginSession) : PacketHandler(session) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun addFriend(qqNumber: Long, message: Supplier<String>) {
|
||||
addFriend(qqNumber, lazy { message.get() })
|
||||
}
|
||||
|
||||
|
||||
@JvmSynthetic
|
||||
suspend fun addFriend(qqNumber: Long, message: Lazy<String> = lazyOf("")): CompletableFuture<AddFriendResult> {
|
||||
val future = CompletableFuture<AddFriendResult>()
|
||||
//@JvmSynthetic
|
||||
suspend fun addFriend(qqNumber: Long, message: Lazy<String> = lazyOf("")): CompletableDeferred<AddFriendResult> {
|
||||
val future = CompletableDeferred<AddFriendResult>()
|
||||
val session = AddFriendSession(qqNumber, future, message)
|
||||
// uploadImageSessions.add(session)
|
||||
session.sendAddRequest()
|
||||
@ -108,14 +96,14 @@ class ActionPacketHandler(session: LoginSession) : PacketHandler(session) {
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
this.sKeyRefresherFuture?.cancel(true)
|
||||
this.sKeyRefresherFuture = null
|
||||
this.sKeyRefresherJob?.cancel()
|
||||
this.sKeyRefresherJob = null
|
||||
}
|
||||
|
||||
private inner class UploadImageSession(
|
||||
private val group: Long,
|
||||
private val future: CompletableFuture<AddFriendResult>,
|
||||
private val image: BufferedImage
|
||||
private val future: CompletableDeferred<AddFriendResult>
|
||||
//private val image: BufferedImage
|
||||
) {
|
||||
lateinit var id: ByteArray
|
||||
|
||||
@ -167,7 +155,7 @@ class ActionPacketHandler(session: LoginSession) : PacketHandler(session) {
|
||||
|
||||
private inner class AddFriendSession(
|
||||
private val qq: Long,
|
||||
private val future: CompletableFuture<AddFriendResult>,
|
||||
private val future: CompletableDeferred<AddFriendResult>,
|
||||
private val message: Lazy<String>
|
||||
) {
|
||||
lateinit var id: ByteArray
|
||||
@ -180,7 +168,7 @@ class ActionPacketHandler(session: LoginSession) : PacketHandler(session) {
|
||||
|
||||
when (packet) {
|
||||
is ServerCanAddFriendResponsePacket -> {
|
||||
if (!(packet.idByteArray[2] == id[0] && packet.idByteArray[3] == id[1])) {
|
||||
if (!(packet.idByteArray.contentEquals(id))) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -211,7 +199,7 @@ class ActionPacketHandler(session: LoginSession) : PacketHandler(session) {
|
||||
|
||||
|
||||
suspend fun sendAddRequest() {
|
||||
session.socket.sendPacket(ClientCanAddFriendPacket(session.bot.account.qqNumber, qq, session.sessionKey).also { this.id = it.packetIdLast })
|
||||
session.socket.sendPacket(ClientCanAddFriendPacket(session.bot.account.qqNumber, qq, session.sessionKey).also { this.id = it.idHex.hexToBytes() })
|
||||
}
|
||||
|
||||
fun close() {
|
@ -1,11 +1,13 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.handler
|
||||
|
||||
import kotlinx.io.core.Closeable
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.event.events.ServerPacketReceivedEvent
|
||||
import net.mamoe.mirai.network.LoginSession
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.utils.MiraiDatagramChannel
|
||||
|
||||
/**
|
||||
* 网络接口.
|
||||
@ -14,9 +16,21 @@ import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
interface DataPacketSocket {
|
||||
interface DataPacketSocket : Closeable {
|
||||
val owner: Bot
|
||||
|
||||
val serverIp: String
|
||||
|
||||
/**
|
||||
* UDP 通道
|
||||
*/
|
||||
val channel: MiraiDatagramChannel
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
val isOpen: Boolean
|
||||
|
||||
/**
|
||||
* 分发数据包给 [PacketHandler]
|
||||
*/
|
||||
@ -31,7 +45,5 @@ interface DataPacketSocket {
|
||||
*/
|
||||
suspend fun sendPacket(packet: ClientPacket)
|
||||
|
||||
fun isClosed(): Boolean
|
||||
|
||||
fun close()
|
||||
override fun close()
|
||||
}
|
@ -9,14 +9,13 @@ import net.mamoe.mirai.getGroupByNumber
|
||||
import net.mamoe.mirai.getQQ
|
||||
import net.mamoe.mirai.message.MessageChain
|
||||
import net.mamoe.mirai.network.LoginSession
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerFriendMessageEventPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerGroupMessageEventPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerGroupUploadFileEventPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.distributePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ClientSendFriendMessagePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ClientSendGroupMessagePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ServerSendFriendMessageResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ServerSendGroupMessageResponsePacket
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
|
||||
/**
|
||||
* 处理消息事件, 承担消息发送任务.
|
||||
@ -24,10 +23,10 @@ import net.mamoe.mirai.network.protocol.tim.packet.action.ServerSendGroupMessage
|
||||
* @author Him188moe
|
||||
*/
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
class MessagePacketHandler(session: LoginSession) : PacketHandler(session) {
|
||||
class EventPacketHandler(session: LoginSession) : PacketHandler(session) {
|
||||
internal var ignoreMessage: Boolean = true
|
||||
|
||||
override suspend fun onPacketReceived(packet: ServerPacket) {
|
||||
override suspend fun onPacketReceived(packet: ServerPacket): Unit = with(session) {
|
||||
when (packet) {
|
||||
is ServerGroupUploadFileEventPacket -> {
|
||||
//todo
|
||||
@ -36,21 +35,28 @@ class MessagePacketHandler(session: LoginSession) : PacketHandler(session) {
|
||||
is ServerFriendMessageEventPacket -> {
|
||||
if (ignoreMessage) return
|
||||
|
||||
FriendMessageEvent(session.bot, session.bot.getQQ(packet.qq), packet.message).broadcast()
|
||||
FriendMessageEvent(bot, bot.getQQ(packet.qq), packet.message).broadcast()
|
||||
}
|
||||
|
||||
is ServerGroupMessageEventPacket -> {
|
||||
if (ignoreMessage) return
|
||||
|
||||
if (packet.qq == session.bot.account.qqNumber) return
|
||||
if (packet.qq.toLong() == bot.account.qqNumber) return
|
||||
|
||||
GroupMessageEvent(session.bot, session.bot.getGroupByNumber(packet.groupNumber), session.bot.getQQ(packet.qq), packet.message).broadcast()
|
||||
GroupMessageEvent(bot, bot.getGroupByNumber(packet.groupNumber), bot.getQQ(packet.qq), packet.message).broadcast()
|
||||
}
|
||||
|
||||
is ServerSendFriendMessageResponsePacket,
|
||||
is ServerSendGroupMessageResponsePacket -> {
|
||||
//ignored
|
||||
}
|
||||
|
||||
is ServerFieldOnlineStatusChangedPacket.Encrypted -> distributePacket(packet.decrypt(sessionKey))
|
||||
is ServerFieldOnlineStatusChangedPacket -> {
|
||||
MiraiLogger.logInfo("${packet.qq.toLong()} 登录状态改变为 ${packet.status}")
|
||||
//TODO
|
||||
}
|
||||
|
||||
else -> {
|
||||
//ignored
|
||||
}
|
@ -2,6 +2,7 @@ package net.mamoe.mirai.network.protocol.tim.handler
|
||||
|
||||
import net.mamoe.mirai.network.LoginSession
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* 数据包(接受/发送)处理器
|
||||
@ -17,17 +18,18 @@ abstract class PacketHandler(
|
||||
}
|
||||
|
||||
class PacketHandlerNode<T : PacketHandler>(
|
||||
val clazz: Class<T>,
|
||||
val clazz: KClass<T>,
|
||||
val instance: T
|
||||
)
|
||||
|
||||
fun PacketHandler.asNode(): PacketHandlerNode<PacketHandler> {
|
||||
return PacketHandlerNode(this.javaClass, this)
|
||||
fun <T : PacketHandler> T.asNode(): PacketHandlerNode<T> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return PacketHandlerNode(this::class as KClass<T>, this)
|
||||
}
|
||||
|
||||
class PacketHandlerList : MutableList<PacketHandlerNode<*>> by mutableListOf() {
|
||||
|
||||
fun <T : PacketHandler> get(clazz: Class<T>): T {
|
||||
operator fun <T : PacketHandler> get(clazz: KClass<T>): T {
|
||||
this.forEach {
|
||||
if (it.clazz == clazz) {
|
||||
@Suppress("UNCHECKED_CAST")
|
@ -4,7 +4,6 @@ import kotlinx.coroutines.CompletableDeferred
|
||||
import net.mamoe.mirai.network.LoginSession
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
@ -20,7 +19,7 @@ import kotlin.reflect.KClass
|
||||
*
|
||||
* @see LoginSession.expectPacket
|
||||
*/
|
||||
open class TemporaryPacketHandler<P : ServerPacket>(
|
||||
class TemporaryPacketHandler<P : ServerPacket>(
|
||||
private val expectationClass: KClass<P>,
|
||||
private val deferred: CompletableDeferred<Unit>,
|
||||
private val fromSession: LoginSession
|
@ -0,0 +1,46 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.utils.writeHex
|
||||
|
||||
//TODO 将序列 ID 从包 ID 中独立出来
|
||||
abstract class ClientPacket : Packet(), Closeable {
|
||||
/**
|
||||
* Encode this packet.
|
||||
*
|
||||
* Before sending the packet, a [tail][TIMProtocol.tail] is added.
|
||||
*/
|
||||
protected abstract fun encode(builder: BytePacketBuilder)
|
||||
|
||||
companion object {
|
||||
@Suppress("PrivatePropertyName")
|
||||
private val UninitializedByteReadPacket = ByteReadPacket(IoBuffer.Empty, IoBuffer.EmptyPool)
|
||||
}
|
||||
|
||||
/**
|
||||
* 务必 [ByteReadPacket.close] 或 [close] 或使用 [Closeable.use]
|
||||
*/
|
||||
var packet: ByteReadPacket = UninitializedByteReadPacket
|
||||
get() {
|
||||
if (field === UninitializedByteReadPacket) build()
|
||||
return field
|
||||
}
|
||||
|
||||
private fun build(): ByteReadPacket {
|
||||
packet = buildPacket {
|
||||
writeHex(TIMProtocol.head)
|
||||
writeHex(TIMProtocol.ver)
|
||||
writeHex(idHex)
|
||||
encode(this)
|
||||
writeHex(TIMProtocol.tail)
|
||||
}
|
||||
return packet
|
||||
}
|
||||
|
||||
override fun toString(): String = packetToString()
|
||||
|
||||
override fun close() = if (this.packet === UninitializedByteReadPacket) Unit else this.packet.close()
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readUByte
|
||||
import kotlinx.io.core.readUInt
|
||||
import net.mamoe.mirai.utils.OnlineStatus
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
||||
/**
|
||||
* 好友在线状态改变
|
||||
*/
|
||||
@PacketId("00 81")
|
||||
class ServerFieldOnlineStatusChangedPacket(input: ByteReadPacket) : ServerPacket(input) {
|
||||
var qq: UInt by Delegates.notNull()
|
||||
lateinit var status: OnlineStatus
|
||||
|
||||
override fun decode() = with(input) {
|
||||
qq = readUInt()
|
||||
discardExact(8)
|
||||
status = OnlineStatus.ofId(readUByte())
|
||||
}
|
||||
|
||||
//在线 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
|
||||
|
||||
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
|
||||
fun decrypt(sessionKey: ByteArray): ServerFieldOnlineStatusChangedPacket = ServerFieldOnlineStatusChangedPacket(this.decryptBy(sessionKey)).setId(this.idHex)
|
||||
}
|
||||
}
|
@ -1,7 +1,15 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.writeUByte
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import java.io.DataInputStream
|
||||
import net.mamoe.mirai.utils.encryptAndWrite
|
||||
import net.mamoe.mirai.utils.writeHex
|
||||
import net.mamoe.mirai.utils.writeQQ
|
||||
import net.mamoe.mirai.utils.writeRandom
|
||||
|
||||
/**
|
||||
* 获取升级天数等.
|
||||
@ -14,13 +22,13 @@ class ClientAccountInfoRequestPacket(
|
||||
private val qq: Long,
|
||||
private val sessionKey: ByteArray
|
||||
) : ClientPacket() {
|
||||
override fun encode() {
|
||||
this.writeRandom(2)//part of packet id
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeRandom(2)
|
||||
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(TIMProtocol.fixVer2)
|
||||
this.encryptAndWrite(sessionKey) {
|
||||
writeByte(0x88)
|
||||
writeUByte(0x88.toUByte())
|
||||
writeQQ(qq)
|
||||
writeByte(0x00)
|
||||
}
|
||||
@ -28,7 +36,7 @@ class ClientAccountInfoRequestPacket(
|
||||
}
|
||||
|
||||
@PacketId("00 5C")
|
||||
class ServerAccountInfoResponsePacket(input: DataInputStream) : ServerPacket(input) {
|
||||
class ServerAccountInfoResponsePacket(input: ByteReadPacket) : ServerPacket(input) {
|
||||
//等级
|
||||
//升级剩余活跃天数
|
||||
//ignored
|
||||
@ -37,7 +45,7 @@ class ServerAccountInfoResponsePacket(input: DataInputStream) : ServerPacket(inp
|
||||
}
|
||||
|
||||
@PacketId("00 5C")
|
||||
class Encrypted(inputStream: DataInputStream) : ServerPacket(inputStream) {
|
||||
class Encrypted(inputStream: ByteReadPacket) : ServerPacket(inputStream) {
|
||||
fun decrypt(sessionKey: ByteArray): ServerAccountInfoResponsePacket = ServerAccountInfoResponsePacket(this.decryptBy(sessionKey)).setId(this.idHex)
|
||||
}
|
||||
}
|
@ -1,21 +1,21 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import java.io.DataInputStream
|
||||
import java.io.IOException
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
@PacketId("00 58")
|
||||
class ClientHeartbeatPacket(
|
||||
private val qq: Long,
|
||||
private val sessionKey: ByteArray
|
||||
) : ClientPacket() {
|
||||
@Throws(IOException::class)
|
||||
override fun encode() {
|
||||
this.writeRandom(2)
|
||||
override val idHex: String by lazy {
|
||||
super.idHex + " " + getRandomByteArray(2).toUHexString()
|
||||
}
|
||||
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(TIMProtocol.fixVer)
|
||||
this.encryptAndWrite(sessionKey) {
|
||||
@ -24,4 +24,4 @@ class ClientHeartbeatPacket(
|
||||
}
|
||||
}
|
||||
|
||||
class ServerHeartbeatResponsePacket(input: DataInputStream) : ServerPacket(input)
|
||||
class ServerHeartbeatResponsePacket(input: ByteReadPacket) : ServerPacket(input)
|
@ -0,0 +1,28 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import net.mamoe.mirai.utils.hexToUBytes
|
||||
|
||||
|
||||
abstract class Packet {
|
||||
open val idHex: String by lazy {
|
||||
this::class.annotations.filterIsInstance<PacketId>().firstOrNull()?.value?.trim() ?: ""
|
||||
}
|
||||
|
||||
open val fixedId: String by lazy {
|
||||
when (this.idHex.length) {
|
||||
0 -> "__ __ __ __"
|
||||
2 -> this.idHex + " __ __ __"
|
||||
5 -> this.idHex + " __ __"
|
||||
7 -> this.idHex + " __"
|
||||
else -> this.idHex
|
||||
}
|
||||
}
|
||||
|
||||
open val idByteArray: ByteArray by lazy {
|
||||
idHex.hexToUBytes().toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
internal expect fun Packet.packetToString(): String
|
@ -1,8 +1,6 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE)
|
||||
annotation class PacketId(
|
@ -1,144 +1,149 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.message.MessageChain
|
||||
import net.mamoe.mirai.message.internal.readMessageChain
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.utils.dataDecode
|
||||
import net.mamoe.mirai.utils.hexToBytes
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
import net.mamoe.mirai.utils.toUInt
|
||||
import java.io.DataInputStream
|
||||
import net.mamoe.mirai.utils.*
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
data class EventPacketIdentity(
|
||||
val from: UInt,//对于好友消息, 这个是发送人
|
||||
val to: UInt,//对于好友消息, 这个是bot
|
||||
internal val uniqueId: IoBuffer//8
|
||||
) {
|
||||
override fun toString(): String = "EPI(from=$from, to=$to)"
|
||||
}
|
||||
|
||||
fun BytePacketBuilder.writeEventPacketIdentity(identity: EventPacketIdentity) = with(identity) {
|
||||
writeUInt(from)
|
||||
writeUInt(to)
|
||||
writeFully(uniqueId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Packet id: `00 CE` or `00 17`
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
abstract class ServerEventPacket(input: DataInputStream, val packetId: ByteArray, val eventIdentity: ByteArray) : ServerPacket(input) {
|
||||
@PacketId("00 17")
|
||||
class Raw(input: DataInputStream, private val packetId: ByteArray) : ServerPacket(input) {
|
||||
abstract class ServerEventPacket(input: ByteReadPacket, packetId: ByteArray, val eventIdentity: EventPacketIdentity) : ServerPacket(input) {
|
||||
override val idByteArray: ByteArray = packetId
|
||||
override var idHex: String = packetId.toUHexString()
|
||||
|
||||
fun distribute(): ServerEventPacket {
|
||||
val eventIdentity = this.input.readNBytes(16)
|
||||
val type = this.input.goto(18).readNBytes(2)
|
||||
class Raw(input: ByteReadPacket, private val packetId: ByteArray) : ServerPacket(input) {
|
||||
|
||||
fun distribute(): ServerEventPacket = with(input) {
|
||||
val eventIdentity = EventPacketIdentity(
|
||||
from = readUInt(),
|
||||
to = readUInt(),
|
||||
uniqueId = readIoBuffer(8)
|
||||
)
|
||||
readBytes(2).takeIf { it[0].toUInt() != 0x1Fu && it[1].toUInt() != 0x40u }?.debugPrint("type前面2个byte")
|
||||
val type = readBytes(2)
|
||||
return when (type.toUHexString()) {
|
||||
"00 C4" -> {
|
||||
if (this.input.goto(33).readBoolean()) {
|
||||
ServerAndroidOnlineEventPacket(this.input, packetId, eventIdentity)
|
||||
discardExact(13)
|
||||
if (readBoolean()) {
|
||||
ServerAndroidOnlineEventPacket(input, packetId, eventIdentity)
|
||||
} else {
|
||||
ServerAndroidOfflineEventPacket(this.input, packetId, eventIdentity)
|
||||
ServerAndroidOfflineEventPacket(input, packetId, eventIdentity)
|
||||
}
|
||||
}
|
||||
"00 2D" -> ServerGroupUploadFileEventPacket(this.input, packetId, eventIdentity)
|
||||
"00 2D" -> ServerGroupUploadFileEventPacket(input, packetId, eventIdentity)
|
||||
|
||||
"00 52" -> ServerGroupMessageEventPacket(this.input, packetId, eventIdentity)
|
||||
"00 52" -> ServerGroupMessageEventPacket(input, packetId, eventIdentity)
|
||||
|
||||
"00 A6" -> ServerFriendMessageEventPacket(this.input, packetId, eventIdentity)
|
||||
"00 A6" -> ServerFriendMessageEventPacket(input, packetId, eventIdentity)
|
||||
|
||||
//"02 10", "00 12" -> ServerUnknownEventPacket(this.input, packetId, eventIdentity)
|
||||
//"02 10", "00 12" -> ServerUnknownEventPacket(input, packetId, eventIdentity)
|
||||
|
||||
else -> UnknownServerEventPacket(this.input, packetId, eventIdentity)
|
||||
}.setId(this.idHex)
|
||||
else -> UnknownServerEventPacket(input, packetId, eventIdentity)
|
||||
}.setId(idHex)
|
||||
}
|
||||
|
||||
@PacketId("00 17")
|
||||
class Encrypted(input: DataInputStream, private val packetId: ByteArray) : ServerPacket(input) {
|
||||
class Encrypted(input: ByteReadPacket, private val packetId: ByteArray) : ServerPacket(input) {
|
||||
fun decrypt(sessionKey: ByteArray): Raw = Raw(this.decryptBy(sessionKey), packetId).setId(this.idHex)
|
||||
}
|
||||
}
|
||||
|
||||
@PacketId("")
|
||||
inner class ResponsePacket(
|
||||
val qq: Long,
|
||||
val bot: Long,
|
||||
val sessionKey: ByteArray
|
||||
) : ClientPacket() {
|
||||
override fun encode() {
|
||||
this.write(packetId)//packet id 4bytes
|
||||
override val idHex: String = this@ServerEventPacket.idHex
|
||||
override val idByteArray: ByteArray = this@ServerEventPacket.idByteArray
|
||||
override val fixedId: String = idHex
|
||||
|
||||
this.writeQQ(qq)
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(bot)
|
||||
this.writeHex(TIMProtocol.fixVer2)
|
||||
this.encryptAndWrite(sessionKey) {
|
||||
write(eventIdentity)
|
||||
writeEventPacketIdentity(eventIdentity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFixedId(): String {
|
||||
return packetId.toUHexString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unknown event
|
||||
*/
|
||||
class UnknownServerEventPacket(input: DataInputStream, packetId: ByteArray, eventIdentity: ByteArray) : ServerEventPacket(input, packetId, eventIdentity) {
|
||||
class UnknownServerEventPacket(input: ByteReadPacket, packetId: ByteArray, eventIdentity: EventPacketIdentity) : ServerEventPacket(input, packetId, eventIdentity) {
|
||||
override fun decode() {
|
||||
super.decode()
|
||||
println("UnknownServerEventPacket data: " + this.input.goto(0).readAllBytes().toUHexString())
|
||||
println("UnknownServerEventPacket data: " + this.input.readBytes().toUHexString())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Android 客户端上线
|
||||
*/
|
||||
class ServerAndroidOnlineEventPacket(input: DataInputStream, packetId: ByteArray, eventIdentity: ByteArray) : ServerEventPacket(input, packetId, eventIdentity)
|
||||
class ServerAndroidOnlineEventPacket(input: ByteReadPacket, packetId: ByteArray, eventIdentity: EventPacketIdentity) : ServerEventPacket(input, packetId, eventIdentity)
|
||||
|
||||
/**
|
||||
* Android 客户端下线
|
||||
*/
|
||||
class ServerAndroidOfflineEventPacket(input: DataInputStream, packetId: ByteArray, eventIdentity: ByteArray) : ServerEventPacket(input, packetId, eventIdentity)
|
||||
class ServerAndroidOfflineEventPacket(input: ByteReadPacket, packetId: ByteArray, eventIdentity: EventPacketIdentity) : ServerEventPacket(input, packetId, eventIdentity)
|
||||
|
||||
/**
|
||||
* 群文件上传
|
||||
*/
|
||||
class ServerGroupUploadFileEventPacket(input: DataInputStream, packetId: ByteArray, eventIdentity: ByteArray) : ServerEventPacket(input, packetId, eventIdentity) {
|
||||
class ServerGroupUploadFileEventPacket(input: ByteReadPacket, packetId: ByteArray, eventIdentity: EventPacketIdentity) : ServerEventPacket(input, packetId, eventIdentity) {
|
||||
private lateinit var xmlMessage: String
|
||||
|
||||
override fun decode() {
|
||||
xmlMessage = String(this.input.goto(65).readNBytes(this.input.goto(60).readShort().toInt()))
|
||||
this.input.discardExact(60)
|
||||
val size = this.input.readShort().toInt()
|
||||
this.input.discardExact(3)
|
||||
xmlMessage = this.input.readString(size)
|
||||
}//todo test
|
||||
}
|
||||
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
class ServerGroupMessageEventPacket(input: DataInputStream, packetId: ByteArray, eventIdentity: ByteArray) : ServerEventPacket(input, packetId, eventIdentity) {
|
||||
var groupNumber: Long = 0
|
||||
var qq: Long = 0
|
||||
class ServerGroupMessageEventPacket(input: ByteReadPacket, packetId: ByteArray, eventIdentity: EventPacketIdentity) : ServerEventPacket(input, packetId, eventIdentity) {
|
||||
var groupNumber: UInt by Delegates.notNull()
|
||||
var qq: UInt by Delegates.notNull()
|
||||
lateinit var senderName: String
|
||||
lateinit var message: MessageChain
|
||||
|
||||
enum class MessageType {
|
||||
NORMAL,
|
||||
XML,
|
||||
AT,
|
||||
FACE,//qq自带表情 [face107.gif]
|
||||
override fun decode() = with(input) {
|
||||
discardExact(31)
|
||||
groupNumber = readUInt()
|
||||
discardExact(1)
|
||||
qq = readUInt()
|
||||
|
||||
PLAIN_TEXT, //纯文本
|
||||
IMAGE, //自定义图片 {F50C5235-F958-6DF7-4EFA-397736E125A4}.gif
|
||||
discardExact(48)
|
||||
readLVByteArray()
|
||||
discardExact(2)//2个0x00
|
||||
message = readMessageChain()
|
||||
|
||||
ANONYMOUS,//匿名用户发出的消息
|
||||
|
||||
OTHER,
|
||||
}
|
||||
|
||||
|
||||
override fun decode() {
|
||||
println(this.input.goto(0).readAllBytes().toUHexString())
|
||||
groupNumber = this.input.goto(51).readInt().toLong()
|
||||
qq = this.input.goto(56).readNBytes(4).toUInt().toLong()
|
||||
|
||||
this.input.goto(108)
|
||||
this.input.readLVByteArray()
|
||||
input.skip(2)//2个0x00
|
||||
message = input.readMessageChain()
|
||||
|
||||
val map = input.readTLVMap(true)
|
||||
if (map.containsKey(18)) {
|
||||
this.senderName = dataDecode(map.getValue(18)) {
|
||||
val tlv = it.readTLVMap(true)
|
||||
tlv.printTLVMap()
|
||||
val map = readTLVMap(true)
|
||||
map.printTLVMap("父map")
|
||||
if (map.containsKey(0x18)) {
|
||||
senderName = map.getValue(0x18).read {
|
||||
val tlv = readTLVMap(true)
|
||||
tlv.printTLVMap("子map")
|
||||
|
||||
when {
|
||||
tlv.containsKey(0x01) -> String(tlv.getValue(0x01))
|
||||
@ -147,128 +152,37 @@ class ServerGroupMessageEventPacket(input: DataInputStream, packetId: ByteArray,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
messageType = when (val id = this.input.goto(110 + fontLength + 2).readByte().toInt()) {
|
||||
0x13 -> MessageType.NORMAL
|
||||
0x0E -> MessageType.XML
|
||||
0x06 -> MessageType.AT
|
||||
|
||||
|
||||
0x01 -> MessageType.PLAIN_TEXT
|
||||
0x02 -> MessageType.FACE
|
||||
0x03 -> MessageType.IMAGE
|
||||
0x19 -> MessageType.ANONYMOUS
|
||||
|
||||
else -> {
|
||||
MiraiLogger.debug("ServerGroupMessageEventPacket id=$id")
|
||||
MessageType.OTHER
|
||||
}
|
||||
}*/
|
||||
|
||||
/*
|
||||
when (messageType) {
|
||||
MessageType.NORMAL -> {
|
||||
val gzippedMessage = this.input.goto(110 + fontLength + 16).readNBytes(this.input.goto(110 + fontLength + 3).readShort().toInt() - 11)
|
||||
ByteArrayOutputStream().let {
|
||||
GZIPInputStream(gzippedMessage.inputStream()).transferTo(it)
|
||||
message = String(it.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
MessageType.XML -> {
|
||||
val gzippedMessage = this.input.goto(110 + fontLength + 9).readNBytes(this.input.goto(110 + fontLength + 3).readShort().toInt() - 4)
|
||||
ByteArrayOutputStream().let {
|
||||
GZIPInputStream(gzippedMessage.inputStream()).transferTo(it)
|
||||
message = String(it.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
MessageType.FACE -> {
|
||||
val faceId = this.input.goto(110 + fontLength + 8).readByte()
|
||||
message = "[face${faceId}.gif]"
|
||||
}
|
||||
|
||||
MessageType.AT, MessageType.OTHER, MessageType.PLAIN_TEXT, MessageType.IMAGE, MessageType.ANONYMOUS -> {
|
||||
var messageLength: Int = this.input.goto(110 + fontLength + 6).readShort().toInt()
|
||||
message = String(this.input.goto(110 + fontLength + 8).readNBytes(messageLength))
|
||||
|
||||
val oeLength: Int
|
||||
if (this.input.readByte().toInt() == 6) {
|
||||
oeLength = this.input.readShort().toInt()
|
||||
this.input.skip(4)
|
||||
val messageLength2 = this.input.readShort().toInt()
|
||||
val message2 = String(this.input.readNBytes(messageLength2))
|
||||
message += message2
|
||||
messageLength += messageLength2
|
||||
} else {
|
||||
oeLength = this.input.readShort().toInt()
|
||||
}
|
||||
|
||||
//读取 nick, ignore.
|
||||
/*
|
||||
when (this.input.goto(110 + fontLength + 3 + oeLength).readByteAt().toInt()) {
|
||||
12 -> {
|
||||
this.input.skip(4)//maybe 5?
|
||||
|
||||
}
|
||||
19 -> {
|
||||
|
||||
}
|
||||
0x0E -> {
|
||||
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(String("E7 BE A4".hexToBytes()))
|
||||
|
||||
|
||||
println(".".toByteArray().toUByteArray().toUHexString())
|
||||
//长文本 22 96 29 7B B4 DF 94 AA 00 01 9F 8E 09 18 85 5B 1F 40 00 52 00 00 00 1B 00 09 00 06 00 01 00 00 00 01 00 0A 00 04 01 00 00 00 00 0C 00 05 00 01 00 01 01 22 96 29 7B 01 3E 03 3F A2 00 03 7E F3 5D 7B 97 57 00 00 F3 32 00 B8 00 01 01 00 00 00 00 00 00 00 4D 53 47 00 00 00 00 00 5D 7B 97 56 7F D0 53 BB 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 12 01 00 0F E9 95 BF E6 96 87 E6 9C AC E6 B6 88 E6 81 AF 0E 00 0E 01 00 04 00 00 00 09 07 00 04 00 00 00 01 19 00 35 01 00 32 AA 02 2F 50 03 60 00 68 00 9A 01 26 08 09 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 05 00 04 00 00 00 01 08 00 04 00 00 00 01 01 00 09 48 69 6D 31 38 38 6D 6F 65 03 00 01 04 04 00 04 00 00 00 08
|
||||
val packet = ServerGroupMessageEventPacket(("" +
|
||||
"22 96 29 7B B4 DF 94 AA 00 09 8F 37 0A 65 07 2E 1F 40 00 52 00 00 00 1B 00 09 00 06 00 01 00 00 00 01 00 0A 00 04 01 00 00 00 00 0C 00 05 00 01 00 01 01 22 96 29 7B 01 3E 03 3F A2 00 03 7F 67 5D 7B AE D7 00 00 F3 36 02 E7 00 02 02 00 1B 10 00 00 00 00 4D 53 47 00 00 00 00 00 5D 7B AE D6 F4 91 87 BE 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 E7 89 9B E9 80 BC 03 00 CB 02 00 2A 7B 37 41 41 34 42 33 41 41 2D 38 43 33 43 2D 30 46 34 35 2D 32 44 39 42 2D 37 46 33 30 32 41 30 41 43 45 41 41 7D 2E 6A 70 67 04 00 04 83 81 3B E2 05 00 04 B8 8B 33 79 06 00 04 00 00 00 50 07 00 01 43 08 00 00 09 00 01 01 0B 00 00 14 00 04 00 00 00 00 15 00 04 00 00 00 41 16 00 04 00 00 00 34 18 00 04 00 00 03 73 FF 00 5C 15 36 20 39 32 6B 41 31 43 38 33 38 31 33 62 65 32 62 38 38 62 33 33 37 39 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 37 41 41 34 42 33 41 41 2D 38 43 33 43 2D 30 46 34 35 2D 32 44 39 42 2D 37 46 33 30 32 41 30 41 43 45 41 41 7D 2E 6A 70 67 41 01 00 09 01 00 06 E7 89 9B E9 80 BC 03 00 77 02 00 2A 7B 37 41 41 34 42 33 41 41 2D 38 43 33 43 2D 30 46 34 35 2D 32 44 39 42 2D 37 46 33 30 32 41 30 41 43 45 41 41 7D 2E 6A 70 67 04 00 04 83 81 3B E2 05 00 04 B8 8B 33 79 06 00 04 00 00 00 50 07 00 01 43 08 00 00 09 00 01 01 0B 00 00 14 00 04 00 00 00 00 15 00 04 00 00 00 41 16 00 04 00 00 00 34 18 00 04 00 00 03 73 FF 00 08 15 37 20 20 38 41 41 41 02 00 14 01 00 01 AF 0B 00 08 00 01 00 04 52 CC F5 D0 FF 00 02 14 F0 03 00 CE 02 00 2A 7B 31 46 42 34 43 32 35 45 2D 42 34 46 45 2D 31 32 45 34 2D 46 33 42 42 2D 38 31 39 31 33 37 42 44 39 39 30 39 7D 2E 6A 70 67 04 00 04 B8 27 4B C6 05 00 04 79 5C B1 A3 06 00 04 00 00 00 50 07 00 01 41 08 00 00 09 00 01 01 0B 00 00 14 00 04 03 00 00 00 15 00 04 00 00 00 4E 16 00 04 00 00 00 23 18 00 04 00 00 02 A2 FF 00 5F 15 36 20 39 35 6B 44 31 41 62 38 32 37 34 62 63 36 37 39 35 63 62 31 61 33 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 31 46 42 34 43 32 35 45 2D 42 34 46 45 2D 31 32 45 34 2D 46 33 42 42 2D 38 31 39 31 33 37 42 44 39 39 30 39 7D 2E 6A 70 67 41 42 43 41 0E 00 07 01 00 04 00 00 00 09 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" +
|
||||
"").hexToBytes().dataInputStream(), byteArrayOf(), byteArrayOf())
|
||||
packet.decode()
|
||||
println(packet)
|
||||
}
|
||||
|
||||
//牛逼[图片]牛逼[图片] 22 96 29 7B B4 DF 94 AA 00 08 74 A4 09 18 8D CC 1F 40 00 52 00 00 00 1B 00 09 00 06 00 01 00 00 00 01 00 0A 00 04 01 00 00 00 00 0C 00 05 00 01 00 01 01 22 96 29 7B 01 3E 03 3F A2 00 03 7F 64 5D 7B AC BD 00 00 F3 36 02 03 00 02 01 00 00 00 00 00 00 00 4D 53 47 00 00 00 00 00 5D 7B AC BD 12 73 DB A2 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 E7 89 9B E9 80 BC 03 00 CB 02 00 2A 7B 37 41 41 34 42 33 41 41 2D 38 43 33 43 2D 30 46 34 35 2D 32 44 39 42 2D 37 46 33 30 32 41 30 41 43 45 41 41 7D 2E 6A 70 67 04 00 04 B4 52 77 F1 05 00 04 BC EB 03 B7 06 00 04 00 00 00 50 07 00 01 43 08 00 00 09 00 01 01 0B 00 00 14 00 04 00 00 00 00 15 00 04 00 00 00 41 16 00 04 00 00 00 34 18 00 04 00 00 03 73 FF 00 5C 15 36 20 39 32 6B 41 31 43 62 34 35 32 37 37 66 31 62 63 65 62 30 33 62 37 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 37 41 41 34 42 33 41 41 2D 38 43 33 43 2D 30 46 34 35 2D 32 44 39 42 2D 37 46 33 30 32 41 30 41 43 45 41 41 7D 2E 6A 70 67 41 01 00 09 01 00 06 E7 89 9B E9 80 BC 03 00 77 02 00 2A 7B 37 41 41 34 42 33 41 41 2D 38 43 33 43 2D 30 46 34 35 2D 32 44 39 42 2D 37 46 33 30 32 41 30 41 43 45 41 41 7D 2E 6A 70 67 04 00 04 B4 52 77 F1 05 00 04 BC EB 03 B7 06 00 04 00 00 00 50 07 00 01 43 08 00 00 09 00 01 01 0B 00 00 14 00 04 00 00 00 00 15 00 04 00 00 00 41 16 00 04 00 00 00 34 18 00 04 00 00 03 73 FF 00 08 15 37 20 20 38 41 41 41 0E 00 0E 01 00 04 00 00 00 09 07 00 04 00 00 00 01 19 00 35 01 00 32 AA 02 2F 50 03 60 00 68 00 9A 01 26 08 09 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 05 00 04 00 00 00 01 08 00 04 00 00 00 01 01 00 09 48 69 6D 31 38 38 6D 6F 65 03 00 01 04 04 00 04 00 00 00 08
|
||||
//牛逼[图片]牛逼 22 96 29 7B B4 DF 94 AA 00 0B C1 0A 09 18 89 93 1F 40 00 52 00 00 00 1B 00 09 00 06 00 01 00 00 00 01 00 0A 00 04 01 00 00 00 00 0C 00 05 00 01 00 01 01 22 96 29 7B 01 3E 03 3F A2 00 03 7E F5 5D 7B 97 E7 00 00 F3 32 01 8D 00 02 01 00 00 00 00 00 00 00 4D 53 47 00 00 00 00 00 5D 7B 97 E6 FA BE 7F DC 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 E7 89 9B E9 80 BC 03 00 CF 02 00 2A 7B 39 44 32 44 45 39 31 41 2D 33 39 38 39 2D 39 35 35 43 2D 44 35 42 34 2D 37 46 41 32 37 38 39 37 38 36 30 39 7D 2E 6A 70 67 04 00 04 97 15 7F 03 05 00 04 79 5C B1 A3 06 00 04 00 00 00 50 07 00 01 41 08 00 00 09 00 01 01 0B 00 00 14 00 04 03 00 00 00 15 00 04 00 00 00 3C 16 00 04 00 00 00 40 18 00 04 00 00 03 CC FF 00 60 15 36 20 39 36 6B 45 31 41 39 37 31 35 37 66 30 33 37 39 35 63 62 31 61 33 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 39 44 32 44 45 39 31 41 2D 33 39 38 39 2D 39 35 35 43 2D 44 35 42 34 2D 37 46 41 32 37 38 39 37 38 36 30 39 7D 2E 6A 70 67 31 32 31 32 41 01 00 09 01 00 06 E7 89 9B E9 80 BC 0E 00 0E 01 00 04 00 00 00 09 07 00 04 00 00 00 01 19 00 35 01 00 32 AA 02 2F 50 03 60 00 68 00 9A 01 26 08 09 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 05 00 04 00 00 00 01 08 00 04 00 00 00 01 01 00 09 48 69 6D 31 38 38 6D 6F 65 03 00 01 04 04 00 04 00 00 00 08
|
||||
|
||||
class ServerFriendMessageEventPacket(input: DataInputStream, packetId: ByteArray, eventIdentity: ByteArray) : ServerEventPacket(input, packetId, eventIdentity) {
|
||||
var qq: Long = 0
|
||||
class ServerFriendMessageEventPacket(input: ByteReadPacket, packetId: ByteArray, eventIdentity: EventPacketIdentity) : ServerEventPacket(input, packetId, eventIdentity) {
|
||||
val group: UInt get() = eventIdentity.from
|
||||
val qq: UInt get() = eventIdentity.to
|
||||
|
||||
lateinit var message: MessageChain
|
||||
|
||||
//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 E7 86 74 F2 64 55 AD 9A EB 2F B9 DF F1 7F 8C 28 00 0B 78 14 5D A2 F5 CB 01 1D 00 00 00 00 01 00 00 00 01 4D 53 47 00 00 00 00 00 5D A2 F5 CA 9D 26 CB 5E 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
|
||||
|
||||
override fun decode() {
|
||||
input.goto(0)
|
||||
println("ServerFriendMessageEventPacket.input=" + input.readAllBytes().toUHexString())
|
||||
input.goto(0)
|
||||
override fun decode() = with(input) {
|
||||
input.discardExact(2)
|
||||
val l1 = readShort()
|
||||
discardExact(l1.toInt())
|
||||
discardExact(69)
|
||||
readLVByteArray()//font
|
||||
discardExact(2)//2个0x00
|
||||
message = readMessageChain()
|
||||
|
||||
qq = input.readUIntAt(0).toLong()
|
||||
|
||||
val l1 = input.readShortAt(22)
|
||||
input.goto(93 + l1)
|
||||
input.readLVByteArray()//font
|
||||
input.skip(2)//2个0x00
|
||||
message = input.readMessageChain()
|
||||
|
||||
val map: Map<Int, ByteArray> = input.readTLVMap(true).withDefault { byteArrayOf() }
|
||||
println(map.getValue(18))
|
||||
val map: Map<Int, ByteArray> = readTLVMap(true).withDefault { byteArrayOf() }
|
||||
println("map.getValue(18)=" + map.getValue(18))
|
||||
|
||||
//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
|
||||
message = MessageChain(PlainText(let {
|
||||
event = MessageChain(PlainText(let {
|
||||
val length = input.readShortAt(101 + offset)//
|
||||
input.goto(103 + offset).readString(length.toInt())
|
||||
}))*/
|
||||
@ -318,9 +232,9 @@ B1 89 BE 09 8F 00 1A E5 00 0B 03 A2 09 90 BB 7A 1F 40 00 A6 00 00 00 20 00 05 00
|
||||
|
||||
backup
|
||||
|
||||
class ServerFriendMessageEventPacket(input: DataInputStream, packetId: ByteArray, eventIdentity: ByteArray) : ServerEventPacket(input, packetId, eventIdentity) {
|
||||
class ServerFriendMessageEventPacket(input: ByteReadPacket, packetId: ByteArray, eventIdentity: EventPacketIdentity) : ServerEventPacket(input, packetId, eventIdentity) {
|
||||
var qq: Long = 0
|
||||
lateinit var message: String
|
||||
lateinit var event: String
|
||||
|
||||
|
||||
|
||||
@ -330,7 +244,7 @@ class ServerFriendMessageEventPacket(input: DataInputStream, packetId: ByteArray
|
||||
val msgLength = input.readShortAt(22)
|
||||
val fontLength = input.readShortAt(93+msgLength)
|
||||
val offset = msgLength+fontLength
|
||||
message = if(input.readByteAt(97+offset).toUHexString() == "02"){
|
||||
event = if(input.readByteAt(97+offset).toUHexString() == "02"){
|
||||
"[face" + input.goto(103+offset).readByteAt(1).toInt().toString() + ".gif]"
|
||||
//.gif
|
||||
}else {
|
@ -0,0 +1,91 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
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 net.mamoe.mirai.utils.*
|
||||
|
||||
|
||||
abstract class ServerPacket(val input: ByteReadPacket) : Packet(), Closeable {
|
||||
override var idHex: String = EMPTY_ID_HEX
|
||||
get() {
|
||||
if (field === EMPTY_ID_HEX) {
|
||||
idHex = (this::class.annotations.firstOrNull { it::class == PacketId::class } as? PacketId)?.value?.trim()
|
||||
?: ""
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
var encoded: Boolean = false
|
||||
|
||||
open fun decode() {
|
||||
|
||||
}
|
||||
|
||||
override fun close() = this.input.close()
|
||||
|
||||
companion object {
|
||||
private const val EMPTY_ID_HEX = "EMPTY_ID_HEX"
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String = this.packetToString()
|
||||
|
||||
fun getFixedId(id: String): String = when (id.length) {
|
||||
0 -> "__ __ __ __"
|
||||
2 -> "$id __ __ __"
|
||||
5 -> "$id __ __"
|
||||
7 -> "$id __"
|
||||
else -> id
|
||||
}
|
||||
|
||||
fun decryptBy(key: ByteArray): ByteReadPacket {
|
||||
return ByteReadPacket(decryptAsByteArray(key))
|
||||
}
|
||||
|
||||
fun decryptBy(key: IoBuffer): ByteReadPacket {
|
||||
return ByteReadPacket(decryptAsByteArray(key))
|
||||
}
|
||||
|
||||
fun decryptBy(keyHex: String): ByteReadPacket {
|
||||
return this.decryptBy(keyHex.hexToBytes())
|
||||
}
|
||||
|
||||
fun decryptBy(key1: ByteArray, key2: ByteArray): ByteReadPacket {
|
||||
return TEA.decrypt(this.decryptAsByteArray(key1), key2).toReadPacket()
|
||||
}
|
||||
|
||||
|
||||
fun decryptBy(key1: String, key2: ByteArray): ByteReadPacket {
|
||||
return this.decryptBy(key1.hexToBytes(), key2)
|
||||
}
|
||||
|
||||
fun decryptBy(key1: String, key2: IoBuffer): ByteReadPacket {
|
||||
return this.decryptBy(key1.hexToBytes(), key2.readBytes())
|
||||
}
|
||||
|
||||
|
||||
fun decryptBy(key1: ByteArray, key2: String): ByteReadPacket {
|
||||
return this.decryptBy(key1, key2.hexToBytes())
|
||||
}
|
||||
|
||||
|
||||
fun decryptBy(keyHex1: String, keyHex2: String): ByteReadPacket {
|
||||
return this.decryptBy(keyHex1.hexToBytes(), keyHex2.hexToBytes())
|
||||
}
|
||||
|
||||
fun decryptAsByteArray(key: ByteArray): ByteArray {
|
||||
return TEA.decrypt(input.readRemainingBytes().cutTail(1), key)
|
||||
}
|
||||
|
||||
fun decryptAsByteArray(keyHex: String): ByteArray = this.decryptAsByteArray(keyHex.hexToBytes())
|
||||
fun decryptAsByteArray(buffer: IoBuffer): ByteArray = this.decryptAsByteArray(buffer.readBytes())
|
||||
}
|
||||
|
||||
fun <P : ServerPacket> P.setId(idHex: String): P {
|
||||
this.idHex = idHex
|
||||
return this
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.utils.LoggerTextFormat
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
|
||||
|
||||
class UnknownServerPacket(input: ByteReadPacket) : ServerPacket(input) {
|
||||
override fun decode() {
|
||||
val raw = this.input.readBytes()
|
||||
MiraiLogger.logDebug("UnknownServerPacket data: " + raw.toUHexString())
|
||||
}
|
||||
|
||||
|
||||
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
|
||||
fun decrypt(sessionKey: ByteArray): UnknownServerPacket = UnknownServerPacket(this.decryptBy(sessionKey)).setId(this.idHex)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return LoggerTextFormat.LIGHT_RED.toString() + super.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ID: 00 17
|
||||
|
||||
长度 95
|
||||
76 E4 B8 DD //1994701021
|
||||
76 E4 B8 DD //1994701021
|
||||
00 0B B9 A9 09 90 BB 54 1F //类似Event的uniqueId?
|
||||
40 02
|
||||
10 00 00 00
|
||||
18 00
|
||||
08 00
|
||||
02 00
|
||||
01 00
|
||||
09 00
|
||||
06 41 4B DA 4C 00 00
|
||||
00 0A
|
||||
00 04
|
||||
01 00 00 00 00 00
|
||||
00 06 00 00
|
||||
00 0E
|
||||
08 02
|
||||
1A 02 08 49 0A 0C 08 A2 FF 8C F0
|
||||
03 10 CA EB 8B ED 05
|
||||
|
||||
或者
|
||||
|
||||
长度63
|
||||
00 00 27 10 76 E4 B8 DD
|
||||
00 09 ED 26 64 73 0E CA 1F 40
|
||||
00 12 00 00
|
||||
00 08
|
||||
00 0A
|
||||
00 04
|
||||
01 00 00
|
||||
00 02
|
||||
|
||||
值都是一样的.
|
||||
*/
|
@ -0,0 +1,55 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.utils.gotoWhere
|
||||
import net.mamoe.mirai.utils.toReadPacket
|
||||
|
||||
expect class PlatformImage
|
||||
|
||||
expect class ClientTryGetImageIDPacket(
|
||||
botNumber: Long,
|
||||
sessionKey: ByteArray,
|
||||
groupNumberOrQQNumber: Long,
|
||||
image: PlatformImage
|
||||
) : ClientPacket
|
||||
|
||||
|
||||
abstract class ServerTryGetImageIDResponsePacket(input: ByteReadPacket) : ServerPacket(input) {
|
||||
|
||||
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
|
||||
fun decrypt(sessionKey: ByteArray): ServerTryGetImageIDResponsePacket {
|
||||
val data = this.decryptAsByteArray(sessionKey)
|
||||
println(data.size)
|
||||
println(data.size)
|
||||
if (data.size == 209) {
|
||||
return ServerTryGetImageIDSuccessPacket(data.toReadPacket()).setId(this.idHex)
|
||||
}
|
||||
|
||||
return ServerTryGetImageIDFailedPacket(data.toReadPacket())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器未存有图片, 返回一个 key 用于客户端上传
|
||||
*/
|
||||
class ServerTryGetImageIDSuccessPacket(input: ByteReadPacket) : ServerTryGetImageIDResponsePacket(input) {
|
||||
lateinit var uKey: ByteArray
|
||||
|
||||
override fun decode() {
|
||||
this.input.gotoWhere(ubyteArrayOf(0x42u, 0x80u, 0x01u))
|
||||
uKey = this.input.readBytes(128)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器已经存有这个图片
|
||||
*/
|
||||
class ServerTryGetImageIDFailedPacket(input: ByteReadPacket) : ServerTryGetImageIDResponsePacket(input) {
|
||||
override fun decode() {
|
||||
|
||||
}
|
||||
}
|
@ -2,12 +2,15 @@
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.action
|
||||
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.utils.getRandomByteArray
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
import java.io.DataInputStream
|
||||
import java.util.*
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.setId
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
/**
|
||||
* 向服务器检查是否可添加某人为好友
|
||||
@ -20,15 +23,11 @@ class ClientCanAddFriendPacket(
|
||||
val qq: Long,
|
||||
val sessionKey: ByteArray
|
||||
) : ClientPacket() {
|
||||
val packetIdLast = getRandomByteArray(2)
|
||||
|
||||
override fun getFixedId(): String {
|
||||
return idHex + " " + packetIdLast.toUHexString()
|
||||
override val idHex: String by lazy {
|
||||
super.idHex + " " + getRandomByteArray(2).toUHexString()
|
||||
}
|
||||
|
||||
override fun encode() {
|
||||
this.write(packetIdLast)//id, 2bytes
|
||||
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(bot)
|
||||
this.writeHex(TIMProtocol.fixVer2)
|
||||
this.encryptAndWrite(sessionKey) {
|
||||
@ -38,7 +37,7 @@ class ClientCanAddFriendPacket(
|
||||
}
|
||||
|
||||
@PacketId("00 A7")
|
||||
class ServerCanAddFriendResponsePacket(input: DataInputStream) : ServerPacket(input) {
|
||||
class ServerCanAddFriendResponsePacket(input: ByteReadPacket) : ServerPacket(input) {
|
||||
lateinit var state: State
|
||||
|
||||
enum class State {
|
||||
@ -50,7 +49,8 @@ class ServerCanAddFriendResponsePacket(input: DataInputStream) : ServerPacket(in
|
||||
|
||||
|
||||
override fun decode() {
|
||||
val data = input.goto(0).readAllBytes()
|
||||
input.readBytes()
|
||||
val data = input.readRemainingBytes()
|
||||
if (data.size == 99) {
|
||||
state = State.ALREADY_ADDED
|
||||
return
|
||||
@ -61,13 +61,13 @@ class ServerCanAddFriendResponsePacket(input: DataInputStream) : ServerPacket(in
|
||||
0x99u -> State.ALREADY_ADDED
|
||||
0x03u,
|
||||
0x04u -> State.FAILED
|
||||
else -> throw IllegalArgumentException(Arrays.toString(data))
|
||||
else -> throw IllegalArgumentException(data.contentToString())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@PacketId("00 A7")
|
||||
class Encrypted(inputStream: DataInputStream) : ServerPacket(inputStream) {
|
||||
class Encrypted(inputStream: ByteReadPacket) : ServerPacket(inputStream) {
|
||||
fun decrypt(sessionKey: ByteArray): ServerCanAddFriendResponsePacket {
|
||||
return ServerCanAddFriendResponsePacket(this.decryptBy(sessionKey)).setId(this.idHex)
|
||||
}
|
||||
@ -84,15 +84,11 @@ class ClientAddFriendPacket(
|
||||
val qq: Long,
|
||||
val sessionKey: ByteArray
|
||||
) : ClientPacket() {
|
||||
val packetIdLast = getRandomByteArray(2)
|
||||
|
||||
override fun getFixedId(): String {
|
||||
return idHex + " " + packetIdLast.toUHexString()
|
||||
override val idHex: String by lazy {
|
||||
super.idHex + " " + getRandomByteArray(2).toUHexString()
|
||||
}
|
||||
|
||||
override fun encode() {
|
||||
this.write(packetIdLast)//id, 2bytes
|
||||
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(bot)
|
||||
this.writeHex(TIMProtocol.fixVer2)
|
||||
this.encryptAndWrite(sessionKey) {
|
||||
@ -104,17 +100,17 @@ class ClientAddFriendPacket(
|
||||
}
|
||||
|
||||
|
||||
class ServerAddFriendResponsePacket(input: DataInputStream) : ServerAddContactResponsePacket(input)
|
||||
class ServerAddFriendResponsePacket(input: ByteReadPacket) : ServerAddContactResponsePacket(input)
|
||||
|
||||
class ServerAddGroupResponsePacket(input: DataInputStream) : ServerAddContactResponsePacket(input)
|
||||
class ServerAddGroupResponsePacket(input: ByteReadPacket) : ServerAddContactResponsePacket(input)
|
||||
|
||||
/**
|
||||
* 添加好友/群的回复
|
||||
*/
|
||||
abstract class ServerAddContactResponsePacket(input: DataInputStream) : ServerPacket(input) {
|
||||
abstract class ServerAddContactResponsePacket(input: ByteReadPacket) : ServerPacket(input) {
|
||||
|
||||
|
||||
class Raw(input: DataInputStream) : ServerPacket(input) {
|
||||
class Raw(input: ByteReadPacket) : ServerPacket(input) {
|
||||
|
||||
override fun decode() {
|
||||
|
||||
@ -125,7 +121,7 @@ abstract class ServerAddContactResponsePacket(input: DataInputStream) : ServerPa
|
||||
TODO()
|
||||
}
|
||||
|
||||
class Encrypted(input: DataInputStream) : ServerPacket(input) {
|
||||
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
|
||||
fun decrypt(sessionKey: ByteArray): Raw = Raw(this.decryptBy(sessionKey))
|
||||
}
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
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.toByteArray
|
||||
import net.mamoe.mirai.message.internal.toPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.utils.dataEncode
|
||||
import java.io.DataInputStream
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
@PacketId("00 CD")
|
||||
class ClientSendFriendMessagePacket(
|
||||
private val botQQ: Long,
|
||||
@ -17,8 +17,8 @@ class ClientSendFriendMessagePacket(
|
||||
private val sessionKey: ByteArray,
|
||||
private val message: MessageChain
|
||||
) : ClientPacket() {
|
||||
override fun encode() {
|
||||
this.writeRandom(2)//part of packet id
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeRandom(2)
|
||||
|
||||
this.writeQQ(botQQ)
|
||||
this.writeHex(TIMProtocol.fixVer2)
|
||||
@ -30,7 +30,7 @@ class ClientSendFriendMessagePacket(
|
||||
writeHex("37 0F")//TIM最新: 38 03
|
||||
writeQQ(botQQ)
|
||||
writeQQ(targetQQ)
|
||||
write(md5(dataEncode { md5Key -> md5Key.writeQQ(targetQQ); md5Key.write(sessionKey) }))
|
||||
writeFully(md5(buildPacket { writeQQ(targetQQ); writeFully(sessionKey) }.readBytes()))
|
||||
writeHex("00 0B")
|
||||
writeRandom(2)
|
||||
writeTime()
|
||||
@ -54,11 +54,11 @@ class ClientSendFriendMessagePacket(
|
||||
writeHex(TIMProtocol.messageConst1)//... 85 E9 BB 91
|
||||
writeZero(2)
|
||||
|
||||
write(message.toByteArray())
|
||||
writePacket(message.toPacket())
|
||||
|
||||
/*
|
||||
//Plain text
|
||||
val bytes = message.toByteArray()
|
||||
val bytes = event.toPacket()
|
||||
it.writeByte(0x01)
|
||||
it.writeShort(bytes.size + 3)
|
||||
it.writeByte(0x01)
|
||||
@ -69,4 +69,4 @@ class ClientSendFriendMessagePacket(
|
||||
}
|
||||
|
||||
@PacketId("00 CD")
|
||||
class ServerSendFriendMessageResponsePacket(input: DataInputStream) : ServerPacket(input)
|
||||
class ServerSendFriendMessageResponsePacket(input: ByteReadPacket) : ServerPacket(input)
|
@ -0,0 +1,52 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.action
|
||||
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.message.MessageChain
|
||||
import net.mamoe.mirai.message.internal.toPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
|
||||
@PacketId("00 02")
|
||||
class ClientSendGroupMessagePacket(
|
||||
private val botQQ: Long,
|
||||
private val groupId: Long,//不是 number
|
||||
private val sessionKey: ByteArray,
|
||||
private val message: MessageChain
|
||||
) : ClientPacket() {
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeRandom(2)
|
||||
this.writeQQ(botQQ)
|
||||
this.writeHex(TIMProtocol.fixVer2)
|
||||
|
||||
this.encryptAndWrite(sessionKey) {
|
||||
writeByte(0x2A)
|
||||
writeGroup(groupId)
|
||||
|
||||
writeLVPacket {
|
||||
writeHex("00 01 01")
|
||||
writeHex("00 00 00 00 00 00 00 4D 53 47 00 00 00 00 00")
|
||||
|
||||
writeTime()
|
||||
writeRandom(4)
|
||||
writeHex("00 00 00 00 09 00 86")
|
||||
writeHex(TIMProtocol.messageConst1)
|
||||
writeZero(2)
|
||||
|
||||
writePacket(message.toPacket())
|
||||
}
|
||||
/*it.writeByte(0x01)
|
||||
it.writeShort(bytes.size + 3)
|
||||
it.writeByte(0x01)
|
||||
it.writeShort(bytes.size)
|
||||
it.write(bytes)*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PacketId("00 02")
|
||||
class ServerSendGroupMessageResponsePacket(input: ByteReadPacket) : ServerPacket(input)
|
@ -2,32 +2,32 @@
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.writeUByte
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.utils.ClientLoginStatus
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
/**
|
||||
* 改变在线状态: "我在线上", "隐身" 等
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
@PacketId("00 EC")
|
||||
class ClientChangeOnlineStatusPacket(
|
||||
private val qq: Long,
|
||||
private val sessionKey: ByteArray,
|
||||
private val loginStatus: ClientLoginStatus
|
||||
|
||||
private val loginStatus: OnlineStatus
|
||||
) : ClientPacket() {
|
||||
override val idHex: String by lazy {
|
||||
super.idHex + " " + getRandomByteArray(2).toUHexString()
|
||||
}
|
||||
|
||||
override fun encode() {
|
||||
this.writeRandom(2)//part of packet id
|
||||
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(TIMProtocol.fixVer2)
|
||||
this.encryptAndWrite(sessionKey) {
|
||||
writeHex("01 00")
|
||||
writeByte(loginStatus.id.toInt())
|
||||
writeUByte(loginStatus.id)
|
||||
writeHex("00 01 00 01 00 04 00 00 00 00")
|
||||
}
|
||||
}
|
@ -1,17 +1,16 @@
|
||||
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 net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.utils.TEA
|
||||
import net.mamoe.mirai.utils.Tested
|
||||
import net.mamoe.mirai.utils.hexToBytes
|
||||
import java.io.DataOutputStream
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
|
||||
/**
|
||||
* Password submission (0836_622)
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
@PacketId("08 36 31 03")
|
||||
@Tested
|
||||
@ -21,10 +20,11 @@ class ClientPasswordSubmissionPacket(
|
||||
private val loginTime: Int,
|
||||
private val loginIP: String,
|
||||
private val privateKey: ByteArray,//16 random by client
|
||||
private val token0825: ByteArray//56 from server
|
||||
private val token0825: ByteArray,//56 from server
|
||||
private val randomDeviceName: Boolean
|
||||
) : ClientPacket() {
|
||||
|
||||
override fun encode() {
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(TIMProtocol.passwordSubmissionTLV1)
|
||||
|
||||
@ -36,7 +36,7 @@ class ClientPasswordSubmissionPacket(
|
||||
|
||||
//TODO shareKey 极大可能为 publicKey, key0836 计算得到
|
||||
this.encryptAndWrite(TIMProtocol.shareKey.hexToBytes()) {
|
||||
writePart1(qq, password, loginTime, loginIP, privateKey, token0825)
|
||||
writePart1(qq, password, loginTime, loginIP, privateKey, token0825, randomDeviceName)
|
||||
writePart2()
|
||||
}
|
||||
}
|
||||
@ -46,16 +46,13 @@ class ClientPasswordSubmissionPacket(
|
||||
//但为简化处理, 特固定这个 id
|
||||
|
||||
@PacketId("08 36 31 04")
|
||||
class ClientLoginResendPacket3104(qq: Long, password: String, loginTime: Int, loginIP: String, privateKey: ByteArray, token0825: ByteArray, token00BA: ByteArray, tlv0006: ByteArray? = null)
|
||||
: ClientLoginResendPacket(qq, password, loginTime, loginIP, privateKey, token0825, token00BA, tlv0006)
|
||||
class ClientLoginResendPacket3104(qq: Long, password: String, loginTime: Int, loginIP: String, privateKey: ByteArray, token0825: ByteArray, token00BA: ByteArray, randomDeviceName: Boolean, tlv0006: IoBuffer? = null) : ClientLoginResendPacket(qq, password, loginTime, loginIP, privateKey, token0825, token00BA, randomDeviceName, tlv0006)
|
||||
|
||||
@PacketId("08 36 31 05")
|
||||
class ClientLoginResendPacket3105(qq: Long, password: String, loginTime: Int, loginIP: String, privateKey: ByteArray, token0825: ByteArray, token00BA: ByteArray)
|
||||
: ClientLoginResendPacket(qq, password, loginTime, loginIP, privateKey, token0825, token00BA, null)
|
||||
class ClientLoginResendPacket3105(qq: Long, password: String, loginTime: Int, loginIP: String, privateKey: ByteArray, token0825: ByteArray, token00BA: ByteArray, randomDeviceName: Boolean, tlv0006: IoBuffer? = null) : ClientLoginResendPacket(qq, password, loginTime, loginIP, privateKey, token0825, token00BA, randomDeviceName, tlv0006)
|
||||
|
||||
@PacketId("08 36 31 06")
|
||||
class ClientLoginResendPacket3106(qq: Long, password: String, loginTime: Int, loginIP: String, privateKey: ByteArray, token0825: ByteArray, token00BA: ByteArray, tlv0006: ByteArray? = null)
|
||||
: ClientLoginResendPacket(qq, password, loginTime, loginIP, privateKey, token0825, token00BA, tlv0006)
|
||||
class ClientLoginResendPacket3106(qq: Long, password: String, loginTime: Int, loginIP: String, privateKey: ByteArray, token0825: ByteArray, token00BA: ByteArray, randomDeviceName: Boolean, tlv0006: IoBuffer? = null) : ClientLoginResendPacket(qq, password, loginTime, loginIP, privateKey, token0825, token00BA, randomDeviceName, tlv0006)
|
||||
|
||||
|
||||
open class ClientLoginResendPacket constructor(
|
||||
@ -66,9 +63,10 @@ open class ClientLoginResendPacket constructor(
|
||||
private val privateKey: ByteArray,
|
||||
private val token0825: ByteArray,
|
||||
private val token00BA: ByteArray,
|
||||
private val tlv0006: ByteArray? = null
|
||||
private val randomDeviceName: Boolean = false,
|
||||
private val tlv0006: IoBuffer? = null
|
||||
) : ClientPacket() {
|
||||
override fun encode() {
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(TIMProtocol.passwordSubmissionTLV1)
|
||||
|
||||
@ -79,13 +77,14 @@ open class ClientLoginResendPacket constructor(
|
||||
this.writeHex(TIMProtocol.key0836)//16
|
||||
|
||||
this.encryptAndWrite(TIMProtocol.shareKey.hexToBytes()) {
|
||||
writePart1(qq, password, loginTime, loginIP, privateKey, token0825, tlv0006)
|
||||
writePart1(qq, password, loginTime, loginIP, privateKey, token0825, randomDeviceName, tlv0006)
|
||||
|
||||
writeHex("01 10") //tag
|
||||
writeHex("00 3C")//length
|
||||
writeHex("00 01")//tag
|
||||
writeHex("00 38")//length
|
||||
write(token00BA)//value
|
||||
writeHex("01 10")
|
||||
writeHex("00 3C")
|
||||
writeHex("00 01")
|
||||
|
||||
writeHex("00 38")
|
||||
writeFully(token00BA)
|
||||
|
||||
writePart2()
|
||||
}
|
||||
@ -93,32 +92,40 @@ open class ClientLoginResendPacket constructor(
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
private fun DataOutputStream.writePart1(qq: Long, password: String, loginTime: Int, loginIP: String, privateKey: ByteArray, token0825: ByteArray, tlv0006: ByteArray? = null) {
|
||||
private fun BytePacketBuilder.writePart1(
|
||||
qq: Long,
|
||||
password: String,
|
||||
loginTime: Int,
|
||||
loginIP: String,
|
||||
privateKey: ByteArray,
|
||||
token0825: ByteArray,
|
||||
randomDeviceName: Boolean,
|
||||
tlv0006: IoBuffer? = null
|
||||
) {
|
||||
|
||||
//this.writeInt(System.currentTimeMillis().toInt())
|
||||
this.writeHex("01 12")//tag
|
||||
this.writeHex("00 38")//length
|
||||
this.write(token0825)//length
|
||||
this.writeFully(token0825)//length
|
||||
this.writeHex("03 0F")//tag
|
||||
this.writeDeviceName(false)
|
||||
this.writeDeviceName(randomDeviceName)
|
||||
|
||||
this.writeHex("00 05 00 06 00 02")
|
||||
this.writeQQ(qq)
|
||||
this.writeHex("00 06")//tag
|
||||
this.writeHex("00 78")//length
|
||||
if (tlv0006 != null) {
|
||||
this.write(tlv0006)
|
||||
MiraiLogger.logDebug("tlv0006!=null")
|
||||
this.writeFully(tlv0006)
|
||||
} else {
|
||||
MiraiLogger.logDebug("tlv0006==null")
|
||||
this.writeTLV0006(qq, password, loginTime, loginIP, privateKey)
|
||||
}
|
||||
//fix
|
||||
this.writeHex(TIMProtocol.passwordSubmissionTLV2)
|
||||
this.writeHex("00 1A")//tag
|
||||
this.writeHex("00 40")//length
|
||||
this.write(TEA.encrypt(TIMProtocol.passwordSubmissionTLV2.hexToBytes(), privateKey))
|
||||
this.writeFully(TEA.encrypt(TIMProtocol.passwordSubmissionTLV2.hexToBytes(), privateKey))
|
||||
this.writeHex(TIMProtocol.constantData1)
|
||||
this.writeHex(TIMProtocol.constantData2)
|
||||
this.writeQQ(qq)
|
||||
@ -133,7 +140,7 @@ private fun DataOutputStream.writePart1(qq: Long, password: String, loginTime: I
|
||||
}
|
||||
|
||||
|
||||
private fun DataOutputStream.writePart2() {
|
||||
private fun BytePacketBuilder.writePart2() {
|
||||
|
||||
this.writeHex("03 12")//tag
|
||||
this.writeHex("00 05")//length
|
||||
@ -161,9 +168,8 @@ private fun DataOutputStream.writePart2() {
|
||||
//value
|
||||
this.writeHex("E9 AA 2B 4D 26 4C 76 18 FE 59 D5 A9 82 6A 0C 04 B4 49 50 D7 9B B1 FE 5D 97 54 8D 82 F3 22 C2 48 B9 C9 22 69 CA 78 AD 3E 2D E9 C9 DF A8 9E 7D 8C 8D 6B DF 4C D7 34 D0 D3")
|
||||
|
||||
this.writeHex("00 14")//length
|
||||
writeCRC32()
|
||||
|
||||
this.writeHex("00 14")
|
||||
this.writeCRC32()
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
enum class LoginState {
|
||||
enum class LoginResult {
|
||||
/**
|
||||
* 登录成功
|
||||
*/
|
||||
@ -39,6 +36,11 @@ enum class LoginState {
|
||||
*/
|
||||
UNKNOWN,
|
||||
|
||||
/**
|
||||
* 未知. 更换服务器或等几分钟再登录可能解决.
|
||||
*/
|
||||
INTERNAL_ERROR,
|
||||
|
||||
/**
|
||||
* 超时
|
||||
*/
|
@ -1,43 +1,46 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import java.io.DataInputStream
|
||||
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.setId
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
/**
|
||||
* SKey 用于 http api
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
@PacketId("00 1D")
|
||||
class ClientSKeyRequestPacket(
|
||||
private val qq: Long,
|
||||
private val sessionKey: ByteArray
|
||||
) : ClientPacket() {
|
||||
override fun encode() {
|
||||
this.writeRandom(2)//part of packet id
|
||||
override val idHex: String by lazy {
|
||||
super.idHex + " " + getRandomByteArray(2).toUHexString()
|
||||
}
|
||||
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(TIMProtocol.fixVer2)
|
||||
this.encryptAndWrite(sessionKey) {
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
writeQQ(qq)
|
||||
writeHex(TIMProtocol.fixVer2)
|
||||
encryptAndWrite(sessionKey) {
|
||||
writeHex("33 00 05 00 08 74 2E 71 71 2E 63 6F 6D 00 0A 71 75 6E 2E 71 71 2E 63 6F 6D 00 0C 71 7A 6F 6E 65 2E 71 71 2E 63 6F 6D 00 0C 6A 75 62 61 6F 2E 71 71 2E 63 6F 6D 00 09 6B 65 2E 71 71 2E 63 6F 6D")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
@PacketId("00 1D")
|
||||
|
||||
class ClientSKeyRefreshmentRequestPacket(
|
||||
private val qq: Long,
|
||||
private val sessionKey: ByteArray
|
||||
) : ClientPacket() {
|
||||
override fun encode() {
|
||||
this.writeRandom(2)//part of packet id
|
||||
override val idHex: String by lazy {
|
||||
super.idHex + " " + getRandomByteArray(2).toUHexString()
|
||||
}
|
||||
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(qq)
|
||||
this.encryptAndWrite(sessionKey) {
|
||||
writeHex("33 00 05 00 08 74 2E 71 71 2E 63 6F 6D 00 0A 71 75 6E 2E 71 71 2E 63 6F 6D 00 0C 71 7A 6F 6E 65 2E 71 71 2E 63 6F 6D 00 0C 6A 75 62 61 6F 2E 71 71 2E 63 6F 6D 00 09 6B 65 2E 71 71 2E 63 6F 6D")
|
||||
@ -45,18 +48,16 @@ class ClientSKeyRefreshmentRequestPacket(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
class ServerSKeyResponsePacket(input: DataInputStream) : ServerPacket(input) {
|
||||
class ServerSKeyResponsePacket(input: ByteReadPacket) : ServerPacket(input) {
|
||||
lateinit var sKey: String
|
||||
|
||||
override fun decode() {
|
||||
this.sKey = String(this.input.goto(4).readNBytes(10))
|
||||
override fun decode() = with(input) {
|
||||
discardExact(4)
|
||||
sKey = this.readString(10)//todo test
|
||||
MiraiLogger.logDebug("SKey=$sKey")
|
||||
}
|
||||
|
||||
|
||||
class Encrypted(inputStream: DataInputStream) : ServerPacket(inputStream) {
|
||||
class Encrypted(inputStream: ByteReadPacket) : ServerPacket(inputStream) {
|
||||
fun decrypt(sessionKey: ByteArray): ServerSKeyResponsePacket = ServerSKeyResponsePacket(this.decryptBy(sessionKey)).setId(this.idHex)
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.setId
|
||||
import net.mamoe.mirai.utils.*
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
sealed class ServerLoginResponsePacket(input: ByteReadPacket) : ServerPacket(input)
|
||||
|
||||
class ServerLoginResponseFailedPacket(val loginResult: LoginResult, input: ByteReadPacket) : ServerLoginResponsePacket(input)
|
||||
|
||||
/**
|
||||
* 服务器进行加密后返回 privateKey
|
||||
*
|
||||
* @author NaturalHG
|
||||
*/
|
||||
@PacketId("08 36 31 03")
|
||||
class ServerLoginResponseKeyExchangePacket(input: ByteReadPacket, val flag: Flag) : ServerLoginResponsePacket(input) {
|
||||
enum class Flag {
|
||||
`08 36 31 03`,
|
||||
OTHER,
|
||||
}
|
||||
|
||||
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)//22
|
||||
this.input.discardExact(4)//00 06 00 78
|
||||
tlv0006 = this.input.readIoBuffer(0x78)
|
||||
|
||||
when (flag) {
|
||||
Flag.`08 36 31 03` -> {//TODO 在解析时分类而不是在这里
|
||||
this.input.discardExact(8)//01 10 00 3C 00 01 00 38
|
||||
tokenUnknown = this.input.readBytes(56)
|
||||
//println(tokenUnknown!!.toUHexString())
|
||||
}
|
||||
|
||||
Flag.OTHER -> {
|
||||
//do nothing in this packet.
|
||||
//[this.token] will be set in [BotNetworkHandler]
|
||||
//token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Encrypted(input: ByteReadPacket, private val flag: Flag) : ServerPacket(input) {
|
||||
@Tested
|
||||
fun decrypt(privateKey: ByteArray): ServerLoginResponseKeyExchangePacket = ServerLoginResponseKeyExchangePacket(this.decryptBy(TIMProtocol.shareKey, privateKey), flag).setId(this.idHex)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Gender(val id: Boolean) {
|
||||
MALE(false),
|
||||
FEMALE(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @author NaturalHG
|
||||
*/
|
||||
class ServerLoginResponseSuccessPacket(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 (val flag = readBytes(2).toUHexString()) {
|
||||
"01 07" -> 0
|
||||
"00 33" -> 28
|
||||
"01 10" -> 64
|
||||
else -> throw IllegalStateException(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
|
||||
}
|
||||
|
||||
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
|
||||
fun decrypt(privateKey: ByteArray): ServerLoginResponseSuccessPacket = ServerLoginResponseSuccessPacket(this.decryptBy(TIMProtocol.shareKey, privateKey)).setId(this.idHex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 收到这个包意味着需要验证码登录, 并且能得到验证码图片文件的一部分
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
class ServerLoginResponseVerificationCodeInitPacket(input: ByteReadPacket) : ServerLoginResponsePacket(input) {
|
||||
|
||||
lateinit var verifyCodePart1: IoBuffer
|
||||
lateinit var token00BA: ByteArray
|
||||
var unknownBoolean: Boolean? = null
|
||||
|
||||
|
||||
@Tested
|
||||
override fun decode() {
|
||||
this.input.discardExact(78)
|
||||
//println(this.input.readRemainingBytes().toUHexString())
|
||||
val verifyCodeLength = this.input.readShort()//2bytes
|
||||
this.verifyCodePart1 = this.input.readIoBuffer(verifyCodeLength)
|
||||
|
||||
this.input.discardExact(1)
|
||||
|
||||
this.unknownBoolean = this.input.readByte().toInt() == 1
|
||||
|
||||
this.input.discardExact(this.input.remaining - 60)
|
||||
this.token00BA = this.input.readBytes(40)
|
||||
}
|
||||
|
||||
|
||||
class Encrypted(input: ByteReadPacket) : ServerPacket(input) {
|
||||
fun decrypt(): ServerLoginResponseVerificationCodeInitPacket = ServerLoginResponseVerificationCodeInitPacket(this.decryptAsByteArray(TIMProtocol.shareKey).toReadPacket()).setId(this.idHex)
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import java.io.DataInputStream
|
||||
|
||||
/**
|
||||
* Congratulations!
|
||||
@ -10,4 +10,4 @@ import java.io.DataInputStream
|
||||
* @author Him188moe
|
||||
*/
|
||||
@PacketId("00 EC")
|
||||
class ServerLoginSuccessPacket(input: DataInputStream) : ServerPacket(input)
|
||||
class ServerLoginSuccessPacket(input: ByteReadPacket) : ServerPacket(input)
|
@ -0,0 +1,116 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.setId
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
@PacketId("08 28 04 34")
|
||||
class ClientSessionRequestPacket(
|
||||
private val qq: Long,
|
||||
private val serverIp: String,
|
||||
private val token38: IoBuffer,
|
||||
private val token88: IoBuffer,
|
||||
private val encryptionKey: IoBuffer
|
||||
) : ClientPacket() {
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(qq)
|
||||
this.writeHex("02 00 00 00 01 2E 01 00 00 68 52 00 30 00 3A")
|
||||
this.writeHex("00 38")
|
||||
this.writeFully(token38)
|
||||
this.encryptAndWrite(encryptionKey) {
|
||||
writeHex("00 07 00 88")
|
||||
writeFully(token88)
|
||||
writeHex("00 0C 00 16 00 02 00 00 00 00 00 00 00 00 00 00")
|
||||
writeIP(serverIp)
|
||||
writeHex("1F 40 00 00 00 00 00 15 00 30 00 01")//fix1
|
||||
writeHex("01 92 A5 D2 59 00 10 54 2D CF 9B 60 BF BB EC 0D D4 81 CE 36 87 DE 35 02 AE 6D ED DC 00 10 ")
|
||||
writeHex("06 A9 12 97 B7 F8 76 25 AF AF D3 EA B4 C8 BC E7")//fix0836
|
||||
writeHex("00 36 00 12 00 02 00 01 00 00 00 05 00 00 00 00 00 00 00 00 00 00")
|
||||
writeHex(TIMProtocol.constantData1)
|
||||
writeHex(TIMProtocol.constantData2)
|
||||
writeQQ(qq)
|
||||
writeHex("00 00 00 00 00 1F 00 22 00 01")
|
||||
writeHex("1A 68 73 66 E4 BA 79 92 CC C2 D4 EC 14 7C 8B AF 43 B0 62 FB 65 58 A9 EB 37 55 1D 26 13 A8 E5 3D")//device ID
|
||||
|
||||
//tlv0106
|
||||
writeHex("01 05 00 30")
|
||||
writeHex("00 01 01 02 00 14 01 01 00 10")
|
||||
writeRandom(16)
|
||||
writeHex("00 14 01 02 00 10")
|
||||
writeRandom(16)
|
||||
|
||||
writeHex("01 0B 00 85 00 02")
|
||||
writeHex("B9 ED EF D7 CD E5 47 96 7A B5 28 34 CA 93 6B 5C")//fix2
|
||||
writeRandom(1)
|
||||
writeHex("10 00 00 00 00 00 00 00 02")
|
||||
|
||||
//fix3
|
||||
writeHex("00 63 3E 00 63 02 04 03 06 02 00 04 00 52 D9 00 00 00 00 A9 58 3E 6D 6D 49 AA F6 A6 D9 33 0A E7 7E 36 84 03 01 00 00 68 20 15 8B 00 00 01 02 00 00 03 00 07 DF 00 0A 00 0C 00 01 00 04 00 03 00 04 20 5C 00")
|
||||
writeRandom(32)//md5 32
|
||||
writeHex("68")
|
||||
|
||||
writeHex("00 00 00 00 00 2D 00 06 00 01")
|
||||
writeIP(localIpAddress())//todo random to avoid being banned?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@PacketId("08 28 04 34")
|
||||
class ServerSessionKeyResponsePacket(inputStream: ByteReadPacket) : ServerPacket(inputStream) {
|
||||
lateinit var sessionKey: ByteArray
|
||||
lateinit var tlv0105: ByteReadPacket
|
||||
|
||||
@Tested
|
||||
override fun decode() = with(input) {
|
||||
when (val dataLength = remaining) {
|
||||
407L -> {
|
||||
input.discardExact(25)//todo test
|
||||
sessionKey = input.readBytes(16)
|
||||
}
|
||||
|
||||
439L -> {
|
||||
input.discardExact(63)
|
||||
sessionKey = input.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)
|
||||
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))
|
||||
writeHex("00 40 02 02 03 3C 01 03 00 00")
|
||||
input.discardExact(11)
|
||||
writeFully(input.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
|
||||
*/
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException(dataLength.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)
|
||||
|
||||
}
|
||||
|
||||
class Encrypted(inputStream: ByteReadPacket) : ServerPacket(inputStream) {
|
||||
fun decrypt(sessionResponseDecryptionKey: IoBuffer): ServerSessionKeyResponsePacket =
|
||||
ServerSessionKeyResponsePacket(this.decryptBy(sessionResponseDecryptionKey)).setId(this.idHex)
|
||||
}
|
||||
}
|
@ -1,11 +1,15 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.utils.hexToBytes
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
import java.io.DataInputStream
|
||||
import java.io.IOException
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.setId
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
/**
|
||||
* The packet received when logging in, used to redirect server address
|
||||
@ -17,7 +21,7 @@ import java.io.IOException
|
||||
*/
|
||||
@Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
@PacketId("08 25 31 01")
|
||||
class ServerTouchResponsePacket(inputStream: DataInputStream) : ServerPacket(inputStream) {
|
||||
class ServerTouchResponsePacket(inputStream: ByteReadPacket) : ServerPacket(inputStream) {
|
||||
var serverIP: String? = null
|
||||
|
||||
var loginTime: Int = 0
|
||||
@ -30,28 +34,28 @@ class ServerTouchResponsePacket(inputStream: DataInputStream) : ServerPacket(inp
|
||||
}
|
||||
|
||||
|
||||
override fun decode() {
|
||||
when (val id = input.readByte().toUByte().toInt()) {
|
||||
0xFE -> {
|
||||
input.skip(94)
|
||||
serverIP = input.readIP()
|
||||
override fun decode() = with(input) {
|
||||
when (val id = readByte().toUByte().toInt()) {
|
||||
0xFE -> {//todo 在 packet 解析时分类而不是在这里分类
|
||||
discardExact(94)
|
||||
serverIP = readIP()
|
||||
}
|
||||
0x00 -> {
|
||||
input.skip(4)
|
||||
token0825 = input.readNBytes(56)
|
||||
input.skip(6)
|
||||
discardExact(4)
|
||||
token0825 = readBytes(56)
|
||||
discardExact(6)
|
||||
|
||||
loginTime = input.readInt()
|
||||
loginIP = input.readIP()
|
||||
loginTime = readInt()
|
||||
loginIP = readIP()
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw IllegalStateException(arrayOf(id.toUByte()).toUByteArray().toUHexString())
|
||||
throw IllegalStateException(id.toByte().toUHexString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Encrypted(private val type: Type, inputStream: DataInputStream) : ServerPacket(inputStream) {
|
||||
class Encrypted(private val type: Type, inputStream: ByteReadPacket) : ServerPacket(inputStream) {
|
||||
|
||||
fun decrypt(): ServerTouchResponsePacket = ServerTouchResponsePacket(decryptBy(when (type) {
|
||||
Type.TYPE_08_25_31_02 -> TIMProtocol.redirectionKey.hexToBytes()
|
||||
@ -61,16 +65,13 @@ class ServerTouchResponsePacket(inputStream: DataInputStream) : ServerPacket(inp
|
||||
}
|
||||
|
||||
/**
|
||||
* The packet to touch server, that is, to start the connection to the server.
|
||||
* The packet to sendTouch server, that is, to start the connection to the server.
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
@PacketId("08 25 31 01")
|
||||
class ClientTouchPacket(private val qq: Long, private val serverIp: String) : ClientPacket() {
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun encode() {
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(TIMProtocol.fixVer)
|
||||
this.writeHex(TIMProtocol.touchKey)
|
||||
@ -94,13 +95,11 @@ class ClientTouchPacket(private val qq: Long, private val serverIp: String) : Cl
|
||||
*/
|
||||
@PacketId("08 25 31 02")
|
||||
class ClientServerRedirectionPacket(private val serverIP: String, private val qq: Long) : ClientPacket() {
|
||||
|
||||
override fun encode() {
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(TIMProtocol.fixVer)
|
||||
this.writeHex(TIMProtocol.redirectionKey)
|
||||
|
||||
|
||||
this.encryptAndWrite(TIMProtocol.redirectionKey) {
|
||||
this.writeHex(TIMProtocol.constantData1)
|
||||
this.writeHex(TIMProtocol.constantData2)
|
@ -1,25 +1,32 @@
|
||||
@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.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.utils.Tested
|
||||
import java.io.DataInputStream
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.setId
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
/**
|
||||
* 客户端请求验证码图片数据的第几部分
|
||||
*/
|
||||
@PacketId("00 BA 31")
|
||||
class ClientVerificationCodeTransmissionRequestPacket(
|
||||
private val packetId: Int,
|
||||
private val packetIdLast: Int,//ubyte
|
||||
private val qq: Long,
|
||||
private val token0825: ByteArray,
|
||||
private val verificationSequence: Int,
|
||||
private val token00BA: ByteArray
|
||||
) : ClientPacket() {
|
||||
@Tested
|
||||
override fun encode() {
|
||||
this.writeByte(packetId)//part of packet id
|
||||
override val idHex: String by lazy {
|
||||
super.idHex + " " + packetIdLast.toByte().toUHexString()
|
||||
}
|
||||
|
||||
@Tested
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(TIMProtocol.fixVer)
|
||||
this.writeHex(TIMProtocol.key00BA)
|
||||
@ -27,13 +34,13 @@ class ClientVerificationCodeTransmissionRequestPacket(
|
||||
writeHex("00 02 00 00 08 04 01 E0")
|
||||
writeHex(TIMProtocol.constantData2)
|
||||
writeHex("00 00 38")
|
||||
write(token0825)
|
||||
writeFully(token0825)
|
||||
writeHex("01 03 00 19")
|
||||
writeHex(TIMProtocol.publicKey)
|
||||
writeHex("13 00 05 00 00 00 00")
|
||||
writeByte(verificationSequence)
|
||||
writeUByte(verificationSequence.toUByte())
|
||||
writeHex("00 28")
|
||||
write(token00BA)
|
||||
writeFully(token00BA)
|
||||
writeHex("00 10")
|
||||
writeHex(TIMProtocol.key00BAFix)
|
||||
}
|
||||
@ -45,19 +52,21 @@ class ClientVerificationCodeTransmissionRequestPacket(
|
||||
*/
|
||||
@PacketId("00 BA 32")
|
||||
class ClientVerificationCodeSubmitPacket(
|
||||
private val packetIdLast: Int,
|
||||
private val packetIdLast: Int,//ubyte
|
||||
private val qq: Long,
|
||||
private val token0825: ByteArray,
|
||||
private val captcha: String,
|
||||
private val verificationToken: ByteArray
|
||||
private val verificationToken: IoBuffer
|
||||
) : ClientPacket() {
|
||||
init {
|
||||
require(captcha.length == 4) { "captcha.length must == 4" }
|
||||
}
|
||||
|
||||
override fun encode() {
|
||||
this.writeByte(packetIdLast)//part of packet id
|
||||
override val idHex: String by lazy {
|
||||
super.idHex + " " + packetIdLast.toByte().toUHexString()
|
||||
}
|
||||
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(TIMProtocol.fixVer)
|
||||
this.writeHex(TIMProtocol.key00BA)
|
||||
@ -65,25 +74,21 @@ class ClientVerificationCodeSubmitPacket(
|
||||
writeHex("00 02 00 00 08 04 01 E0")
|
||||
writeHex(TIMProtocol.constantData2)
|
||||
writeHex("01 00 38")
|
||||
write(token0825)
|
||||
writeFully(token0825)
|
||||
writeHex("01 03")
|
||||
|
||||
writeShort(25)
|
||||
writeHex(TIMProtocol.publicKey)//25
|
||||
|
||||
writeHex("14 00 05 00 00 00 00 00 04")
|
||||
write(captcha.toUpperCase().toByteArray())
|
||||
writeStringUtf8(captcha.toUpperCase())
|
||||
writeHex("00 38")
|
||||
write(verificationToken)
|
||||
writeFully(verificationToken)
|
||||
|
||||
writeShort(16)
|
||||
writeHex(TIMProtocol.key00BAFix)//16
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFixedId(): String {
|
||||
return this.idHex + " " + packetIdLast
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,9 +100,11 @@ class ClientVerificationCodeRefreshPacket(
|
||||
private val qq: Long,
|
||||
private val token0825: ByteArray
|
||||
) : ClientPacket() {
|
||||
override fun encode() {
|
||||
this.writeByte(packetIdLast)//part of packet id
|
||||
override val idHex: String by lazy {
|
||||
super.idHex + " " + packetIdLast.toByte().toUHexString()
|
||||
}
|
||||
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeQQ(qq)
|
||||
this.writeHex(TIMProtocol.fixVer)
|
||||
this.writeHex(TIMProtocol.key00BA)
|
||||
@ -105,24 +112,20 @@ class ClientVerificationCodeRefreshPacket(
|
||||
writeHex("00 02 00 00 08 04 01 E0")
|
||||
writeHex(TIMProtocol.constantData2)
|
||||
writeHex("00 00 38")
|
||||
write(token0825)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFixedId(): String {
|
||||
return this.idHex + " " + packetIdLast
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码输入错误, 同时也会给一部分验证码
|
||||
*/
|
||||
@PacketId("00 BA 32")
|
||||
class ServerCaptchaWrongPacket(input: DataInputStream, dataSize: Int, packetId: ByteArray) : ServerCaptchaTransmissionPacket(input, dataSize, packetId)
|
||||
class ServerCaptchaWrongPacket(input: ByteReadPacket, packetIdLast: Int) : ServerCaptchaTransmissionPacket(input, packetIdLast)
|
||||
|
||||
/**
|
||||
* 服务器发送验证码图片文件一部分过来
|
||||
@ -130,38 +133,34 @@ class ServerCaptchaWrongPacket(input: DataInputStream, dataSize: Int, packetId:
|
||||
* @author Him188moe
|
||||
*/
|
||||
@PacketId("00 BA 31")
|
||||
open class ServerCaptchaTransmissionPacket(input: DataInputStream, private val dataSize: Int, private val packetId: ByteArray) : ServerCaptchaPacket(input) {
|
||||
open class ServerCaptchaTransmissionPacket(input: ByteReadPacket, val packetIdLast: Int) : ServerCaptchaPacket(input) {
|
||||
|
||||
lateinit var captchaSectionN: ByteArray
|
||||
lateinit var verificationToken: ByteArray//56bytes
|
||||
lateinit var captchaSectionN: IoBuffer
|
||||
lateinit var verificationToken: IoBuffer//56bytes
|
||||
var transmissionCompleted: Boolean = false//验证码是否已经传输完成
|
||||
lateinit var token00BA: ByteArray//40 bytes
|
||||
var packetIdLast: Int = 0
|
||||
|
||||
|
||||
override fun decode() {
|
||||
this.verificationToken = this.input.readNBytesAt(10, 56)
|
||||
override fun decode() = with(input) {
|
||||
input.discardExact(10)//13 00 05 01 00 00 01 23 00 38
|
||||
verificationToken = readIoBuffer(56)
|
||||
|
||||
val length = this.input.readShortAt(66)
|
||||
this.captchaSectionN = this.input.readNBytes(length)
|
||||
val length = readShort()
|
||||
captchaSectionN = readIoBuffer(length)
|
||||
|
||||
this.input.skip(1)
|
||||
val byte = this.input.readByteAt(69 + length).toInt()
|
||||
this.transmissionCompleted = byte == 0
|
||||
discardExact(1)
|
||||
val byte = readByte().toInt()
|
||||
transmissionCompleted = byte == 0
|
||||
|
||||
this.token00BA = this.input.readNBytesAt(dataSize - 56 - 2, 40)
|
||||
this.packetIdLast = packetId[3].toInt()
|
||||
}
|
||||
|
||||
override fun getFixedId(): String {
|
||||
return this.idHex + " " + packetIdLast
|
||||
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()
|
||||
ServerVerificationCodeTransmissionPacket(data.dataInputStream(), data.size, "00 BA 31 01".hexToBytes()).let {
|
||||
ServerVerificationCodeTransmissionPacket(data.toReadPacket(), data.size, "00 BA 31 01".hexToBytes()).let {
|
||||
it.dataDecode()
|
||||
println(it.toString())
|
||||
}
|
||||
@ -173,35 +172,31 @@ fun main() {
|
||||
* @author Him188moe
|
||||
*/
|
||||
@PacketId("00 BA 32")
|
||||
class ServerCaptchaCorrectPacket(input: DataInputStream) : ServerCaptchaPacket(input) {
|
||||
|
||||
class ServerCaptchaCorrectPacket(input: ByteReadPacket) : ServerCaptchaPacket(input) {
|
||||
lateinit var token00BA: ByteArray//56 bytes
|
||||
|
||||
|
||||
override fun decode() {
|
||||
token00BA = this.input.readNBytesAt(10, 56)
|
||||
override fun decode() = with(input) {
|
||||
discardExact(10)//14 00 05 00 00 00 00 00 00 38
|
||||
token00BA = readBytes(56)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ServerCaptchaPacket(input: DataInputStream) : ServerPacket(input) {
|
||||
abstract class ServerCaptchaPacket(input: ByteReadPacket) : ServerPacket(input) {
|
||||
|
||||
@PacketId("00 BA")
|
||||
class Encrypted(input: DataInputStream, private val id: String) : ServerPacket(input) {
|
||||
|
||||
class Encrypted(input: ByteReadPacket, override var idHex: String) : ServerPacket(input) {
|
||||
fun decrypt(): ServerCaptchaPacket {
|
||||
val data = this.decryptAsByteArray(TIMProtocol.key00BA)
|
||||
if (id.startsWith("00 BA 32")) {
|
||||
if (idHex.startsWith("00 BA 32")) {
|
||||
return when (data.size) {
|
||||
66,
|
||||
95 -> ServerCaptchaCorrectPacket(data.dataInputStream())
|
||||
//66 -> ServerVerificationCodeUnknownPacket(data.dataInputStream())
|
||||
else -> return ServerCaptchaWrongPacket(data.dataInputStream(), data.size, this.input.readNBytesAt(3, 4))
|
||||
95 -> ServerCaptchaCorrectPacket(data.toReadPacket())
|
||||
//66 -> ServerVerificationCodeUnknownPacket(data.toReadPacket())
|
||||
else -> return ServerCaptchaWrongPacket(data.toReadPacket(), idHex.substringAfterLast(" ").toInt(16))
|
||||
}.setId(this.idHex)
|
||||
}
|
||||
|
||||
return ServerCaptchaTransmissionPacket(data.dataInputStream(), data.size, this.input.readNBytesAt(3, 4)).setId(this.idHex)
|
||||
return ServerCaptchaTransmissionPacket(data.toReadPacket(), idHex.substringAfterLast(" ").toInt(16)).setId(this.idHex)
|
||||
}
|
||||
|
||||
override fun getFixedId(): String = this.getFixedId(id)
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
data class BotAccount(
|
||||
val qqNumber: Long,//实际上是 UInt
|
||||
val password: String//todo 不保存 password?
|
@ -0,0 +1,37 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.String
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
@JvmOverloads
|
||||
fun ByteArray.toHexString(separator: String = " "): String = this.joinToString(separator) {
|
||||
var ret = it.toString(16).toUpperCase()
|
||||
if (ret.length == 1) {
|
||||
ret = "0$ret"
|
||||
}
|
||||
return@joinToString ret
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun ByteArray.toUHexString(separator: String = " "): String = this.toUByteArray().toUHexString(separator)
|
||||
|
||||
fun ByteArray.stringOf(): String = String(this)
|
||||
|
||||
//@JvmSynthetic TODO 等待 kotlin 修复 bug 后添加这个注解
|
||||
@JvmOverloads
|
||||
fun UByteArray.toUHexString(separator: String = " "): String = this.joinToString(separator) {
|
||||
var ret = it.toString(16).toUpperCase()
|
||||
if (ret.length == 1) {
|
||||
ret = "0$ret"
|
||||
}
|
||||
return@joinToString ret
|
||||
}
|
||||
|
||||
fun ByteArray.toReadPacket() = ByteReadPacket(this)
|
||||
|
||||
fun <R> ByteArray.read(t: ByteReadPacket.() -> R): R = this.toReadPacket().run(t)
|
||||
|
||||
fun ByteArray.cutTail(length: Int): ByteArray = this.copyOfRange(0, this.size - length)
|
@ -0,0 +1,189 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
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.action.ServerCanAddFriendResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ServerSendFriendMessageResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ServerSendGroupMessageResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.*
|
||||
|
||||
|
||||
fun ByteReadPacket.readRemainingBytes(
|
||||
n: Int = remaining.toInt()//not that safe but adequate
|
||||
): ByteArray = ByteArray(n).also { readAvailable(it, 0, n) }
|
||||
|
||||
fun ByteReadPacket.readIoBuffer(
|
||||
n: Int = remaining.toInt()//not that safe but adequate
|
||||
): IoBuffer = IoBuffer.Pool.borrow().also { this.readFully(it, n) }
|
||||
|
||||
fun ByteReadPacket.readIoBuffer(n: Number) = this.readIoBuffer(n.toInt())
|
||||
|
||||
//必须消耗完 packet
|
||||
fun ByteReadPacket.parseServerPacket(size: Int): ServerPacket {//TODO 优化
|
||||
discardExact(3)
|
||||
|
||||
val idHex = readInt().toUHexString(" ")
|
||||
|
||||
discardExact(7)//4 for qq number, 3 for 0x00 0x00 0x00. 但更可能是应该 discard 8
|
||||
return when (idHex) {
|
||||
"08 25 31 01" -> ServerTouchResponsePacket.Encrypted(ServerTouchResponsePacket.Type.TYPE_08_25_31_01, this)
|
||||
"08 25 31 02" -> ServerTouchResponsePacket.Encrypted(ServerTouchResponsePacket.Type.TYPE_08_25_31_02, this)
|
||||
|
||||
"08 36 31 03", "08 36 31 04", "08 36 31 05", "08 36 31 06" -> {
|
||||
when (size) {
|
||||
271, 207 -> return ServerLoginResponseKeyExchangePacket.Encrypted(this, when (idHex) {
|
||||
"08 36 31 03" -> ServerLoginResponseKeyExchangePacket.Flag.`08 36 31 03`
|
||||
else -> ServerLoginResponseKeyExchangePacket.Flag.OTHER
|
||||
}).setId(idHex)
|
||||
871 -> return ServerLoginResponseVerificationCodeInitPacket.Encrypted(this).setId(idHex)
|
||||
}
|
||||
|
||||
if (size > 700) {
|
||||
return ServerLoginResponseSuccessPacket.Encrypted(this).setId(idHex)
|
||||
}
|
||||
|
||||
println(size)
|
||||
return ServerLoginResponseFailedPacket(when (size) {
|
||||
135 -> {//包数据错误. 目前怀疑是 tlv0006
|
||||
this.readRemainingBytes().cutTail(1).decryptBy(TIMProtocol.shareKey).read {
|
||||
discardExact(51)
|
||||
MiraiLogger.logError("Internal logError: " + readLVString())//抱歉,请重新输入密码。
|
||||
}
|
||||
|
||||
LoginResult.INTERNAL_ERROR
|
||||
} //可能是包数据错了. 账号没有被ban, 用TIM官方可以登录
|
||||
|
||||
319, 351 -> LoginResult.WRONG_PASSWORD
|
||||
//135 -> LoginState.RETYPE_PASSWORD
|
||||
63, 279 -> LoginResult.BLOCKED
|
||||
263 -> LoginResult.UNKNOWN_QQ_NUMBER
|
||||
551, 487 -> LoginResult.DEVICE_LOCK
|
||||
343, 359 -> LoginResult.TAKEN_BACK
|
||||
|
||||
else -> LoginResult.UNKNOWN
|
||||
/*
|
||||
//unknown
|
||||
63 -> throw IllegalArgumentException(bytes.size.toString() + " (Unknown logError)")
|
||||
351 -> throw IllegalArgumentException(bytes.size.toString() + " (Unknown logError)")
|
||||
|
||||
else -> throw IllegalArgumentException(bytes.size.toString())*/
|
||||
}, this).setId(idHex)
|
||||
}
|
||||
|
||||
"08 28 04 34" -> ServerSessionKeyResponsePacket.Encrypted(this)
|
||||
|
||||
|
||||
else -> when (idHex.substring(0, 5)) {
|
||||
"00 EC" -> ServerLoginSuccessPacket(this)
|
||||
"00 1D" -> ServerSKeyResponsePacket.Encrypted(this)
|
||||
"00 5C" -> ServerAccountInfoResponsePacket.Encrypted(this)
|
||||
|
||||
"00 58" -> ServerHeartbeatResponsePacket(this)
|
||||
|
||||
"00 BA" -> ServerCaptchaPacket.Encrypted(this, idHex)
|
||||
|
||||
|
||||
"00 CE", "00 17" -> ServerEventPacket.Raw.Encrypted(this, idHex.hexToBytes())
|
||||
|
||||
"00 81" -> ServerFieldOnlineStatusChangedPacket.Encrypted(this)
|
||||
|
||||
"00 CD" -> ServerSendFriendMessageResponsePacket(this)
|
||||
"00 02" -> ServerSendGroupMessageResponsePacket(this)
|
||||
|
||||
"00 A7" -> ServerCanAddFriendResponsePacket(this)
|
||||
|
||||
"03 88" -> ServerTryGetImageIDResponsePacket.Encrypted(this)
|
||||
|
||||
else -> UnknownServerPacket.Encrypted(this)
|
||||
}
|
||||
}.setId(idHex)
|
||||
}
|
||||
|
||||
fun ByteReadPacket.readIP(): String = buildString(4 + 3) {
|
||||
repeat(4) {
|
||||
val byte = readUByte()
|
||||
this.append(byte.toString())
|
||||
if (it != 3) this.append(".")
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteReadPacket.readLVString(): String = String(this.readLVByteArray())
|
||||
|
||||
fun ByteReadPacket.readLVByteArray(): ByteArray = this.readBytes(this.readShort().toInt())
|
||||
|
||||
fun ByteReadPacket.readTLVMap(expectingEOF: Boolean = false): Map<Int, ByteArray> {
|
||||
val map = mutableMapOf<Int, ByteArray>()
|
||||
var type: UByte
|
||||
|
||||
try {
|
||||
type = readUByte()
|
||||
} catch (e: EOFException) {
|
||||
if (expectingEOF) {
|
||||
return map
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
while (type != UByte.MAX_VALUE) {
|
||||
map[type.toInt()] = this.readLVByteArray()
|
||||
|
||||
try {
|
||||
type = readUByte()
|
||||
} catch (e: EOFException) {
|
||||
if (expectingEOF) {
|
||||
return map
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
fun Map<Int, ByteArray>.printTLVMap(name: String) = debugPrintln("TLVMap $name= " + this.mapValues { (_, value) -> value.toUHexString() })
|
||||
|
||||
fun ByteReadPacket.readString(length: Number): String = String(this.readBytes(length.toInt()))
|
||||
|
||||
private const val TRUE_BYTE_VALUE: Byte = 1
|
||||
fun ByteReadPacket.readBoolean(): Boolean = this.readByte() == TRUE_BYTE_VALUE
|
||||
fun ByteReadPacket.readLVNumber(): Number {
|
||||
return when (this.readShort().toInt()) {
|
||||
1 -> this.readByte()
|
||||
2 -> this.readShort()
|
||||
4 -> this.readInt()
|
||||
8 -> this.readLong()
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
//添加@JvmSynthetic 导致 idea 无法检查这个文件的错误
|
||||
//@JvmSynthetic
|
||||
@Deprecated("Low efficiency", ReplaceWith(""))
|
||||
fun ByteReadPacket.gotoWhere(matcher: UByteArray): ByteReadPacket {
|
||||
return this.gotoWhere(matcher.toByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* 去往下一个含这些连续字节的位置
|
||||
*/
|
||||
@Deprecated("Low efficiency", ReplaceWith(""))
|
||||
fun ByteReadPacket.gotoWhere(matcher: ByteArray): ByteReadPacket {
|
||||
require(matcher.isNotEmpty())
|
||||
|
||||
loop@
|
||||
do {
|
||||
val byte = this.readByte()
|
||||
if (byte == matcher[0]) {
|
||||
//todo mark here
|
||||
for (i in 1 until matcher.size) {
|
||||
val b = this.readByte()
|
||||
if (b != matcher[i]) {
|
||||
continue@loop //todo goto mark
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
} while (true)
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.toByteArray
|
||||
import kotlinx.io.core.writeFully
|
||||
|
||||
private const val GTK_BASE_VALUE: Int = 5381
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
internal fun getGTK(sKey: String): Int {
|
||||
var value = GTK_BASE_VALUE
|
||||
for (c in sKey.toCharArray()) {
|
||||
value += (value shl 5) + c.toInt()
|
||||
}
|
||||
|
||||
value = value and Int.MAX_VALUE
|
||||
return value
|
||||
}
|
||||
|
||||
@Tested
|
||||
fun BytePacketBuilder.writeCRC32() = writeCRC32(getRandomByteArray(16))
|
||||
|
||||
fun BytePacketBuilder.writeCRC32(key: ByteArray) {
|
||||
writeFully(key)//key
|
||||
writeInt(crc32(key))
|
||||
}
|
||||
|
||||
fun md5(str: String): ByteArray = md5(str.toByteArray())
|
@ -0,0 +1,10 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.core.IoBuffer
|
||||
|
||||
/**
|
||||
* 让用户处理验证码
|
||||
*
|
||||
* @return 用户输入得到的验证码. 非 null 时一定 `length==4`.
|
||||
*/
|
||||
internal expect suspend fun solveCaptcha(captchaBuffer: IoBuffer): String?
|
@ -2,7 +2,5 @@ package net.mamoe.mirai.utils
|
||||
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
class ContactList<C : Contact> : MutableMap<Long, C> by mutableMapOf()
|
@ -0,0 +1,28 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.core.Input
|
||||
import kotlinx.io.core.IoBuffer
|
||||
import kotlinx.io.core.readBytes
|
||||
|
||||
|
||||
internal object DebugLogger : MiraiLogger by PlatformLogger("Packet Debug")
|
||||
|
||||
internal fun debugPrintln(any: Any?) = DebugLogger.logPurple(any)
|
||||
|
||||
@Deprecated("Debugging Warning", ReplaceWith(""))
|
||||
internal fun ByteArray.debugPrint(name: String): ByteArray {
|
||||
DebugLogger.logPurple(name + "=" + this.toUHexString())
|
||||
return this
|
||||
}
|
||||
|
||||
@Deprecated("Debugging Warning", ReplaceWith(""))
|
||||
internal fun IoBuffer.debugPrint(name: String): IoBuffer {
|
||||
val readBytes = this.readBytes()
|
||||
DebugLogger.logPurple(name + "=" + readBytes.toUHexString())
|
||||
return readBytes.toIoBuffer()
|
||||
}
|
||||
|
||||
@Deprecated("Debugging Warning", ReplaceWith(""))
|
||||
internal fun Input.debugDiscardExact(n: Number, name: String = "") {
|
||||
DebugLogger.logPurple("Discarded($n) $name=" + this.readBytes(n.toInt()).toUHexString())
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
|
||||
inline fun <T> MutableIterable<T>.removeIfInlined(predicate: (T) -> Boolean) = iterator().removeIfInlined(predicate)
|
||||
|
||||
inline fun <T> MutableIterator<T>.removeIfInlined(predicate: (T) -> Boolean) {
|
||||
while (this.hasNext()) {
|
||||
if (predicate(this.next())) {
|
||||
this.remove()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.ServerTouchResponsePacket
|
||||
|
||||
class LoginConfiguration {
|
||||
/**
|
||||
* 等待 [ServerTouchResponsePacket] 的时间
|
||||
*/
|
||||
var touchTimeoutMillis: Long = 2000
|
||||
|
||||
var randomDeviceName: Boolean = false
|
||||
|
||||
companion object {
|
||||
val Default = LoginConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
interface MiraiLogger {
|
||||
companion object : MiraiLogger by PlatformLogger("[TOP Level]")
|
||||
|
||||
var identity: String?
|
||||
|
||||
fun logInfo(any: Any?) = log(any)
|
||||
|
||||
fun log(e: Throwable)
|
||||
|
||||
fun log(any: Any?)
|
||||
|
||||
fun logError(any: Any?)
|
||||
|
||||
fun logDebug(any: Any?)
|
||||
|
||||
fun logCyan(any: Any?)
|
||||
|
||||
fun logPurple(any: Any?)
|
||||
|
||||
fun logGreen(any: Any?)
|
||||
|
||||
fun logBlue(any: Any?)
|
||||
}
|
||||
|
||||
expect class PlatformLogger @JvmOverloads constructor(identity: String? = null) : MiraiLogger
|
||||
|
||||
fun Throwable.log() = MiraiLogger.log(this)
|
||||
fun Throwable.printStacktrace() = this.log()
|
@ -1,4 +1,5 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
/**
|
||||
@ -7,12 +8,21 @@ package net.mamoe.mirai.utils
|
||||
* @author Him188moe
|
||||
* @see net.mamoe.mirai.network.protocol.tim.packet.login.ClientChangeOnlineStatusPacket
|
||||
*/
|
||||
enum class ClientLoginStatus(
|
||||
// TODO: 2019/8/31 add more ClientLoginStatus
|
||||
enum class OnlineStatus(
|
||||
val id: UByte//1 ubyte
|
||||
) {
|
||||
/**
|
||||
* 我在线上
|
||||
*/
|
||||
ONLINE(0x0Au)
|
||||
ONLINE(0x0Au),
|
||||
|
||||
/**
|
||||
* 忙碌
|
||||
*/
|
||||
BUSY(0x32u);
|
||||
|
||||
|
||||
companion object {
|
||||
fun ofId(id: UByte): OnlineStatus = values().first { it.id == id }
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
|
||||
|
||||
fun BytePacketBuilder.writeZero(count: Int) = repeat(count) { this.writeByte(0) }
|
||||
|
||||
fun BytePacketBuilder.writeRandom(length: Int) = repeat(length) { this.writeByte(Random.Default.nextInt(255).toByte()) }
|
||||
|
||||
fun BytePacketBuilder.writeQQ(qq: Long) = this.writeUInt(qq.toUInt())
|
||||
|
||||
fun BytePacketBuilder.writeGroup(groupIdOrGroupNumber: Long) = this.writeFully(groupIdOrGroupNumber.toUInt().toByteArray())
|
||||
|
||||
fun BytePacketBuilder.writeLVByteArray(byteArray: ByteArray) {
|
||||
this.writeShort(byteArray.size.toShort())
|
||||
this.writeFully(byteArray)
|
||||
}
|
||||
|
||||
fun BytePacketBuilder.writeLVPacket(packet: ByteReadPacket) {
|
||||
this.writeShort(packet.remaining.toShort())
|
||||
this.writePacket(packet)
|
||||
}
|
||||
|
||||
fun BytePacketBuilder.writeLVPacket(builder: BytePacketBuilder.() -> Unit) = this.writeLVPacket(BytePacketBuilder().apply(builder).use { it.build() })
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun BytePacketBuilder.writeLVString(str: String) = this.writeLVByteArray(str.toByteArray())
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun BytePacketBuilder.writeLVHex(hex: String) = this.writeLVByteArray(hex.hexToBytes())
|
||||
|
||||
fun BytePacketBuilder.writeIP(ip: String) = writeFully(ip.trim().split(".").map { it.toUByte() }.toUByteArray())
|
||||
|
||||
fun BytePacketBuilder.writeTime() = this.writeInt(currentTime.toInt())
|
||||
|
||||
fun BytePacketBuilder.writeHex(uHex: String) = this.writeFully(uHex.hexToUBytes())
|
||||
|
||||
fun BytePacketBuilder.encryptAndWrite(key: IoBuffer, encoder: BytePacketBuilder.() -> Unit) = encryptAndWrite(key.readBytes(), encoder)
|
||||
fun BytePacketBuilder.encryptAndWrite(key: ByteArray, encoder: BytePacketBuilder.() -> Unit) = writeFully(TEA.encrypt(BytePacketBuilder().apply(encoder).use { it.build().readBytes() }, key))
|
||||
fun BytePacketBuilder.encryptAndWrite(keyHex: String, encoder: BytePacketBuilder.() -> Unit) = encryptAndWrite(keyHex.hexToBytes(), encoder)
|
||||
|
||||
fun BytePacketBuilder.writeTLV0006(qq: Long, password: String, loginTime: Int, loginIP: String, privateKey: ByteArray) {
|
||||
val firstMD5 = md5(password)
|
||||
val secondMD5 = md5(firstMD5 + byteArrayOf(0, 0, 0, 0) + qq.toUInt().toByteArray())
|
||||
|
||||
this.encryptAndWrite(secondMD5) {
|
||||
writeRandom(4)
|
||||
writeHex("00 02")
|
||||
writeQQ(qq)
|
||||
writeHex(TIMProtocol.constantData2)
|
||||
writeHex("00 00 01")
|
||||
|
||||
writeFully(firstMD5)
|
||||
writeInt(loginTime)
|
||||
writeByte(0)
|
||||
writeZero(4 * 3)
|
||||
writeIP(loginIP)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Tested
|
||||
fun BytePacketBuilder.writeDeviceName(random: Boolean) {
|
||||
val deviceName: String = if (random) {
|
||||
"DESKTOP-" + String(ByteArray(7) {
|
||||
(if (Random.nextBoolean()) Random.nextInt('A'.toInt()..'Z'.toInt())
|
||||
else Random.nextInt('1'.toInt()..'9'.toInt())).toByte()
|
||||
})
|
||||
} else {
|
||||
deviceName
|
||||
}
|
||||
this.writeShort((deviceName.length + 2).toShort())
|
||||
this.writeShort(deviceName.length.toShort())
|
||||
this.writeStringUtf8(deviceName)//TODO TEST?
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
expect val currentTime: Long
|
||||
|
||||
/**
|
||||
* 设备名
|
||||
*/
|
||||
expect val deviceName: String
|
||||
|
||||
|
||||
/**
|
||||
* CRC32 算法
|
||||
*/
|
||||
expect fun crc32(key: ByteArray): Int
|
||||
|
||||
/**
|
||||
* MD5 算法
|
||||
*
|
||||
* @return 16 bytes
|
||||
*/
|
||||
expect fun md5(byteArray: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* Hostname 解析 IP 地址
|
||||
*/
|
||||
expect fun solveIpAddress(hostname: String): String
|
||||
|
||||
/**
|
||||
* Localhost 解析
|
||||
*/
|
||||
expect fun localIpAddress(): String
|
@ -0,0 +1,14 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.core.Closeable
|
||||
import kotlinx.io.core.IoBuffer
|
||||
import kotlinx.io.errors.IOException
|
||||
|
||||
expect class MiraiDatagramChannel(serverHost: String, serverPort: Short) : Closeable {
|
||||
suspend fun read(buffer: IoBuffer): Int
|
||||
suspend fun send(buffer: IoBuffer): Int
|
||||
|
||||
val isOpen: Boolean
|
||||
}
|
||||
|
||||
expect class ClosedChannelException : IOException
|
@ -0,0 +1,25 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.core.IoBuffer
|
||||
import kotlin.jvm.JvmStatic
|
||||
|
||||
|
||||
expect object TEA {
|
||||
internal fun doOption(data: ByteArray, key: ByteArray, encrypt: Boolean): ByteArray
|
||||
|
||||
@JvmStatic
|
||||
fun encrypt(source: ByteArray, key: ByteArray): ByteArray
|
||||
|
||||
@JvmStatic
|
||||
fun decrypt(source: ByteArray, key: ByteArray): ByteArray
|
||||
|
||||
@JvmStatic
|
||||
fun decrypt(source: ByteArray, key: IoBuffer): ByteArray
|
||||
|
||||
@JvmStatic
|
||||
fun decrypt(source: ByteArray, keyHex: String): ByteArray
|
||||
}
|
||||
|
||||
fun ByteArray.decryptBy(key: ByteArray): ByteArray = TEA.decrypt(this, key)
|
||||
fun ByteArray.decryptBy(key: IoBuffer): ByteArray = TEA.decrypt(this, key)
|
||||
fun ByteArray.decryptBy(key: String): ByteArray = TEA.decrypt(this, key)
|
@ -0,0 +1,62 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.core.IoBuffer
|
||||
import kotlinx.io.core.writeFully
|
||||
import kotlin.jvm.Synchronized
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
|
||||
/**
|
||||
* 255 -> 00 00 00 FF
|
||||
*/
|
||||
fun Int.toByteArray(): ByteArray = byteArrayOf(
|
||||
(this.ushr(24) and 0xFF).toByte(),
|
||||
(this.ushr(16) and 0xFF).toByte(),
|
||||
(this.ushr(8) and 0xFF).toByte(),
|
||||
(this.ushr(0) and 0xFF).toByte()
|
||||
)
|
||||
|
||||
/**
|
||||
* 255u -> 00 00 00 FF
|
||||
*/
|
||||
|
||||
fun UInt.toByteArray(): ByteArray = byteArrayOf(
|
||||
(this.shr(24) and 255u).toByte(),
|
||||
(this.shr(16) and 255u).toByte(),
|
||||
(this.shr(8) and 255u).toByte(),
|
||||
(this.shr(0) and 255u).toByte()
|
||||
)
|
||||
|
||||
fun Int.toUHexString(separator: String = " "): String = this.toByteArray().toUHexString(separator)
|
||||
fun Byte.toUHexString(): String = this.toUByte().toString(16).toUpperCase()
|
||||
fun String.hexToBytes(): ByteArray = HexCache.hexToBytes(this)
|
||||
fun String.hexToUBytes(): UByteArray = HexCache.hexToUBytes(this)
|
||||
fun String.hexToInt(): Int = hexToBytes().toUInt().toInt()
|
||||
fun getRandomByteArray(length: Int): ByteArray = ByteArray(length) { Random.nextInt(0..255).toByte() }
|
||||
fun ByteArray.toUInt(): UInt = this[0].toUInt().and(255u).shl(24) + this[1].toUInt().and(255u).shl(16) + this[2].toUInt().and(255u).shl(8) + this[3].toUInt().and(255u).shl(0)
|
||||
|
||||
fun ByteArray.toIoBuffer(): IoBuffer = IoBuffer.Pool.borrow().let { it.writeFully(this); it }
|
||||
|
||||
internal object HexCache {
|
||||
private val hexToByteArrayCacheMap: MutableMap<Int, ByteArray> = mutableMapOf()
|
||||
|
||||
@Synchronized
|
||||
internal fun hexToBytes(hex: String): ByteArray = hex.hashCode().let { id ->
|
||||
if (hexToByteArrayCacheMap.containsKey(id)) {
|
||||
return hexToByteArrayCacheMap[id]!!.copyOf()
|
||||
} else {
|
||||
hexToUBytes(hex).toByteArray().let {
|
||||
hexToByteArrayCacheMap[id] = it.copyOf()
|
||||
return it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun hexToUBytes(hex: String): UByteArray =
|
||||
hex.split(" ").dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
.map { value -> value.trim { it <= ' ' } }
|
||||
.map { s -> s.toUByte(16) }
|
||||
.toUByteArray()
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
//todo
|
@ -3,11 +3,10 @@
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlin.experimental.or
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
/**
|
||||
* Tool class for VarInt or VarLong operations.
|
||||
@ -23,7 +22,7 @@ fun encodeZigZag32(signedInt: Int): Long {
|
||||
}
|
||||
|
||||
|
||||
@JvmSynthetic
|
||||
//@JvmSynthetic
|
||||
fun decodeZigZag32(uint: UInt): Int {
|
||||
return decodeZigZag32(uint.toLong())
|
||||
}
|
||||
@ -41,57 +40,47 @@ fun decodeZigZag64(signedLong: Long): Long {
|
||||
}
|
||||
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataInputStream.readVarInt(): Int {
|
||||
fun ByteReadPacket.readVarInt(): Int {
|
||||
return decodeZigZag32(this.readUnsignedVarInt())
|
||||
}
|
||||
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataInputStream.readUnsignedVarInt(): UInt {
|
||||
fun ByteReadPacket.readUnsignedVarInt(): UInt {
|
||||
return read(this, 5).toUInt()
|
||||
}
|
||||
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataInputStream.readVarLong(): Long {
|
||||
fun ByteReadPacket.readVarLong(): Long {
|
||||
return decodeZigZag64(readUnsignedVarLong().toLong())
|
||||
}
|
||||
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataInputStream.readUnsignedVarLong(): ULong {
|
||||
fun ByteReadPacket.readUnsignedVarLong(): ULong {
|
||||
return read(this, 10).toULong()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataOutputStream.writeVarInt(signedInt: Int) {
|
||||
fun BytePacketBuilder.writeVarInt(signedInt: Int) {
|
||||
this.writeUVarInt(encodeZigZag32(signedInt))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataOutputStream.writeUVarInt(uint: UInt) {
|
||||
fun BytePacketBuilder.writeUVarInt(uint: UInt) {
|
||||
return writeUVarInt(uint.toLong())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataOutputStream.writeUVarInt(uint: Long) {
|
||||
fun BytePacketBuilder.writeUVarInt(uint: Long) {
|
||||
this.write0(uint)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataOutputStream.writeVarLong(signedLong: Long) {
|
||||
fun BytePacketBuilder.writeVarLong(signedLong: Long) {
|
||||
this.writeUVarLong(encodeZigZag64(signedLong))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataOutputStream.writeUVarLong(ulong: Long) {
|
||||
fun BytePacketBuilder.writeUVarLong(ulong: Long) {
|
||||
this.write0(ulong)
|
||||
}
|
||||
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun DataOutputStream.write0(long: Long) {
|
||||
private fun BytePacketBuilder.write0(long: Long) {
|
||||
var value = long
|
||||
do {
|
||||
var temp = (value and 127).toByte()
|
||||
@ -99,34 +88,19 @@ private fun DataOutputStream.write0(long: Long) {
|
||||
if (value != 0L) {
|
||||
temp = temp or 128.toByte()
|
||||
}
|
||||
this.writeByte(temp.toInt())
|
||||
this.writeByte(temp)
|
||||
} while (value != 0L)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun read(stream: DataInputStream, maxSize: Int): Long {
|
||||
private fun read(stream: ByteReadPacket, maxSize: Int): Long {
|
||||
var value: Long = 0
|
||||
var size = 0
|
||||
var b = stream.readByte().toInt()
|
||||
while (b and 0x80 == 0x80) {
|
||||
value = value or ((b and 0x7F).toLong() shl size++ * 7)
|
||||
require(size < maxSize) { "VarLong too big" }
|
||||
require(size < maxSize) { "VarLong too bigger(expecting maxSize=$maxSize)" }
|
||||
b = stream.readByte().toInt()
|
||||
}
|
||||
|
||||
return value or ((b and 0x7F).toLong() shl size * 7)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun read(stream: InputStream, maxSize: Int): Long {
|
||||
var value: Long = 0
|
||||
var size = 0
|
||||
var b = stream.read()
|
||||
while (b and 0x80 == 0x80) {
|
||||
value = value or ((b and 0x7F).toLong() shl size++ * 7)
|
||||
require(size < maxSize) { "VarLong too big" }
|
||||
b = stream.read()
|
||||
}
|
||||
|
||||
return value or ((b and 0x7F).toLong() shl size * 7)
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.mamoe.mirai
|
||||
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.QQ
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.goto
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.LoginState
|
||||
import net.mamoe.mirai.utils.ContactList
|
||||
import net.mamoe.mirai.utils.LoggerTextFormat
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* The mirror of functions in inner classes of [Bot]
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
//Contacts
|
||||
fun Bot.getQQ(number: Long): QQ = this.contacts.getQQ(number)
|
||||
|
||||
fun Bot.getGroupByNumber(number: Long): Group = this.contacts.getGroupByNumber(number)
|
||||
|
||||
fun Bot.getGroupById(number: Long): Group = this.contacts.getGroupById(number)
|
||||
|
||||
val Bot.groups: ContactList<Group> get() = this.contacts.groups
|
||||
|
||||
val Bot.qqs: ContactList<QQ> get() = this.contacts.qqs
|
||||
|
||||
|
||||
//NetworkHandler
|
||||
suspend fun Bot.sendPacket(packet: ClientPacket) = this.network.socket.sendPacket(packet)
|
||||
|
||||
suspend fun Bot.login(): LoginState = this.network.login()
|
||||
|
||||
//BotAccount
|
||||
val Bot.qqNumber: Long get() = this.account.qqNumber
|
||||
|
||||
|
||||
//logging
|
||||
fun Bot.log(o: Any?) = info(o)
|
||||
|
||||
fun Bot.println(o: Any?) = info(o)
|
||||
fun Bot.info(o: Any?) = print(this, o.toString(), LoggerTextFormat.RESET)
|
||||
|
||||
fun Bot.error(o: Any?) = print(this, o.toString(), LoggerTextFormat.RED)
|
||||
|
||||
fun Bot.notice(o: Any?) = print(this, o.toString(), LoggerTextFormat.LIGHT_BLUE)
|
||||
|
||||
fun Bot.purple(o: Any?) = print(this, o.toString(), LoggerTextFormat.PURPLE)
|
||||
|
||||
fun Bot.cyan(o: Any?) = print(this, o.toString(), LoggerTextFormat.LIGHT_CYAN)
|
||||
fun Bot.green(o: Any?) = print(this, o.toString(), LoggerTextFormat.GREEN)
|
||||
|
||||
fun Bot.debug(o: Any?) = print(this, o.toString(), LoggerTextFormat.YELLOW)
|
||||
|
||||
fun Bot.printPacketDebugging(packet: ServerPacket) {
|
||||
debug("Packet=$packet")
|
||||
debug("Packet size=" + packet.input.goto(0).readAllBytes().size)
|
||||
debug("Packet data=" + packet.input.goto(0).readAllBytes().toUHexString())
|
||||
}
|
||||
|
||||
private fun print(bot: Bot, value: String?, color: LoggerTextFormat = LoggerTextFormat.WHITE) {
|
||||
val s = SimpleDateFormat("MM-dd HH:mm:ss").format(Date())
|
||||
println("$color[Mirai] $s #R${bot.id}: $value")
|
||||
}
|
||||
|
||||
private fun print(value: String?, color: LoggerTextFormat = LoggerTextFormat.WHITE) {
|
||||
val s = SimpleDateFormat("MM-dd HH:mm:ss").format(Date())
|
||||
println("$color[Mirai] $s : $value")
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package net.mamoe.mirai.event.internal
|
||||
|
||||
import net.mamoe.mirai.event.Event
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.allSuperclasses
|
||||
import kotlin.reflect.full.isSuperclassOf
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal actual inline fun <E : Event> loopAllListeners(clazz: KClass<E>, consumer: (EventListeners<in E>) -> Unit) {
|
||||
clazz.allSuperclasses.forEach {
|
||||
if (Event::class.isSuperclassOf(it)) {
|
||||
consumer((it as KClass<out Event>).listeners as EventListeners<in E>)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
package net.mamoe.mirai.message
|
||||
|
||||
/**
|
||||
* @author LamGC
|
||||
*/
|
||||
@Suppress("EnumEntryName", "unused", "SpellCheckingInspection")
|
||||
enum class FaceID constructor(val id: Int) {
|
||||
unknown(0xff),
|
||||
// TODO: 2019/9/1 添加更多表情
|
||||
jingya(0),
|
||||
piezui(1),
|
||||
se(2),
|
||||
fadai(3),
|
||||
deyi(4),
|
||||
liulei(5),
|
||||
haixiu(6),
|
||||
bizui(7),
|
||||
shui(8),
|
||||
daku(9),
|
||||
ganga(10),
|
||||
fanu(11),
|
||||
tiaopi(12),
|
||||
ciya(13),
|
||||
weixiao(14),
|
||||
nanguo(15),
|
||||
ku(16),
|
||||
zhuakuang(18),
|
||||
tu(19),
|
||||
touxiao(20),
|
||||
keai(21),
|
||||
baiyan(22),
|
||||
aoman(23),
|
||||
ji_e(24),
|
||||
kun(25),
|
||||
jingkong(26),
|
||||
liuhan(27),
|
||||
hanxiao(28),
|
||||
dabing(29),
|
||||
fendou(30),
|
||||
zhouma(31),
|
||||
yiwen(32),
|
||||
yun(34),
|
||||
zhemo(35),
|
||||
shuai(36),
|
||||
kulou(37),
|
||||
qiaoda(38),
|
||||
zaijian(39),
|
||||
fadou(41),
|
||||
aiqing(42),
|
||||
tiaotiao(43),
|
||||
zhutou(46),
|
||||
yongbao(49),
|
||||
dan_gao(53),
|
||||
shandian(54),
|
||||
zhadan(55),
|
||||
dao(56),
|
||||
zuqiu(57),
|
||||
bianbian(59),
|
||||
kafei(60),
|
||||
fan(61),
|
||||
meigui(63),
|
||||
diaoxie(64),
|
||||
aixin(66),
|
||||
xinsui(67),
|
||||
liwu(69),
|
||||
taiyang(74),
|
||||
yueliang(75),
|
||||
qiang(76),
|
||||
ruo(77),
|
||||
woshou(78),
|
||||
shengli(79),
|
||||
feiwen(85),
|
||||
naohuo(86),
|
||||
xigua(89),
|
||||
lenghan(96),
|
||||
cahan(97),
|
||||
koubi(98),
|
||||
guzhang(99),
|
||||
qiudale(100),
|
||||
huaixiao(101),
|
||||
zuohengheng(102),
|
||||
youhengheng(103),
|
||||
haqian(104),
|
||||
bishi(105),
|
||||
weiqu(106),
|
||||
kuaikule(107),
|
||||
yinxian(108),
|
||||
qinqin(109),
|
||||
xia(110),
|
||||
kelian(111),
|
||||
caidao(112),
|
||||
pijiu(113),
|
||||
lanqiu(114),
|
||||
pingpang(115),
|
||||
shiai(116),
|
||||
piaochong(117),
|
||||
baoquan(118),
|
||||
gouyin(119),
|
||||
quantou(120),
|
||||
chajin(121),
|
||||
aini(122),
|
||||
bu(123),
|
||||
hao(124),
|
||||
zhuanquan(125),
|
||||
ketou(126),
|
||||
huitou(127),
|
||||
tiaosheng(128),
|
||||
huishou(129),
|
||||
jidong(130),
|
||||
jiewu(131),
|
||||
xianwen(132),
|
||||
zuotaiji(133),
|
||||
youtaiji(134),
|
||||
shuangxi(136),
|
||||
bianpao(137),
|
||||
denglong(138),
|
||||
facai(139),
|
||||
K_ge(140),
|
||||
gouwu(141),
|
||||
youjian(142),
|
||||
shuai_qi(143),
|
||||
hecai(144),
|
||||
qidao(145),
|
||||
baojin(146),
|
||||
bangbangtang(147),
|
||||
he_nai(148),
|
||||
xiamian(149),
|
||||
xiangjiao(150),
|
||||
feiji(151),
|
||||
kaiche(152),
|
||||
gaotiezuochetou(153),
|
||||
chexiang(154),
|
||||
gaotieyouchetou(155),
|
||||
duoyun(156),
|
||||
xiayu(157),
|
||||
chaopiao(158),
|
||||
xiongmao(159),
|
||||
dengpao(160),
|
||||
fengche(161),
|
||||
naozhong(162),
|
||||
dasan(163),
|
||||
caiqiu(164),
|
||||
zuanjie(165),
|
||||
shafa(166),
|
||||
zhijin(167),
|
||||
yao(168),
|
||||
shouqiang(169),
|
||||
qingwa(170);
|
||||
|
||||
override fun toString(): String {
|
||||
return "$name($id)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun ofId(id: Int): FaceID {
|
||||
for (value in values()) {
|
||||
if (value.id == id) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return unknown
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -5,13 +5,13 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.network.LoginSession
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.image.ClientTryGetImageIDPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.image.ServerTryGetImageIDFailedPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.image.ServerTryGetImageIDResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.image.ServerTryGetImageIDSuccessPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.md5
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientTryGetImageIDPacketJvm
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerTryGetImageIDFailedPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerTryGetImageIDResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerTryGetImageIDSuccessPacket
|
||||
import net.mamoe.mirai.qqNumber
|
||||
import net.mamoe.mirai.utils.ImageNetworkUtils
|
||||
import net.mamoe.mirai.utils.md5
|
||||
import net.mamoe.mirai.utils.toByteArray
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
import java.awt.image.BufferedImage
|
||||
@ -32,7 +32,7 @@ class UnsolvedImage(private val filename: String, val image: BufferedImage) {
|
||||
|
||||
suspend fun upload(session: LoginSession, contact: Contact): CompletableDeferred<Unit> {
|
||||
return session.expectPacket<ServerTryGetImageIDResponsePacket> {
|
||||
toSend { ClientTryGetImageIDPacket(session.bot.qqNumber, session.sessionKey, contact.number, image) }
|
||||
toSend { ClientTryGetImageIDPacketJvm(session.bot.qqNumber, session.sessionKey, contact.number, image) }
|
||||
|
||||
onExpect {
|
||||
when (it) {
|
||||
|
@ -1,171 +0,0 @@
|
||||
package net.mamoe.mirai.message.internal
|
||||
|
||||
import net.mamoe.mirai.message.*
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.io.DataInputStream
|
||||
|
||||
internal fun ByteArray.parseMessageFace(): Face = dataDecode(this) {
|
||||
//00 01 AF 0B 00 08 00 01 00 04 52 CC F5 D0 FF 00 02 14 F0
|
||||
//00 01 0C 0B 00 08 00 01 00 04 52 CC F5 D0 FF 00 02 14 4D
|
||||
it.skip(1)
|
||||
|
||||
val id1 = FaceID.ofId(it.readLVNumber().toInt())//可能这个是id, 也可能下面那个
|
||||
it.skip(it.readByte().toLong())
|
||||
it.readLVNumber()//某id?
|
||||
return@dataDecode Face(id1)
|
||||
}
|
||||
|
||||
internal fun ByteArray.parsePlainText(): PlainText = dataDecode(this) {
|
||||
it.skip(1)
|
||||
PlainText(it.readLVString())
|
||||
}
|
||||
|
||||
internal fun ByteArray.parseMessageImage0x06(): Image = dataDecode(this) {
|
||||
it.skip(1)
|
||||
MiraiLogger.debug("好友的图片")
|
||||
MiraiLogger.debug(this.toUHexString())
|
||||
val filenameLength = it.readShort()
|
||||
val suffix = it.readString(filenameLength).substringAfter(".")
|
||||
it.skip(this.size - 37 - 1 - filenameLength - 2)
|
||||
val imageId = String(it.readNBytes(36))
|
||||
MiraiLogger.debug(imageId)
|
||||
it.skip(1)//0x41
|
||||
return@dataDecode Image("{$imageId}.$suffix")
|
||||
}
|
||||
|
||||
internal fun ByteArray.parseMessageImage0x03(): Image = dataDecode(this) {
|
||||
it.skip(1)
|
||||
return@dataDecode Image(String(it.readLVByteArray()))
|
||||
/*
|
||||
println(String(it.readLVByteArray()))
|
||||
it.readTLVMap()
|
||||
return@dataDecode Image(String(it.readLVByteArray().cutTail(5).getRight(42)))
|
||||
/
|
||||
it.skip(data.size - 47)
|
||||
val imageId = String(it.readNBytes(42))
|
||||
it.skip(1)//0x41
|
||||
it.skip(1)//0x42
|
||||
it.skip(1)//0x43
|
||||
it.skip(1)//0x41
|
||||
|
||||
return@dataDecode Image(imageId)*/
|
||||
}
|
||||
|
||||
internal fun ByteArray.parseMessageChain(): MessageChain = dataDecode(this) {
|
||||
it.readMessageChain()
|
||||
}
|
||||
|
||||
internal fun DataInputStream.readMessage(): Message? {
|
||||
val messageType = this.readByte().toInt()
|
||||
val sectionLength = this.readShort().toLong()//sectionLength: short
|
||||
val sectionData = this.readNBytes(sectionLength)
|
||||
return when (messageType) {
|
||||
0x01 -> sectionData.parsePlainText()
|
||||
0x02 -> sectionData.parseMessageFace()
|
||||
0x03 -> sectionData.parseMessageImage0x03()
|
||||
0x06 -> sectionData.parseMessageImage0x06()
|
||||
|
||||
|
||||
0x19 -> {//长文本
|
||||
val value = readLVByteArray()
|
||||
//todo 未知压缩算法
|
||||
PlainText(String(value))
|
||||
|
||||
// PlainText(String(GZip.uncompress( value)))
|
||||
}
|
||||
|
||||
|
||||
0x14 -> {//长文本
|
||||
val value = readLVByteArray()
|
||||
println(value.size)
|
||||
println(value.toUHexString())
|
||||
//todo 未知压缩算法
|
||||
this.skip(7)//几个TLV
|
||||
return PlainText(String(value))
|
||||
}
|
||||
|
||||
0x0E -> {
|
||||
//null
|
||||
null
|
||||
}
|
||||
|
||||
else -> {
|
||||
println("未知的messageType=0x${messageType.toByte().toUHexString()}")
|
||||
println("后文=${this.readAllBytes().toUHexString()}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun DataInputStream.readMessageChain(): MessageChain {
|
||||
val chain = MessageChain()
|
||||
var got: Message? = null
|
||||
do {
|
||||
if (got != null) {
|
||||
chain.concat(got)
|
||||
}
|
||||
if (this.available() == 0) {
|
||||
return chain
|
||||
}
|
||||
got = this.readMessage()
|
||||
} while (got != null)
|
||||
return chain
|
||||
}
|
||||
|
||||
fun MessageChain.toByteArray(): ByteArray = dataEncode { result ->
|
||||
this@toByteArray.list.forEach { message ->
|
||||
result.write(with(message) {
|
||||
when (this) {
|
||||
is Face -> dataEncode { section ->
|
||||
section.writeByte(MessageType.FACE.intValue)
|
||||
|
||||
section.writeLVByteArray(dataEncode { child ->
|
||||
child.writeShort(1)
|
||||
child.writeByte(this.id.id)
|
||||
|
||||
child.writeHex("0B 00 08 00 01 00 04 52 CC F5 D0 FF")
|
||||
|
||||
child.writeShort(2)
|
||||
child.writeByte(0x14)//??
|
||||
child.writeByte(this.id.id + 65)
|
||||
})
|
||||
}
|
||||
|
||||
is At -> throw UnsupportedOperationException("At is not supported now but is expecting to be supported")
|
||||
|
||||
is Image -> dataEncode { section ->
|
||||
section.writeByte(MessageType.IMAGE.intValue)
|
||||
|
||||
section.writeLVByteArray(dataEncode { child ->
|
||||
child.writeByte(0x02)
|
||||
child.writeLVString(this.imageId)
|
||||
child.writeHex("04 00 " +
|
||||
"04 9B 53 B0 08 " +
|
||||
"05 00 " +
|
||||
"04 D9 8A 5A 70 " +
|
||||
"06 00 " +
|
||||
"04 00 00 00 50 " +
|
||||
"07 00 " +
|
||||
"01 43 08 00 00 09 00 01 01 0B 00 00 14 00 04 11 00 00 00 15 00 04 00 00 02 BC 16 00 04 00 00 02 BC 18 00 04 00 00 7D 5E FF 00 5C 15 36 20 39 32 6B 41 31 43 39 62 35 33 62 30 30 38 64 39 38 61 35 61 37 30 20")
|
||||
child.writeHex("20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20")
|
||||
child.writeBytes(this.imageId)
|
||||
child.writeByte(0x41)
|
||||
})
|
||||
}
|
||||
|
||||
is PlainText -> dataEncode { section ->
|
||||
section.writeByte(MessageType.PLAIN_TEXT.intValue)
|
||||
|
||||
section.writeLVByteArray(dataEncode { child ->
|
||||
child.writeByte(0x01)
|
||||
child.writeLVString(this.stringValue)
|
||||
})
|
||||
}
|
||||
|
||||
else -> throw UnsupportedOperationException("${this::class.simpleName} is not supported")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
package net.mamoe.mirai.network
|
||||
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler.BotSocket
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler.LoginHandler
|
||||
import net.mamoe.mirai.network.protocol.tim.handler.ActionPacketHandler
|
||||
import net.mamoe.mirai.network.protocol.tim.handler.DataPacketSocket
|
||||
import net.mamoe.mirai.network.protocol.tim.handler.MessagePacketHandler
|
||||
import net.mamoe.mirai.network.protocol.tim.handler.TemporaryPacketHandler
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.Packet
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerEventPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.LoginState
|
||||
|
||||
/**
|
||||
* Mirai 的网络处理器, 它承担所有数据包([Packet])的处理任务.
|
||||
* [BotNetworkHandler] 是全异步和线程安全的.
|
||||
*
|
||||
* [BotNetworkHandler] 由 2 个模块构成:
|
||||
* - [BotSocket]: 处理数据包底层的发送([ByteArray])
|
||||
* - [PacketHandler]: 制作 [ClientPacket] 并传递给 [BotSocket] 发送; 分析 [ServerPacket] 并处理
|
||||
*
|
||||
* 其中, [PacketHandler] 由 4 个子模块构成:
|
||||
* - [DebugPacketHandler] 输出 [Packet.toString]
|
||||
* - [LoginHandler] 处理 touch/login/verification code 相关
|
||||
* - [MessagePacketHandler] 处理消息相关(群消息/好友消息)([ServerEventPacket])
|
||||
* - [ActionPacketHandler] 处理动作相关(踢人/加入群/好友列表等)
|
||||
*
|
||||
* A BotNetworkHandler is used to connect with Tencent servers.
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
interface BotNetworkHandler {
|
||||
/**
|
||||
* 网络层处理器. 用于编码/解码 [Packet], 发送/接受 [ByteArray]
|
||||
*
|
||||
* java 调用方式: `botNetWorkHandler.getSocket()`
|
||||
*/
|
||||
val socket: DataPacketSocket
|
||||
|
||||
/**
|
||||
* 消息处理. 如发送好友消息, 接受群消息并触发事件
|
||||
*
|
||||
* java 调用方式: `botNetWorkHandler.getMessage()`
|
||||
*/
|
||||
val message: MessagePacketHandler
|
||||
|
||||
/**
|
||||
* 动作处理. 如发送好友请求, 处理别人发来的好友请求等
|
||||
*
|
||||
* java 调用方式: `botNetWorkHandler.getAction()`
|
||||
*/
|
||||
val action: ActionPacketHandler
|
||||
|
||||
/**
|
||||
* 尝试登录
|
||||
*/
|
||||
suspend fun login(): LoginState
|
||||
|
||||
/**
|
||||
* 添加一个临时包处理器
|
||||
*
|
||||
* @see [TemporaryPacketHandler]
|
||||
*/
|
||||
suspend fun addHandler(temporaryPacketHandler: TemporaryPacketHandler<*>)
|
||||
|
||||
fun close()
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package net.mamoe.mirai.network
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
object NetworkScope : CoroutineScope by CoroutineScope(Dispatchers.Default)
|
@ -1,426 +0,0 @@
|
||||
package net.mamoe.mirai.network.protocol.tim
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.mamoe.mirai.*
|
||||
import net.mamoe.mirai.event.EventScope
|
||||
import net.mamoe.mirai.event.ListeningStatus
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.BeforePacketSendEvent
|
||||
import net.mamoe.mirai.event.events.BotLoginSucceedEvent
|
||||
import net.mamoe.mirai.event.events.PacketSentEvent
|
||||
import net.mamoe.mirai.event.events.ServerPacketReceivedEvent
|
||||
import net.mamoe.mirai.event.subscribe
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.LoginSession
|
||||
import net.mamoe.mirai.network.NetworkScope
|
||||
import net.mamoe.mirai.network.protocol.tim.handler.*
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.io.File
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
/**
|
||||
* [BotNetworkHandler] 的内部实现, 该类不会有帮助理解的注解, 请查看 [BotNetworkHandler] 以获取帮助
|
||||
*
|
||||
* @see BotNetworkHandler
|
||||
* @author Him188moe
|
||||
*/
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")//to simplify code
|
||||
internal class TIMBotNetworkHandler(private val bot: Bot) : BotNetworkHandler {
|
||||
override val socket: BotSocket = BotSocket()
|
||||
|
||||
lateinit var loginHandler: LoginHandler
|
||||
|
||||
override lateinit var message: MessagePacketHandler
|
||||
override lateinit var action: ActionPacketHandler
|
||||
|
||||
val packetHandlers: PacketHandlerList = PacketHandlerList()
|
||||
|
||||
internal val temporaryPacketHandlers = Collections.synchronizedList(mutableListOf<TemporaryPacketHandler<*>>())
|
||||
|
||||
|
||||
override suspend fun addHandler(temporaryPacketHandler: TemporaryPacketHandler<*>) {
|
||||
temporaryPacketHandler.send(action.session)
|
||||
temporaryPacketHandlers.add(temporaryPacketHandler)
|
||||
}
|
||||
|
||||
override suspend fun login(): LoginState {
|
||||
return loginInternal(LinkedList(TIMProtocol.SERVER_IP))
|
||||
}
|
||||
|
||||
//嵌套进 login 会导致 kotlin internal CompilationException
|
||||
private suspend fun loginInternal(ipQueue: LinkedList<String>): LoginState {
|
||||
this.socket.close()
|
||||
val ip = ipQueue.poll() ?: return LoginState.UNKNOWN//所有服务器均返回 UNKNOWN
|
||||
|
||||
return socket.touch(ip).let { state ->
|
||||
if (state == LoginState.UNKNOWN || state == LoginState.TIMEOUT) {
|
||||
loginInternal(ipQueue)//超时或未知, 重试连接下一个服务器
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//private | internal
|
||||
private fun onLoggedIn(sessionKey: ByteArray) {
|
||||
val session = LoginSession(bot, sessionKey, socket)
|
||||
message = MessagePacketHandler(session)
|
||||
action = ActionPacketHandler(session)
|
||||
|
||||
packetHandlers.add(message.asNode())
|
||||
packetHandlers.add(action.asNode())
|
||||
}
|
||||
|
||||
private lateinit var sessionKey: ByteArray
|
||||
|
||||
override fun close() {
|
||||
this.packetHandlers.forEach {
|
||||
it.instance.close()
|
||||
}
|
||||
this.socket.close()
|
||||
}
|
||||
|
||||
|
||||
internal inner class BotSocket : DataPacketSocket {
|
||||
override suspend fun distributePacket(packet: ServerPacket) {
|
||||
try {
|
||||
packet.decode()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
bot.printPacketDebugging(packet)
|
||||
return
|
||||
}
|
||||
|
||||
//removeIf is not inline
|
||||
with(temporaryPacketHandlers.iterator()) {
|
||||
while (hasNext()) {
|
||||
if (next().onPacketReceived(action.session, packet)) {
|
||||
remove()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//For debug
|
||||
{
|
||||
if (!packet.javaClass.name.endsWith("Encrypted") && !packet.javaClass.name.endsWith("Raw")) {
|
||||
bot.notice("Packet received: $packet")
|
||||
}
|
||||
}()
|
||||
|
||||
if (packet is ServerEventPacket) {
|
||||
//no need to sync acknowledgement packets
|
||||
NetworkScope.launch {
|
||||
sendPacket(packet.ResponsePacket(bot.qqNumber, sessionKey))
|
||||
}
|
||||
}
|
||||
|
||||
if (ServerPacketReceivedEvent(bot, packet).broadcast().cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
loginHandler.onPacketReceived(packet)
|
||||
packetHandlers.forEach {
|
||||
it.instance.onPacketReceived(packet)
|
||||
}
|
||||
}
|
||||
|
||||
private var socket: DatagramSocket? = null
|
||||
|
||||
internal var serverIP: String = ""
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
restartSocket()
|
||||
}
|
||||
|
||||
internal lateinit var loginResult: CompletableDeferred<LoginState>
|
||||
|
||||
@Synchronized
|
||||
private fun restartSocket() {
|
||||
socket?.close()
|
||||
socket = DatagramSocket(0)
|
||||
socket!!.connect(InetSocketAddress(serverIP, 8000))
|
||||
NetworkScope.launch {
|
||||
while (socket?.isConnected == true) {
|
||||
val packet = DatagramPacket(ByteArray(2048), 2048)
|
||||
kotlin.runCatching { withContext(Dispatchers.IO) { socket?.receive(packet) } }
|
||||
.onSuccess {
|
||||
NetworkScope.launch {
|
||||
distributePacket(ServerPacket.ofByteArray(packet.data.removeZeroTail()))
|
||||
}
|
||||
}.onFailure {
|
||||
if (it.message == "Socket closed" || it.message == "socket closed") {
|
||||
return@launch
|
||||
}
|
||||
it.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun touch(serverAddress: String): LoginState {
|
||||
bot.info("Connecting server: $serverAddress")
|
||||
restartSocket()
|
||||
if (this@TIMBotNetworkHandler::loginHandler.isInitialized) {
|
||||
loginHandler.close()
|
||||
}
|
||||
loginHandler = LoginHandler()
|
||||
this.loginResult = CompletableDeferred()
|
||||
|
||||
serverIP = serverAddress
|
||||
//bot.waitForPacket(ServerTouchResponsePacket::class, timeoutMillis) {
|
||||
// loginResult?.complete(LoginState.TIMEOUT)
|
||||
//}
|
||||
val received = AtomicBoolean(false)
|
||||
ServerPacketReceivedEvent::class.subscribe {
|
||||
if (it.packet is ServerTouchResponsePacket && it.bot === bot) {
|
||||
received.set(true)
|
||||
ListeningStatus.STOPPED
|
||||
} else
|
||||
ListeningStatus.LISTENING
|
||||
}
|
||||
NetworkScope.launch {
|
||||
delay(2000)
|
||||
if (!received.get()) {
|
||||
loginResult.complete(LoginState.TIMEOUT)
|
||||
}
|
||||
}
|
||||
sendPacket(ClientTouchPacket(bot.qqNumber, serverIP))
|
||||
|
||||
return withContext(Dispatchers.IO) { loginResult.await() }
|
||||
}
|
||||
|
||||
override suspend fun sendPacket(packet: ClientPacket) = withContext(NetworkScope.coroutineContext) {
|
||||
checkNotNull(socket) { "network closed" }
|
||||
if (socket!!.isClosed) {
|
||||
return@withContext
|
||||
}
|
||||
|
||||
packet.encodePacket()
|
||||
|
||||
if (BeforePacketSendEvent(bot, packet).broadcast().cancelled) {
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val data = packet.toByteArray()
|
||||
withContext(Dispatchers.IO) {
|
||||
socket!!.send(DatagramPacket(data, data.size))
|
||||
}
|
||||
bot.cyan("Packet sent: $packet")
|
||||
|
||||
EventScope.launch { PacketSentEvent(bot, packet).broadcast() }
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
override val owner: Bot get() = this@TIMBotNetworkHandler.bot
|
||||
|
||||
|
||||
override fun close() {
|
||||
this.socket?.close()
|
||||
if (this::loginResult.isInitialized) {
|
||||
if (!this.loginResult.isCompleted && !this.loginResult.isCancelled) {
|
||||
this.loginResult.cancel(CancellationException("socket closed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isClosed(): Boolean = this.socket?.isClosed ?: true
|
||||
}
|
||||
|
||||
companion object {
|
||||
val captchaLock = Mutex()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录过程
|
||||
*/
|
||||
inner class LoginHandler {
|
||||
private lateinit var token00BA: ByteArray
|
||||
private lateinit var token0825: ByteArray//56
|
||||
private var loginTime: Int = 0
|
||||
private lateinit var loginIP: String
|
||||
private var privateKey: ByteArray = getRandomByteArray(16)
|
||||
|
||||
/**
|
||||
* 0828_decr_key
|
||||
*/
|
||||
private lateinit var sessionResponseDecryptionKey: ByteArray
|
||||
|
||||
private var captchaSectionId: Int = 1
|
||||
private var captchaCache: ByteArray? = byteArrayOf()//每次包只发一部分验证码来
|
||||
|
||||
private var heartbeatJob: Job? = null
|
||||
|
||||
|
||||
suspend fun onPacketReceived(packet: ServerPacket) {
|
||||
when (packet) {
|
||||
is ServerTouchResponsePacket -> {
|
||||
if (packet.serverIP != null) {//redirection
|
||||
socket.serverIP = packet.serverIP!!
|
||||
//connect(packet.serverIP!!)
|
||||
socket.sendPacket(ClientServerRedirectionPacket(packet.serverIP!!, bot.qqNumber))
|
||||
} else {//password submission
|
||||
this.loginIP = packet.loginIP
|
||||
this.loginTime = packet.loginTime
|
||||
this.token0825 = packet.token0825
|
||||
println("token0825=" + this.token0825.toUHexString())
|
||||
socket.sendPacket(ClientPasswordSubmissionPacket(bot.qqNumber, bot.account.password, packet.loginTime, packet.loginIP, this.privateKey, packet.token0825))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerLoginResponseFailedPacket -> {
|
||||
socket.loginResult.complete(packet.loginState)
|
||||
bot.close()
|
||||
return
|
||||
}
|
||||
|
||||
is ServerCaptchaCorrectPacket -> {
|
||||
this.privateKey = getRandomByteArray(16)
|
||||
this.token00BA = packet.token00BA
|
||||
socket.sendPacket(ClientLoginResendPacket3105(bot.qqNumber, bot.account.password, this.loginTime, this.loginIP, this.privateKey, this.token0825, this.token00BA))
|
||||
}
|
||||
|
||||
is ServerLoginResponseVerificationCodeInitPacket -> {
|
||||
//[token00BA]来源之一: 验证码
|
||||
this.token00BA = packet.token00BA
|
||||
this.captchaCache = packet.verifyCodePart1
|
||||
|
||||
if (packet.unknownBoolean == true) {
|
||||
this.captchaSectionId = 1
|
||||
socket.sendPacket(ClientVerificationCodeTransmissionRequestPacket(1, bot.qqNumber, this.token0825, this.captchaSectionId++, this.token00BA))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerCaptchaTransmissionPacket -> {
|
||||
if (packet is ServerCaptchaWrongPacket) {
|
||||
bot.error("验证码错误, 请重新输入")
|
||||
captchaSectionId = 1
|
||||
this.captchaCache = byteArrayOf()
|
||||
}
|
||||
|
||||
this.captchaCache = this.captchaCache!! + packet.captchaSectionN
|
||||
this.token00BA = packet.token00BA
|
||||
|
||||
if (packet.transmissionCompleted) {
|
||||
//todo 验证码多样化处理
|
||||
|
||||
val code = captchaLock.withLock {
|
||||
withContext(Dispatchers.IO) {
|
||||
bot.notice(ImageIO.read(captchaCache!!.inputStream()).createCharImg())
|
||||
}
|
||||
bot.notice("需要验证码登录, 验证码为 4 字母")
|
||||
try {
|
||||
File(System.getProperty("user.dir") + "/temp/Captcha.png")
|
||||
.also { withContext(Dispatchers.IO) { it.createNewFile() } }
|
||||
.writeBytes(this.captchaCache!!)
|
||||
bot.notice("若看不清字符图片, 请查看 Mirai 目录下 /temp/Captcha.png")
|
||||
} catch (e: Exception) {
|
||||
bot.notice("无法写出验证码文件, 请尝试查看以上字符图片")
|
||||
}
|
||||
this.captchaCache = null
|
||||
bot.notice("若要更换验证码, 请直接回车")
|
||||
Scanner(System.`in`).nextLine()
|
||||
}
|
||||
if (code.isEmpty() || code.length != 4) {
|
||||
this.captchaCache = byteArrayOf()
|
||||
this.captchaSectionId = 1
|
||||
socket.sendPacket(ClientVerificationCodeRefreshPacket(packet.packetIdLast + 1, bot.qqNumber, token0825))
|
||||
} else {
|
||||
socket.sendPacket(ClientVerificationCodeSubmitPacket(packet.packetIdLast + 1, bot.qqNumber, token0825, code, packet.verificationToken))
|
||||
}
|
||||
} else {
|
||||
socket.sendPacket(ClientVerificationCodeTransmissionRequestPacket(packet.packetIdLast + 1, bot.qqNumber, token0825, captchaSectionId++, token00BA))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerLoginResponseSuccessPacket -> {
|
||||
this.sessionResponseDecryptionKey = packet.sessionResponseDecryptionKey
|
||||
socket.sendPacket(ClientSessionRequestPacket(bot.qqNumber, socket.serverIP, packet.token38, packet.token88, packet.encryptionKey))
|
||||
}
|
||||
|
||||
//是ClientPasswordSubmissionPacket之后服务器回复的
|
||||
is ServerLoginResponseKeyExchangePacket -> {
|
||||
//if (packet.tokenUnknown != null) {
|
||||
//this.token00BA = packet.token00BA!!
|
||||
//println("token00BA changed!!! to " + token00BA.toUByteArray())
|
||||
//}
|
||||
if (packet.flag == ServerLoginResponseKeyExchangePacket.Flag.`08 36 31 03`) {
|
||||
this.privateKey = packet.privateKeyUpdate
|
||||
socket.sendPacket(ClientLoginResendPacket3104(bot.qqNumber, bot.account.password, loginTime, loginIP, privateKey, token0825, packet.tokenUnknown
|
||||
?: this.token00BA, packet.tlv0006))
|
||||
} else {
|
||||
socket.sendPacket(ClientLoginResendPacket3106(bot.qqNumber, bot.account.password, loginTime, loginIP, privateKey, token0825, packet.tokenUnknown
|
||||
?: token00BA, packet.tlv0006))
|
||||
}
|
||||
}
|
||||
|
||||
is ServerSessionKeyResponsePacket -> {
|
||||
sessionKey = packet.sessionKey
|
||||
|
||||
heartbeatJob = NetworkScope.launch {
|
||||
delay(90000)
|
||||
socket.sendPacket(ClientHeartbeatPacket(bot.qqNumber, sessionKey))
|
||||
}
|
||||
|
||||
socket.loginResult.complete(LoginState.SUCCESS)
|
||||
|
||||
loginHandler.changeOnlineStatus(ClientLoginStatus.ONLINE)//required
|
||||
}
|
||||
|
||||
is ServerLoginSuccessPacket -> {
|
||||
BotLoginSucceedEvent(bot).broadcast()
|
||||
|
||||
//登录成功后会收到大量上次的消息, 忽略掉 todo 优化
|
||||
NetworkScope.launch {
|
||||
delay(3000)
|
||||
message.ignoreMessage = false
|
||||
}
|
||||
|
||||
|
||||
onLoggedIn(sessionKey)
|
||||
}
|
||||
|
||||
|
||||
is ServerCaptchaPacket.Encrypted -> socket.distributePacket(packet.decrypt())
|
||||
is ServerLoginResponseVerificationCodeInitPacket.Encrypted -> socket.distributePacket(packet.decrypt())
|
||||
is ServerLoginResponseKeyExchangePacket.Encrypted -> socket.distributePacket(packet.decrypt(this.privateKey))
|
||||
is ServerLoginResponseSuccessPacket.Encrypted -> socket.distributePacket(packet.decrypt(this.privateKey))
|
||||
is ServerSessionKeyResponsePacket.Encrypted -> socket.distributePacket(packet.decrypt(this.sessionResponseDecryptionKey))
|
||||
is ServerTouchResponsePacket.Encrypted -> socket.distributePacket(packet.decrypt())
|
||||
|
||||
|
||||
is ServerHeartbeatResponsePacket,
|
||||
is UnknownServerPacket -> {
|
||||
//ignored
|
||||
}
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
suspend fun changeOnlineStatus(status: ClientLoginStatus) {
|
||||
socket.sendPacket(ClientChangeOnlineStatusPacket(bot.qqNumber, sessionKey, status))
|
||||
}
|
||||
|
||||
fun close() {
|
||||
this.captchaCache = null
|
||||
|
||||
this.heartbeatJob?.cancel(CancellationException("handler closed"))
|
||||
|
||||
this.heartbeatJob = null
|
||||
}
|
||||
}
|
||||
}
|
@ -1,230 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketNameFormatter.adjustName
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
abstract class ClientPacket : ByteArrayDataOutputStream(), Packet {
|
||||
val idHex: String
|
||||
|
||||
private var encoded: Boolean = false
|
||||
|
||||
init {
|
||||
val annotation = this.javaClass.getAnnotation(PacketId::class.java)
|
||||
idHex = annotation.value.trim()
|
||||
|
||||
try {
|
||||
this.writeHex(TIMProtocol.head)
|
||||
this.writeHex(TIMProtocol.ver)
|
||||
this.writePacketId()
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun writePacketId() {
|
||||
this.writeHex(this@ClientPacket.idHex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode this packet.
|
||||
*
|
||||
*
|
||||
* Before sending the packet, a [tail][TIMProtocol.tail] is added.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
protected abstract fun encode()
|
||||
|
||||
fun encodePacket() {
|
||||
if (encoded) {
|
||||
return
|
||||
}
|
||||
encode()
|
||||
writeHex(TIMProtocol.tail)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun encodeToByteArray(): ByteArray {
|
||||
encodePacket()
|
||||
return toByteArray()
|
||||
}
|
||||
|
||||
open fun getFixedId(): String = when (this.idHex.length) {
|
||||
0 -> "__ __ __ __"
|
||||
2 -> this.idHex + " __ __ __"
|
||||
5 -> this.idHex + " __ __"
|
||||
7 -> this.idHex + " __"
|
||||
else -> this.idHex
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return adjustName(this.javaClass.simpleName + "(${this.getFixedId()})") + this.getAllDeclaredFields().filterNot { it.name == "idHex" || it.name == "idByteArray" || it.name == "encoded" }.joinToString(", ", "{", "}") {
|
||||
it.trySetAccessible(); it.name + "=" + it.get(this).let { value ->
|
||||
when (value) {
|
||||
null -> null
|
||||
is ByteArray -> value.toUHexString()
|
||||
is UByteArray -> value.toUHexString()
|
||||
else -> value.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataOutputStream.writeIP(ip: String) {
|
||||
for (s in ip.trim().split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
||||
this.writeByte(s.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataOutputStream.writeTime() {
|
||||
this.writeInt(System.currentTimeMillis().toInt())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataOutputStream.writeHex(uHex: String) {
|
||||
for (s in uHex.trim().split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
||||
if (s.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
this.writeByte(s.toUByte(16).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
fun DataOutputStream.encryptAndWrite(byteArray: ByteArray, key: ByteArray) {
|
||||
this.write(TEA.encrypt(byteArray, key))
|
||||
}
|
||||
|
||||
fun DataOutputStream.encryptAndWrite(key: ByteArray, encoder: ByteArrayDataOutputStream.() -> Unit) {
|
||||
this.write(TEA.encrypt(ByteArrayDataOutputStream().apply(encoder).use { it.toByteArray() }, key))
|
||||
}
|
||||
|
||||
fun DataOutputStream.encryptAndWrite(keyHex: String, encoder: ByteArrayDataOutputStream.() -> Unit) {
|
||||
this.encryptAndWrite(keyHex.hexToBytes(), encoder)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun DataOutputStream.writeTLV0006(qq: Long, password: String, loginTime: Int, loginIP: String, privateKey: ByteArray) {
|
||||
val firstMD5 = md5(password)
|
||||
val secondMD5 = md5(firstMD5 + "00 00 00 00".hexToBytes() + qq.toUInt().toByteArray())
|
||||
|
||||
this.encryptAndWrite(secondMD5) {
|
||||
writeRandom(4)
|
||||
writeHex("00 02")
|
||||
writeQQ(qq)
|
||||
writeHex(TIMProtocol.constantData2)
|
||||
writeHex("00 00 01")
|
||||
|
||||
write(firstMD5)
|
||||
writeInt(loginTime)
|
||||
writeByte(0)
|
||||
writeZero(4 * 3)
|
||||
writeIP(loginIP)
|
||||
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
|
||||
write(privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
@Tested
|
||||
fun DataOutputStream.writeCRC32() = writeCRC32(getRandomByteArray(16))
|
||||
|
||||
|
||||
fun DataOutputStream.writeCRC32(key: ByteArray) {
|
||||
write(key)//key
|
||||
writeInt(getCrc32(key))
|
||||
}
|
||||
|
||||
|
||||
@Tested
|
||||
fun DataOutputStream.writeDeviceName(random: Boolean = false) {
|
||||
val deviceName: String = if (random) {
|
||||
String(getRandomByteArray(10))
|
||||
} else {
|
||||
InetAddress.getLocalHost().hostName
|
||||
}
|
||||
this.writeShort(deviceName.length + 2)
|
||||
this.writeShort(deviceName.length)
|
||||
this.writeBytes(deviceName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 255 -> 00 00 00 FF
|
||||
*/
|
||||
fun Int.toByteArray(): ByteArray = byteArrayOf(
|
||||
(this.ushr(24) and 0xFF).toByte(),
|
||||
(this.ushr(16) and 0xFF).toByte(),
|
||||
(this.ushr(8) and 0xFF).toByte(),
|
||||
(this.ushr(0) and 0xFF).toByte()
|
||||
)
|
||||
|
||||
/**
|
||||
* 255u -> 00 00 00 FF
|
||||
*/
|
||||
|
||||
fun UInt.toByteArray(): ByteArray = byteArrayOf(
|
||||
(this.shr(24) and 255u).toByte(),
|
||||
(this.shr(16) and 255u).toByte(),
|
||||
(this.shr(8) and 255u).toByte(),
|
||||
(this.shr(0) and 255u).toByte()
|
||||
)
|
||||
|
||||
|
||||
fun Int.toUHexString(separator: String = " "): String = this.toByteArray().toUHexString(separator)
|
||||
|
||||
fun md5(str: String): ByteArray = MessageDigest.getInstance("MD5").digest(str.toByteArray())
|
||||
|
||||
fun md5(byteArray: ByteArray): ByteArray = MessageDigest.getInstance("MD5").digest(byteArray)
|
||||
|
||||
fun DataOutputStream.writeZero(count: Int) {
|
||||
repeat(count) {
|
||||
this.writeByte(0)
|
||||
}
|
||||
}
|
||||
|
||||
fun DataOutputStream.writeRandom(length: Int) {
|
||||
repeat(length) {
|
||||
this.writeByte((Math.random() * 255).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
fun DataOutputStream.writeQQ(qq: Long) {
|
||||
this.write(qq.toUInt().toByteArray())
|
||||
}
|
||||
|
||||
fun DataOutputStream.writeGroup(groupIdOrGroupNumber: Long) {
|
||||
this.write(groupIdOrGroupNumber.toUInt().toByteArray())
|
||||
}
|
||||
|
||||
fun DataOutputStream.writeUByte(uByte: UByte) {
|
||||
this.write(uByte.toInt())
|
||||
}
|
||||
|
||||
fun DataOutputStream.writeLVByteArray(byteArray: ByteArray) {
|
||||
this.writeShort(byteArray.size)
|
||||
this.write(byteArray)
|
||||
}
|
||||
|
||||
fun DataOutputStream.writeLVString(str: String) {
|
||||
this.writeLVByteArray(str.toByteArray())
|
||||
}
|
||||
|
||||
fun DataOutputStream.writeLVHex(hex: String) {
|
||||
this.writeLVByteArray(hex.hexToBytes())
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
interface Packet
|
||||
|
||||
|
||||
internal object PacketNameFormatter {
|
||||
@JvmStatic
|
||||
private var longestNameLength: Int = 43
|
||||
|
||||
@JvmStatic
|
||||
fun adjustName(name: String): String {
|
||||
if (name.length > longestNameLength) {
|
||||
longestNameLength = name.length
|
||||
return name
|
||||
}
|
||||
|
||||
return " ".repeat(longestNameLength - name.length) + name
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.IoBuffer
|
||||
import kotlinx.io.core.internal.DangerousInternalIoApi
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
import java.lang.reflect.Field
|
||||
|
||||
internal object PacketNameFormatter {
|
||||
@JvmStatic
|
||||
private var longestNameLength: Int = 43
|
||||
|
||||
@JvmStatic
|
||||
fun adjustName(name: String): String {
|
||||
if (name.length > longestNameLength) {
|
||||
longestNameLength = name.length
|
||||
return name
|
||||
}
|
||||
|
||||
return " ".repeat(longestNameLength - name.length) + name
|
||||
}
|
||||
}
|
||||
|
||||
private object IgnoreIdList : List<String> by listOf(
|
||||
"idHex",
|
||||
"fixedId",
|
||||
"idByteArray",
|
||||
"encoded",
|
||||
"packet",
|
||||
"Companion",
|
||||
"EMPTY_ID_HEX",
|
||||
"input",
|
||||
"output",
|
||||
"UninitializedByteReadPacket"
|
||||
)
|
||||
|
||||
@UseExperimental(DangerousInternalIoApi::class)
|
||||
internal actual fun Packet.packetToString(): String = PacketNameFormatter.adjustName(this::class.simpleName + "(${this.fixedId})") + this::class.java.allDeclaredFields
|
||||
.filterNot { it.name in IgnoreIdList || "delegate" in it.name || "$" in it.name }
|
||||
.joinToString(", ", "{", "}") {
|
||||
it.isAccessible = true
|
||||
it.name + "=" + it.get(this).let { value ->
|
||||
when (value) {
|
||||
null -> null
|
||||
is ByteArray -> value.toUHexString()
|
||||
is UByteArray -> value.toUHexString()
|
||||
is ByteReadPacket -> "[ByteReadPacket(${value.remaining})]"
|
||||
//is ByteReadPacket -> value.copy().readBytes().toUHexString()
|
||||
is IoBuffer -> "[IoBuffer(${value.readRemaining})]"
|
||||
else -> value.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Class<*>.allDeclaredFields: List<Field>
|
||||
get() {
|
||||
val list = mutableListOf<Field>()
|
||||
|
||||
var clazz: Class<*> = this
|
||||
do {
|
||||
list.addAll(clazz.declaredFields)
|
||||
} while (clazz.let { clazz = it.superclass; clazz.kotlin != Any::class })
|
||||
|
||||
return list
|
||||
}
|
@ -1,331 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketNameFormatter.adjustName
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ServerCanAddFriendResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ServerSendFriendMessageResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.action.ServerSendGroupMessageResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.image.ServerTryGetImageIDResponsePacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.login.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.io.DataInputStream
|
||||
import java.io.EOFException
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
abstract class ServerPacket(val input: DataInputStream) : Packet {
|
||||
var idHex: String
|
||||
|
||||
var idByteArray: ByteArray//fixed 4 size
|
||||
|
||||
|
||||
var encoded: Boolean = false
|
||||
|
||||
init {
|
||||
idHex = try {
|
||||
val annotation = this.javaClass.getAnnotation(PacketId::class.java)
|
||||
annotation.value.trim()
|
||||
} catch (e: NullPointerException) {
|
||||
""
|
||||
}
|
||||
|
||||
idByteArray = if (idHex.isEmpty()) {
|
||||
byteArrayOf(0, 0, 0, 0)
|
||||
} else {
|
||||
idHex.hexToBytes()
|
||||
}
|
||||
}
|
||||
|
||||
fun <P : ServerPacket> P.setId(idHex: String): P {
|
||||
this.idHex = idHex
|
||||
return this
|
||||
}
|
||||
|
||||
open fun decode() {
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun ofByteArray(bytes: ByteArray): ServerPacket {
|
||||
val stream = bytes.dataInputStream()
|
||||
|
||||
stream.skip(3)
|
||||
|
||||
val idHex = stream.readInt().toUHexString(" ")
|
||||
return when (idHex) {
|
||||
"08 25 31 01" -> ServerTouchResponsePacket.Encrypted(ServerTouchResponsePacket.Type.TYPE_08_25_31_01, stream)
|
||||
"08 25 31 02" -> ServerTouchResponsePacket.Encrypted(ServerTouchResponsePacket.Type.TYPE_08_25_31_02, stream)
|
||||
|
||||
"08 36 31 03", "08 36 31 04", "08 36 31 05", "08 36 31 06" -> {
|
||||
when (bytes.size) {
|
||||
271, 207 -> return ServerLoginResponseKeyExchangePacket.Encrypted(stream, when (idHex) {
|
||||
"08 36 31 03" -> ServerLoginResponseKeyExchangePacket.Flag.`08 36 31 03`
|
||||
else -> ServerLoginResponseKeyExchangePacket.Flag.OTHER
|
||||
|
||||
}).apply { this.idHex = idHex }
|
||||
871 -> return ServerLoginResponseVerificationCodeInitPacket.Encrypted(stream).apply { this.idHex = idHex }
|
||||
}
|
||||
|
||||
if (bytes.size > 700) {
|
||||
return ServerLoginResponseSuccessPacket.Encrypted(stream).apply { this.idHex = idHex }
|
||||
}
|
||||
|
||||
println(bytes.size)
|
||||
return ServerLoginResponseFailedPacket(when (bytes.size) {
|
||||
135 -> LoginState.UNKNOWN//账号已经在另一台电脑登录??
|
||||
|
||||
319, 351 -> LoginState.WRONG_PASSWORD
|
||||
//135 -> LoginState.RETYPE_PASSWORD
|
||||
63, 279 -> LoginState.BLOCKED
|
||||
263 -> LoginState.UNKNOWN_QQ_NUMBER
|
||||
551, 487 -> LoginState.DEVICE_LOCK
|
||||
359 -> LoginState.TAKEN_BACK
|
||||
|
||||
else -> LoginState.UNKNOWN
|
||||
/*
|
||||
//unknown
|
||||
63 -> throw IllegalArgumentException(bytes.size.toString() + " (Unknown error)")
|
||||
351 -> throw IllegalArgumentException(bytes.size.toString() + " (Unknown error)")
|
||||
|
||||
else -> throw IllegalArgumentException(bytes.size.toString())*/
|
||||
}, stream).apply { this.idHex = idHex }
|
||||
}
|
||||
|
||||
"08 28 04 34" -> ServerSessionKeyResponsePacket.Encrypted(stream)
|
||||
|
||||
|
||||
else -> when (idHex.substring(0, 5)) {
|
||||
"00 EC" -> ServerLoginSuccessPacket(stream)
|
||||
"00 1D" -> ServerSKeyResponsePacket.Encrypted(stream)
|
||||
"00 5C" -> ServerAccountInfoResponsePacket.Encrypted(stream)
|
||||
|
||||
"00 58" -> ServerHeartbeatResponsePacket(stream)
|
||||
|
||||
"00 BA" -> ServerCaptchaPacket.Encrypted(stream, idHex)
|
||||
|
||||
|
||||
"00 CE", "00 17" -> ServerEventPacket.Raw.Encrypted(stream, idHex.hexToBytes())
|
||||
|
||||
"00 81" -> UnknownServerPacket(stream)
|
||||
|
||||
"00 CD" -> ServerSendFriendMessageResponsePacket(stream)
|
||||
"00 02" -> ServerSendGroupMessageResponsePacket(stream)
|
||||
|
||||
"00 A7" -> ServerCanAddFriendResponsePacket(stream)
|
||||
|
||||
"03 88" -> ServerTryGetImageIDResponsePacket.Encrypted(stream)
|
||||
|
||||
else -> UnknownServerPacket(stream)
|
||||
}
|
||||
}.apply { this.idHex = idHex }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return adjustName(this.javaClass.simpleName + "(${this.getFixedId()})") + this.getAllDeclaredFields().filterNot { it.name == "idHex" || it.name == "idByteArray" || it.name == "encoded" }.joinToString(", ", "{", "}") {
|
||||
it.trySetAccessible(); it.name + "=" + it.get(this).let { value ->
|
||||
when (value) {
|
||||
is ByteArray -> value.toUHexString()
|
||||
is UByteArray -> value.toUHexString()
|
||||
else -> value?.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun getFixedId(): String = getFixedId(this.idHex)
|
||||
|
||||
fun getFixedId(id: String): String = when (id.length) {
|
||||
0 -> "__ __ __ __"
|
||||
2 -> "$id __ __ __"
|
||||
5 -> "$id __ __"
|
||||
7 -> "$id __"
|
||||
else -> id
|
||||
}
|
||||
|
||||
fun decryptBy(key: ByteArray): DataInputStream {
|
||||
return decryptAsByteArray(key).dataInputStream()
|
||||
}
|
||||
|
||||
|
||||
fun decryptBy(keyHex: String): DataInputStream {
|
||||
return this.decryptBy(keyHex.hexToBytes())
|
||||
}
|
||||
|
||||
fun decryptBy(key1: ByteArray, key2: ByteArray): DataInputStream {
|
||||
return TEA.decrypt(this.decryptAsByteArray(key1), key2).dataInputStream()
|
||||
}
|
||||
|
||||
|
||||
fun decryptBy(key1: String, key2: ByteArray): DataInputStream {
|
||||
return this.decryptBy(key1.hexToBytes(), key2)
|
||||
}
|
||||
|
||||
|
||||
fun decryptBy(key1: ByteArray, key2: String): DataInputStream {
|
||||
return this.decryptBy(key1, key2.hexToBytes())
|
||||
}
|
||||
|
||||
|
||||
fun decryptBy(keyHex1: String, keyHex2: String): DataInputStream {
|
||||
return this.decryptBy(keyHex1.hexToBytes(), keyHex2.hexToBytes())
|
||||
}
|
||||
|
||||
fun decryptAsByteArray(key: ByteArray): ByteArray {
|
||||
input.goto(14)
|
||||
return TEA.decrypt(input.readAllBytes().cutTail(1), key)
|
||||
}
|
||||
|
||||
fun decryptAsByteArray(keyHex: String): ByteArray = this.decryptAsByteArray(keyHex.hexToBytes())
|
||||
}
|
||||
|
||||
|
||||
fun DataInputStream.readIP(): String {
|
||||
var buff = ""
|
||||
for (i in 0..3) {
|
||||
val byte = readUnsignedByte()
|
||||
buff += byte.toString()
|
||||
if (i != 3) buff += "."
|
||||
}
|
||||
return buff
|
||||
}
|
||||
|
||||
fun DataInputStream.readLVString(): String {
|
||||
return String(this.readLVByteArray())
|
||||
}
|
||||
|
||||
fun DataInputStream.readLVByteArray(): ByteArray {
|
||||
return this.readNBytes(this.readShort().toInt())
|
||||
}
|
||||
|
||||
fun DataInputStream.readTLVMap(expectingEOF: Boolean = false): Map<Int, ByteArray> {
|
||||
val map = mutableMapOf<Int, ByteArray>()
|
||||
var type: Int
|
||||
|
||||
try {
|
||||
type = readUnsignedByte()
|
||||
} catch (e: EOFException) {
|
||||
if (expectingEOF) {
|
||||
return map
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
while (type != 0xff) {
|
||||
map[type] = this.readLVByteArray()
|
||||
|
||||
try {
|
||||
type = readUnsignedByte()
|
||||
} catch (e: EOFException) {
|
||||
if (expectingEOF) {
|
||||
return map
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
fun Map<Int, ByteArray>.printTLVMap() {
|
||||
println(this.mapValues { (_, value) -> value.toUHexString() })
|
||||
}
|
||||
|
||||
|
||||
fun DataInputStream.readString(length: Number): String {
|
||||
return String(this.readNBytes(length))
|
||||
}
|
||||
|
||||
|
||||
fun ByteArray.dataInputStream(): DataInputStream = DataInputStream(this.inputStream())
|
||||
|
||||
/**
|
||||
* Reset and skip(position)
|
||||
*/
|
||||
fun <N : Number> DataInputStream.goto(position: N): DataInputStream {
|
||||
this.reset()
|
||||
this.skip(position.toLong())
|
||||
return this
|
||||
}
|
||||
|
||||
fun <N : Number> DataInputStream.readNBytesAt(position: N, length: Int): ByteArray {
|
||||
this.goto(position)
|
||||
return this.readNBytes(length)
|
||||
}
|
||||
|
||||
fun <N : Number> DataInputStream.readNBytes(length: N): ByteArray {
|
||||
return this.readNBytes(length.toInt())
|
||||
}
|
||||
|
||||
|
||||
fun DataInputStream.readLVNumber(): Number {
|
||||
return when (this.readShort().toInt()) {
|
||||
1 -> this.readByte()
|
||||
2 -> this.readShort()
|
||||
4 -> this.readInt()
|
||||
8 -> this.readLong()
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
fun DataInputStream.readNBytesIn(range: IntRange): ByteArray {
|
||||
this.goto(range.first)
|
||||
return this.readNBytes(range.last - range.first + 1)
|
||||
}
|
||||
|
||||
fun <N : Number> DataInputStream.readIntAt(position: N): Int {
|
||||
this.goto(position)
|
||||
return this.readInt()
|
||||
}
|
||||
|
||||
|
||||
fun <N : Number> DataInputStream.readUIntAt(position: N): UInt {
|
||||
this.goto(position)
|
||||
return this.readNBytes(4).toUInt()
|
||||
}
|
||||
|
||||
fun DataInputStream.readUInt(): UInt {
|
||||
return this.readNBytes(4).toUInt()
|
||||
}
|
||||
|
||||
fun <N : Number> DataInputStream.readByteAt(position: N): Byte {
|
||||
this.goto(position)
|
||||
return this.readByte()
|
||||
}
|
||||
|
||||
fun <N : Number> DataInputStream.readShortAt(position: N): Short {
|
||||
this.goto(position)
|
||||
return this.readShort()
|
||||
}
|
||||
|
||||
//添加@JvmSynthetic 导致 idea 无法检查这个文件的错误
|
||||
//@JvmSynthetic
|
||||
fun DataInputStream.gotoWhere(matcher: UByteArray): DataInputStream {
|
||||
return this.gotoWhere(matcher.toByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* 去往下一个含这些连续字节的位置
|
||||
*/
|
||||
@Throws(EOFException::class)
|
||||
fun DataInputStream.gotoWhere(matcher: ByteArray): DataInputStream {
|
||||
require(matcher.isNotEmpty())
|
||||
|
||||
loop@
|
||||
do {
|
||||
val byte = this.readByte()
|
||||
if (byte == matcher[0]) {
|
||||
//todo mark here
|
||||
for (i in 1 until matcher.size) {
|
||||
val b = this.readByte()
|
||||
if (b != matcher[i]) {
|
||||
continue@loop //todo goto mark
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
} while (true)
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import net.mamoe.mirai.utils.LoggerTextFormat
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
import java.io.DataInputStream
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
class UnknownServerPacket(input: DataInputStream) : ServerPacket(input) {
|
||||
override fun decode() {
|
||||
MiraiLogger.debug("UnknownServerPacket data: " + this.input.goto(0).readAllBytes().toUHexString())
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return LoggerTextFormat.LIGHT_RED.toString() + super.toString()
|
||||
}
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.image
|
||||
package net.mamoe.mirai.network.protocol.tim.packet
|
||||
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.utils.toByteArray
|
||||
import net.mamoe.mirai.utils.writeUVarInt
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.writeFully
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.DataInputStream
|
||||
|
||||
actual typealias PlatformImage = BufferedImage
|
||||
|
||||
actual typealias ClientTryGetImageIDPacket = ClientTryGetImageIDPacketJvm
|
||||
|
||||
/**
|
||||
* 请求上传图片. 将发送图片的 md5, size.
|
||||
@ -17,13 +20,13 @@ import java.io.DataInputStream
|
||||
* @author Him188moe
|
||||
*/
|
||||
@PacketId("03 88")
|
||||
class ClientTryGetImageIDPacket(
|
||||
class ClientTryGetImageIDPacketJvm(
|
||||
private val botNumber: Long,
|
||||
private val sessionKey: ByteArray,
|
||||
private val groupNumberOrQQNumber: Long,
|
||||
private val image: BufferedImage
|
||||
private val image: PlatformImage
|
||||
) : ClientPacket() {
|
||||
override fun encode() {
|
||||
override fun encode(builder: BytePacketBuilder) = with(builder) {
|
||||
this.writeRandom(2)
|
||||
|
||||
this.writeQQ(botNumber)
|
||||
@ -55,7 +58,7 @@ class ClientTryGetImageIDPacket(
|
||||
|
||||
writeHex("22")
|
||||
writeHex("10")
|
||||
write(md5(byteArray))
|
||||
writeFully(md5(byteArray))
|
||||
|
||||
writeHex("28")
|
||||
writeUVarInt(byteArray.size.toUInt())//E2 0D
|
||||
@ -91,42 +94,4 @@ class ClientTryGetImageIDPacket(
|
||||
writeHex("00")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ServerTryGetImageIDResponsePacket(input: DataInputStream) : ServerPacket(input) {
|
||||
|
||||
class Encrypted(input: DataInputStream) : ServerPacket(input) {
|
||||
fun decrypt(sessionKey: ByteArray): ServerTryGetImageIDResponsePacket {
|
||||
val data = this.decryptAsByteArray(sessionKey)
|
||||
println(data.size)
|
||||
println(data.size)
|
||||
if (data.size == 209) {
|
||||
return ServerTryGetImageIDSuccessPacket(data.dataInputStream()).setId(this.idHex)
|
||||
}
|
||||
|
||||
return ServerTryGetImageIDFailedPacket(data.dataInputStream())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器未存有图片, 返回一个 key 用于客户端上传
|
||||
*/
|
||||
class ServerTryGetImageIDSuccessPacket(input: DataInputStream) : ServerTryGetImageIDResponsePacket(input) {
|
||||
lateinit var uKey: ByteArray
|
||||
|
||||
|
||||
override fun decode() {
|
||||
this.input.gotoWhere(ubyteArrayOf(0x42u, 0x80u, 0x01u))
|
||||
uKey = this.input.readNBytes(128)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器已经存有这个图片
|
||||
*/
|
||||
class ServerTryGetImageIDFailedPacket(input: DataInputStream) : ServerTryGetImageIDResponsePacket(input) {
|
||||
override fun decode() {
|
||||
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.action
|
||||
|
||||
import net.mamoe.mirai.message.MessageChain
|
||||
import net.mamoe.mirai.message.internal.toByteArray
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.utils.dataEncode
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
import java.io.DataInputStream
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
@PacketId("00 02")
|
||||
class ClientSendGroupMessagePacket(
|
||||
private val botQQ: Long,
|
||||
private val groupId: Long,//不是 number
|
||||
private val sessionKey: ByteArray,
|
||||
private val message: MessageChain
|
||||
) : ClientPacket() {
|
||||
override fun encode() {
|
||||
this.writeRandom(2)//part of packet id
|
||||
this.writeQQ(botQQ)
|
||||
this.writeHex(TIMProtocol.fixVer2)
|
||||
|
||||
this.encryptAndWrite(sessionKey) {
|
||||
val bytes = message.toByteArray()
|
||||
writeByte(0x2A)
|
||||
writeGroup(groupId)
|
||||
|
||||
writeLVByteArray(dataEncode { child ->
|
||||
child.writeHex("00 01 01")
|
||||
child.writeHex("00 00 00 00 00 00 00 4D 53 47 00 00 00 00 00")
|
||||
|
||||
child.writeTime()
|
||||
child.writeRandom(4)
|
||||
child.writeHex("00 00 00 00 09 00 86")
|
||||
child.writeHex(TIMProtocol.messageConst1)
|
||||
child.writeZero(2)
|
||||
|
||||
//messages
|
||||
child.write(bytes)
|
||||
})
|
||||
/*it.writeByte(0x01)
|
||||
it.writeShort(bytes.size + 3)
|
||||
it.writeByte(0x01)
|
||||
it.writeShort(bytes.size)
|
||||
it.write(bytes)*/
|
||||
|
||||
println(toByteArray().toUHexString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PacketId("00 02")
|
||||
class ServerSendGroupMessageResponsePacket(input: DataInputStream) : ServerPacket(input)
|
@ -1,12 +0,0 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import java.io.DataInputStream
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
class ServerLoginResponseFailedPacket(val loginState: LoginState, input: DataInputStream) : ServerPacket(input) {
|
||||
override fun decode() {
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.goto
|
||||
import net.mamoe.mirai.utils.Tested
|
||||
import java.io.DataInputStream
|
||||
|
||||
/**
|
||||
* 服务器进行加密后返回 privateKey
|
||||
*
|
||||
* @author NaturalHG
|
||||
*/
|
||||
@PacketId("08 36 31 03")
|
||||
class ServerLoginResponseKeyExchangePacket(input: DataInputStream, val flag: Flag) : ServerPacket(input) {
|
||||
enum class Flag {
|
||||
`08 36 31 03`,
|
||||
OTHER,
|
||||
}
|
||||
|
||||
lateinit var tlv0006: ByteArray//120bytes
|
||||
var tokenUnknown: ByteArray? = null
|
||||
lateinit var privateKeyUpdate: ByteArray//16bytes
|
||||
|
||||
@Tested
|
||||
override fun decode() {
|
||||
this.input.skip(5)
|
||||
privateKeyUpdate = this.input.readNBytes(16)//22
|
||||
//this.input.skip(2)//25
|
||||
this.input.goto(25)
|
||||
tlv0006 = this.input.readNBytes(120)
|
||||
|
||||
when (flag) {
|
||||
Flag.`08 36 31 03` -> {
|
||||
tokenUnknown = this.input.goto(153).readNBytes(56)
|
||||
//println(tokenUnknown!!.toUHexString())
|
||||
}
|
||||
|
||||
Flag.OTHER -> {
|
||||
//do nothing in this packet.
|
||||
//[this.token] will be set in [BotNetworkHandler]
|
||||
//token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Encrypted(input: DataInputStream, private val flag: Flag) : ServerPacket(input) {
|
||||
|
||||
@Tested
|
||||
fun decrypt(privateKey: ByteArray): ServerLoginResponseKeyExchangePacket {
|
||||
return ServerLoginResponseKeyExchangePacket(this.decryptBy(TIMProtocol.shareKey, privateKey), flag).setId(this.idHex)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.goto
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.readNBytesAt
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.readString
|
||||
import net.mamoe.mirai.utils.Tested
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
import java.io.DataInputStream
|
||||
|
||||
/**
|
||||
* @author NaturalHG
|
||||
*/
|
||||
class ServerLoginResponseSuccessPacket(input: DataInputStream) : ServerPacket(input) {
|
||||
lateinit var sessionResponseDecryptionKey: ByteArray//16 bytes|
|
||||
lateinit var nickname: String
|
||||
|
||||
lateinit var token38: ByteArray
|
||||
lateinit var token88: ByteArray
|
||||
lateinit var encryptionKey: ByteArray
|
||||
|
||||
|
||||
@Tested
|
||||
override fun decode() {
|
||||
this.input.skip(7)//8
|
||||
this.encryptionKey = this.input.readNBytes(16)//24
|
||||
|
||||
this.input.skip(2)//26
|
||||
this.token38 = this.input.readNBytes(56)//82
|
||||
|
||||
this.input.skip(60L)//142
|
||||
val msgLength = when (val id = this.input.readNBytes(2).toUByteArray().toUHexString()) {
|
||||
"01 07" -> 0
|
||||
"00 33" -> 28
|
||||
"01 10" -> 64
|
||||
else -> throw IllegalStateException(id)
|
||||
}
|
||||
|
||||
this.sessionResponseDecryptionKey = this.input.readNBytesAt(171 + msgLength, 16)
|
||||
|
||||
this.token88 = this.input.readNBytesAt(189 + msgLength, 136)
|
||||
|
||||
val nickLength = this.input.goto(624 + msgLength).readByte().toInt()
|
||||
this.nickname = this.input.readString(nickLength)
|
||||
|
||||
//this.age = this.input.goto(packetDataLength - 28).readShortAt()
|
||||
|
||||
//this.gender = this.input.goto(packetDataLength - 32).readByteAt().toInt()
|
||||
}
|
||||
|
||||
|
||||
class Encrypted(input: DataInputStream) : ServerPacket(input) {
|
||||
|
||||
fun decrypt(privateKey: ByteArray): ServerLoginResponseSuccessPacket {
|
||||
input.goto(14)
|
||||
return ServerLoginResponseSuccessPacket(this.decryptBy(TIMProtocol.shareKey, privateKey)).setId(this.idHex)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.dataInputStream
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.goto
|
||||
import net.mamoe.mirai.utils.Tested
|
||||
import net.mamoe.mirai.utils.hexToUBytes
|
||||
import java.io.DataInputStream
|
||||
|
||||
/**
|
||||
* 收到这个包意味着需要验证码登录, 并且能得到验证码图片文件的一部分
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
class ServerLoginResponseVerificationCodeInitPacket(input: DataInputStream, private val packetLength: Int) : ServerPacket(input) {
|
||||
|
||||
lateinit var verifyCodePart1: ByteArray
|
||||
lateinit var token00BA: ByteArray
|
||||
var unknownBoolean: Boolean? = null
|
||||
|
||||
|
||||
@Tested
|
||||
override fun decode() {
|
||||
val verifyCodeLength = this.input.goto(78).readShort()//2bytes
|
||||
this.verifyCodePart1 = this.input.readNBytes(verifyCodeLength.toInt())
|
||||
|
||||
this.input.skip(1)
|
||||
|
||||
this.unknownBoolean = this.input.readByte().toInt() == 1
|
||||
|
||||
this.token00BA = this.input.goto(packetLength - 60).readNBytes(40)
|
||||
}
|
||||
|
||||
|
||||
class Encrypted(input: DataInputStream) : ServerPacket(input) {
|
||||
override fun decode() {
|
||||
|
||||
}
|
||||
|
||||
fun decrypt(): ServerLoginResponseVerificationCodeInitPacket = this.decryptAsByteArray(TIMProtocol.shareKey).let {
|
||||
ServerLoginResponseVerificationCodeInitPacket(it.dataInputStream(), it.size).setId(this.idHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val data = "FB 01 04 03 33 00 01 00 BA 02 03 2C 13 00 05 01 00 00 01 23 00 38 D5 01 05 8B 67 4D 52 5A FA 92 DB 99 18 D4 F0 72 03 E0 17 71 7C 8A 45 74 1F C3 2D F8 61 96 0D 93 0D 8C 51 95 70 F8 F9 CB B9 2D 5D BC 4F 5D 89 5F E7 59 8C E4 E5 A2 04 56 02 BC 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 00 82 00 00 00 35 08 03 00 00 00 BA 12 C3 02 00 00 00 04 67 41 4D 41 00 00 B1 8F 0B FC 61 05 00 00 00 01 73 52 47 42 00 AE CE 1C E9 00 00 00 45 50 4C 54 45 FE F6 ED E2 F1 DF F3 FF F2 11 77 48 FE FE F3 F1 F9 EA D7 FD E7 F8 F9 EC FC EF E7 E8 FF EE 2D 69 48 2A 8A 5D 29 7A 52 F0 ED E1 A9 C7 B1 65 96 79 AB E0 C2 C3 F0 D5 42 7D 5C 4A 99 72 89 AA 93 51 73 5C 6E BA 94 42 BD 7A 0B 00 00 09 C5 49 44 41 54 58 C3 AC 99 8B 76 AB 3A 12 44 91 D0 1B 10 08 04 FF FF A9 B3 5B 60 C7 AF 38 77 EE 0C EB C4 76 6C 07 95 BA AB AB AB 75 BA EE 1F 5D DE CB 63 08 C1 C7 A8 AD BC B6 31 46 6B 3B A3 79 94 E7 F3 63 79 E4 D9 98 89 37 7D F7 FF BB 3C 10 82 6F B7 F7 2A B6 5B 07 EF B5 16 60 DE 1B 63 3A 1F 7E BE 6D 0C 50 EC 09 E1 FE 76 B8 3F FC 6B 0C 1D 5B 37 1F 3E E1 AE 61 9A D8 B6 07 93 B7 56 4D 66 22 06 9D FC EE 5F BF F9 3F 81 90 BB 99 F3 49 E2 1D DA 43 38 13 44 14 8C 9D B4 8E E7 65 40 11 E4 BB FE 8C 5B 78 41 F1 AF 01 48 2E 08 6D 8B AD F1 BA 05 F9 C4 E0 25 E6 31 BA EB 2A 90 C3 74 26 FC 1E 80 CF 14 91 44 7F FC 39 FF D4 37 10 82 C0 B3 B0 67 BF 2E 5E 5B F4 46 E5 3A 5F D7 BE A6 54 17 2D 7C 0D D7 DA E1 7E 93 C7 E5 DB 9E 6E 9F FD A3 14 9C 7F 23 97 55 FA DC 6F 74 8A CC 04 49 03 3F DE 4E 5C 4A 95 9C 53 8D 7A EA 82 F9 10 77 7F EE E5 43 28 C2 9F 00 BA 5B C0 59 A4 CE DB BA AE DB BC 14 D5 2A 82 A5 83 E0 30 92 19 A8 99 36 A7 15 7C BC DF F6 BE D7 86 C0 7F C9 C5 ED 93 C7 1F B9 E0 56 CB AC 61 F5 5D 2E 00 F0 93 D2 5A 9D B6 B0 3F 4B 4D 5C B9 0F C1 EE 29 46 65 4E 40 46 E2 E4 1B 91 C3 A5 2E FE D3 7A 3C 84 0B E3 F3 E2 57 16 2F 08 B9 E6 5A 73 29 65 29 4E B9 25 CF 6B 9A 97 48 20 B2 35 46 1B 6D AD D6 9A 57 69 45 B2 1A 00 90 05 51 8D C7 24 F8 1B B9 FE A6 41 B8 83 6C 59 05 84 9F 44 0F 85 05 9A 97 5A 97 BC A6 6D 81 FE 59 DE 2F 4B 5E E4 DF B2 A4 19 AA 06 D9 FE F9 33 4D 7E 6A 40 FC 97 34 BF 84 E4 81 81 ED E9 DC 85 32 56 47 E5 A4 F0 2D 6F 4D 2A BA 65 4B 73 89 B6 58 5E D7 35 8D 69 E4 4A 6B 76 50 C1 5C 3A D9 59 11 CF 37 99 FA 48 88 70 7F F4 9F 22 12 F2 24 91 3E 2B BF 28 A5 34 68 C0 50 A3 55 DD A4 E3 9C 6E 85 99 95 B6 24 2E 18 D9 3C 5C B1 4D AA 2F 08 E1 75 F1 F0 6B 49 FC BC E3 8D 00 01 00 28 42 E6 18 57 D4 B1 4D AE 51 27 D5 EF A2 38 91 39 15 37 6C 5A FE 75 93 49 DB FC 57 3C 12 3F 26 D9 16 1D 83 45 8B 78 39 D8 01 15 00 10 F6 F0 50 03 74 BB 18 91 D3 55 8D 7F BB 53 15 7A".hexToUBytes().toByteArray()
|
||||
ServerLoginResponseVerificationCodeInitPacket(
|
||||
data.dataInputStream(),
|
||||
data.size
|
||||
).let { it.decode(); println(it) }
|
||||
}
|
||||
|
||||
/*
|
||||
data
|
||||
FB 01 04 03 33 00 01 00 BA 02 03 2C 13 00 05 01 00 00 01 23 00 38 D5 01 05 8B 67 4D 52 5A FA 92 DB 99 18 D4 F0 72 03 E0 17 71 7C 8A 45 74 1F C3 2D F8 61 96 0D 93 0D 8C 51 95 70 F8 F9 CB B9 2D 5D BC 4F 5D 89 5F E7 59 8C E4 E5 A2 04 56 02 BC 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 00 82 00 00 00 35 08 03 00 00 00 BA 12 C3 02 00 00 00 04 67 41 4D 41 00 00 B1 8F 0B FC 61 05 00 00 00 01 73 52 47 42 00 AE CE 1C E9 00 00 00 45 50 4C 54 45 FE F6 ED E2 F1 DF F3 FF F2 11 77 48 FE FE F3 F1 F9 EA D7 FD E7 F8 F9 EC FC EF E7 E8 FF EE 2D 69 48 2A 8A 5D 29 7A 52 F0 ED E1 A9 C7 B1 65 96 79 AB E0 C2 C3 F0 D5 42 7D 5C 4A 99 72 89 AA 93 51 73 5C 6E BA 94 42 BD 7A 0B 00 00 09 C5 49 44 41 54 58 C3 AC 99 8B 76 AB 3A 12 44 91 D0 1B 10 08 04 FF FF A9 B3 5B 60 C7 AF 38 77 EE 0C EB C4 76 6C 07 95 BA AB AB AB 75 BA EE 1F 5D DE CB 63 08 C1 C7 A8 AD BC B6 31 46 6B 3B A3 79 94 E7 F3 63 79 E4 D9 98 89 37 7D F7 FF BB 3C 10 82 6F B7 F7 2A B6 5B 07 EF B5 16 60 DE 1B 63 3A 1F 7E BE 6D 0C 50 EC 09 E1 FE 76 B8 3F FC 6B 0C 1D 5B 37 1F 3E E1 AE 61 9A D8 B6 07 93 B7 56 4D 66 22 06 9D FC EE 5F BF F9 3F 81 90 BB 99 F3 49 E2 1D DA 43 38 13 44 14 8C 9D B4 8E E7 65 40 11 E4 BB FE 8C 5B 78 41 F1 AF 01 48 2E 08 6D 8B AD F1 BA 05 F9 C4 E0 25 E6 31 BA EB 2A 90 C3 74 26 FC 1E 80 CF 14 91 44 7F FC 39 FF D4 37 10 82 C0 B3 B0 67 BF 2E 5E 5B F4 46 E5 3A 5F D7 BE A6 54 17 2D 7C 0D D7 DA E1 7E 93 C7 E5 DB 9E 6E 9F FD A3 14 9C 7F 23 97 55 FA DC 6F 74 8A CC 04 49 03 3F DE 4E 5C 4A 95 9C 53 8D 7A EA 82 F9 10 77 7F EE E5 43 28 C2 9F 00 BA 5B C0 59 A4 CE DB BA AE DB BC 14 D5 2A 82 A5 83 E0 30 92 19 A8 99 36 A7 15 7C BC DF F6 BE D7 86 C0 7F C9 C5 ED 93 C7 1F B9 E0 56 CB AC 61 F5 5D 2E 00 F0 93 D2 5A 9D B6 B0 3F 4B 4D 5C B9 0F C1 EE 29 46 65 4E 40 46 E2 E4 1B 91 C3 A5 2E FE D3 7A 3C 84 0B E3 F3 E2 57 16 2F 08 B9 E6 5A 73 29 65 29 4E B9 25 CF 6B 9A 97 48 20 B2 35 46 1B 6D AD D6 9A 57 69 45 B2 1A 00 90 05 51 8D C7 24 F8 1B B9 FE A6 41 B8 83 6C 59 05 84 9F 44 0F 85 05 9A 97 5A 97 BC A6 6D 81 FE 59 DE 2F 4B 5E E4 DF B2 A4 19 AA 06 D9 FE F9 33 4D 7E 6A 40 FC 97 34 BF 84 E4 81 81 ED E9 DC 85 32 56 47 E5 A4 F0 2D 6F 4D 2A BA 65 4B 73 89 B6 58 5E D7 35 8D 69 E4 4A 6B 76 50 C1 5C 3A D9 59 11 CF 37 99 FA 48 88 70 7F F4 9F 22 12 F2 24 91 3E 2B BF 28 A5 34 68 C0 50 A3 55 DD A4 E3 9C 6E 85 99 95 B6 24 2E 18 D9 3C 5C B1 4D AA 2F 08 E1 75 F1 F0 6B 49 FC BC E3 8D 00 01 00 28 42 E6 18 57 D4 B1 4D AE 51 27 D5 EF A2 38 91 39 15 37 6C 5A FE 75 93 49 DB FC 57 3C 12 3F 26 D9 16 1D 83 45 8B 78 39 D8 01 15 00 10 F6 F0 50 03 74 BB 18 91 D3 55 8D 7F BB 53 15 7A
|
||||
|
||||
length 700
|
||||
|
||||
verify code
|
||||
89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 00 82 00 00 00 35 08 03 00 00 00 BA 12 C3 02 00 00 00 04 67 41 4D 41 00 00 B1 8F 0B FC 61 05 00 00 00 01 73 52 47 42 00 AE CE 1C E9 00 00 00 45 50 4C 54 45 FE F6 ED E2 F1 DF F3 FF F2 11 77 48 FE FE F3 F1 F9 EA D7 FD E7 F8 F9 EC FC EF E7 E8 FF EE 2D 69 48 2A 8A 5D 29 7A 52 F0 ED E1 A9 C7 B1 65 96 79 AB E0 C2 C3 F0 D5 42 7D 5C 4A 99 72 89 AA 93 51 73 5C 6E BA 94 42 BD 7A 0B 00 00 09 C5 49 44 41 54 58 C3 AC 99 8B 76 AB 3A 12 44 91 D0 1B 10 08 04 FF FF A9 B3 5B 60 C7 AF 38 77 EE 0C EB C4 76 6C 07 95 BA AB AB AB 75 BA EE 1F 5D DE CB 63 08 C1 C7 A8 AD BC B6 31 46 6B 3B A3 79 94 E7 F3 63 79 E4 D9 98 89 37 7D F7 FF BB 3C 10 82 6F B7 F7 2A B6 5B 07 EF B5 16 60 DE 1B 63 3A 1F 7E BE 6D 0C 50 EC 09 E1 FE 76 B8 3F FC 6B 0C 1D 5B 37 1F 3E E1 AE 61 9A D8 B6 07 93 B7 56 4D 66 22 06 9D FC EE 5F BF F9 3F 81 90 BB 99 F3 49 E2 1D DA 43 38 13 44 14 8C 9D B4 8E E7 65 40 11 E4 BB FE 8C 5B 78 41 F1 AF 01 48 2E 08 6D 8B AD F1 BA 05 F9 C4 E0 25 E6 31 BA EB 2A 90 C3 74 26 FC 1E 80 CF 14 91 44 7F FC 39 FF D4 37 10 82 C0 B3 B0 67 BF 2E 5E 5B F4 46 E5 3A 5F D7 BE A6 54 17 2D 7C 0D D7 DA E1 7E 93 C7 E5 DB 9E 6E 9F FD A3 14 9C 7F 23 97 55 FA DC 6F 74 8A CC 04 49 03 3F DE 4E 5C 4A 95 9C 53 8D 7A EA 82 F9 10 77 7F EE E5 43 28 C2 9F 00 BA 5B C0 59 A4 CE DB BA AE DB BC 14 D5 2A 82 A5 83 E0 30 92 19 A8 99 36 A7 15 7C BC DF F6 BE D7 86 C0 7F C9 C5 ED 93 C7 1F B9 E0 56 CB AC 61 F5 5D 2E 00 F0 93 D2 5A 9D B6 B0 3F 4B 4D 5C B9 0F C1 EE 29 46 65 4E 40 46 E2 E4 1B 91 C3 A5 2E FE D3 7A 3C 84 0B E3 F3 E2 57 16 2F 08 B9 E6 5A 73 29 65 29 4E B9 25 CF 6B 9A 97 48 20 B2 35 46 1B 6D AD D6 9A 57 69 45 B2 1A 00 90 05 51 8D C7 24 F8 1B B9 FE A6 41 B8 83 6C 59 05 84 9F 44 0F 85 05 9A 97 5A 97 BC A6 6D 81 FE 59 DE 2F 4B 5E E4 DF B2 A4 19 AA 06 D9 FE F9 33 4D 7E 6A 40 FC 97 34 BF 84 E4 81 81 ED E9 DC 85 32 56 47 E5 A4 F0 2D 6F 4D 2A BA 65 4B 73 89 B6 58 5E D7 35 8D 69 E4 4A 6B 76 50 C1 5C 3A D9 59 11 CF 37 99 FA 48 88 70 7F F4 9F 22 12 F2 24 91 3E 2B BF 28 A5 34 68 C0 50 A3 55 DD A4 E3 9C 6E 85 99 95 B6 24 2E 18 D9 3C 5C B1 4D AA 2F 08 E1 75 F1 F0 6B 49 FC BC E3 8D
|
||||
|
||||
token00ba
|
||||
42 E6 18 57 D4 B1 4D AE 51 27 D5 EF A2 38 91 39 15 37 6C 5A FE 75 93 49 DB FC 57 3C 12 3F 26 D9 16 1D 83 45 8B 78 39 D8
|
||||
*/
|
@ -1,112 +0,0 @@
|
||||
package net.mamoe.mirai.network.protocol.tim.packet.login
|
||||
|
||||
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.tim.packet.*
|
||||
import net.mamoe.mirai.utils.dataEncode
|
||||
import java.io.DataInputStream
|
||||
import java.net.InetAddress
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
@PacketId("08 28 04 34")
|
||||
class ClientSessionRequestPacket(
|
||||
private val qq: Long,
|
||||
private val serverIp: String,
|
||||
private val token38: ByteArray,
|
||||
private val token88: ByteArray,
|
||||
private val encryptionKey: ByteArray
|
||||
) : ClientPacket() {
|
||||
override fun encode() {
|
||||
this.writeQQ(qq)
|
||||
this.writeHex("02 00 00 00 01 2E 01 00 00 68 52 00 30 00 3A")
|
||||
this.writeHex("00 38")
|
||||
this.write(token38)
|
||||
this.encryptAndWrite(encryptionKey) {
|
||||
writeHex("00 07 00 88")
|
||||
write(token88)
|
||||
writeHex("00 0C 00 16 00 02 00 00 00 00 00 00 00 00 00 00")
|
||||
writeIP(serverIp)
|
||||
writeHex("1F 40 00 00 00 00 00 15 00 30 00 01")//fix1
|
||||
writeHex("01 92 A5 D2 59 00 10 54 2D CF 9B 60 BF BB EC 0D D4 81 CE 36 87 DE 35 02 AE 6D ED DC 00 10 ")
|
||||
writeHex("06 A9 12 97 B7 F8 76 25 AF AF D3 EA B4 C8 BC E7")//fix0836
|
||||
writeHex("00 36 00 12 00 02 00 01 00 00 00 05 00 00 00 00 00 00 00 00 00 00")
|
||||
writeHex(TIMProtocol.constantData1)
|
||||
writeHex(TIMProtocol.constantData2)
|
||||
writeQQ(qq)
|
||||
writeHex("00 00 00 00 00 1F 00 22 00 01")
|
||||
writeHex("1A 68 73 66 E4 BA 79 92 CC C2 D4 EC 14 7C 8B AF 43 B0 62 FB 65 58 A9 EB 37 55 1D 26 13 A8 E5 3D")//device ID
|
||||
|
||||
//tlv0106
|
||||
writeHex("01 05 00 30")
|
||||
writeHex("00 01 01 02 00 14 01 01 00 10")
|
||||
writeRandom(16)
|
||||
writeHex("00 14 01 02 00 10")
|
||||
writeRandom(16)
|
||||
|
||||
writeHex("01 0B 00 85 00 02")
|
||||
writeHex("B9 ED EF D7 CD E5 47 96 7A B5 28 34 CA 93 6B 5C")//fix2
|
||||
writeRandom(1)
|
||||
writeHex("10 00 00 00 00 00 00 00 02")
|
||||
|
||||
//fix3
|
||||
writeHex("00 63 3E 00 63 02 04 03 06 02 00 04 00 52 D9 00 00 00 00 A9 58 3E 6D 6D 49 AA F6 A6 D9 33 0A E7 7E 36 84 03 01 00 00 68 20 15 8B 00 00 01 02 00 00 03 00 07 DF 00 0A 00 0C 00 01 00 04 00 03 00 04 20 5C 00")
|
||||
writeRandom(32)//md5 32
|
||||
writeHex("68")
|
||||
|
||||
writeHex("00 00 00 00 00 2D 00 06 00 01")
|
||||
writeIP(InetAddress.getLocalHost().hostAddress)//todo random to avoid being banned?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
@PacketId("08 28 04 34")
|
||||
class ServerSessionKeyResponsePacket(inputStream: DataInputStream, private val dataLength: Int) : ServerPacket(inputStream) {
|
||||
lateinit var sessionKey: ByteArray
|
||||
lateinit var tlv0105: ByteArray
|
||||
|
||||
|
||||
override fun decode() {
|
||||
when (dataLength) {
|
||||
407 -> {
|
||||
input.goto(25)
|
||||
sessionKey = input.readNBytes(16)
|
||||
}
|
||||
|
||||
439 -> {
|
||||
input.goto(63)
|
||||
sessionKey = input.readNBytes(16)
|
||||
}
|
||||
|
||||
512,
|
||||
527 -> {
|
||||
input.goto(63)
|
||||
sessionKey = input.readNBytes(16)
|
||||
tlv0105 = dataEncode {
|
||||
it.writeHex("01 05 00 88 00 01 01 02 00 40 02 01 03 3C 01 03 00 00")
|
||||
input.goto(dataLength - 122)
|
||||
it.write(input.readNBytes(56))
|
||||
it.writeHex("00 40 02 02 03 3C 01 03 00 00")
|
||||
input.goto(dataLength - 55)
|
||||
it.write(input.readNBytes(56))
|
||||
} //todo 这个 tlv0105似乎可以保存起来然后下次登录时使用.
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException(dataLength.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)
|
||||
|
||||
}
|
||||
|
||||
class Encrypted(inputStream: DataInputStream) : ServerPacket(inputStream) {
|
||||
fun decrypt(sessionResponseDecryptionKey: ByteArray): ServerSessionKeyResponsePacket = this.decryptAsByteArray(sessionResponseDecryptionKey).let {
|
||||
ServerSessionKeyResponsePacket(it.dataInputStream(), it.size).setId(this.idHex)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.io.core.IoBuffer
|
||||
import kotlinx.io.core.readBytes
|
||||
import java.awt.Image
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
/**
|
||||
* 让用户处理验证码
|
||||
*
|
||||
* @return 用户输入得到的验证码
|
||||
*/
|
||||
internal actual suspend fun solveCaptcha(captchaBuffer: IoBuffer): String? = captchaLock.withLock {
|
||||
val captcha = captchaBuffer.readBytes()
|
||||
withContext(Dispatchers.IO) {
|
||||
MiraiLogger.logCyan(ImageIO.read(captcha.inputStream()).createCharImg())
|
||||
}
|
||||
MiraiLogger.logCyan("需要验证码登录, 验证码为 4 字母")
|
||||
try {
|
||||
File(System.getProperty("user.dir") + "/temp/Captcha.png")
|
||||
.also { withContext(Dispatchers.IO) { it.createNewFile(); it.writeBytes(captcha) } }
|
||||
MiraiLogger.logCyan("若看不清字符图片, 请查看 Mirai 目录下 /temp/Captcha.png")
|
||||
} catch (e: Exception) {
|
||||
MiraiLogger.logCyan("无法写出验证码文件, 请尝试查看以上字符图片")
|
||||
}
|
||||
MiraiLogger.logCyan("若要更换验证码, 请直接回车")
|
||||
readLine()?.takeUnless { it.isEmpty() || it.length != 4 }
|
||||
}
|
||||
|
||||
private val captchaLock = Mutex()
|
||||
|
||||
|
||||
/**
|
||||
* @author NaturalHG
|
||||
*/
|
||||
@JvmOverloads
|
||||
internal fun BufferedImage.createCharImg(outputWidth: Int = 100, ignoreRate: Double = 0.95): String {
|
||||
/*
|
||||
* resize Image
|
||||
* */
|
||||
val newHeight = (this.height * (outputWidth.toDouble() / this.width)).toInt()
|
||||
val tmp = this.getScaledInstance(outputWidth, newHeight, Image.SCALE_SMOOTH)
|
||||
val image = BufferedImage(outputWidth, newHeight, BufferedImage.TYPE_INT_ARGB)
|
||||
val g2d = image.createGraphics()
|
||||
g2d.drawImage(tmp, 0, 0, null)
|
||||
|
||||
fun gray(rgb: Int): Int {
|
||||
val r = rgb and 0xff0000 shr 16
|
||||
val g = rgb and 0x00ff00 shr 8
|
||||
val b = rgb and 0x0000ff
|
||||
return (r * 30 + g * 59 + b * 11 + 50) / 100
|
||||
}
|
||||
|
||||
fun grayCompare(g1: Int, g2: Int): Boolean = min(g1, g2).toDouble() / max(g1, g2) >= ignoreRate
|
||||
|
||||
val background = gray(image.getRGB(0, 0))
|
||||
|
||||
return buildString(capacity = height) {
|
||||
|
||||
val lines = mutableListOf<StringBuilder>()
|
||||
|
||||
var minXPos = outputWidth
|
||||
var maxXPos = 0
|
||||
|
||||
for (y in 0 until image.height) {
|
||||
val builderLine = StringBuilder()
|
||||
for (x in 0 until image.width) {
|
||||
val gray = gray(image.getRGB(x, y))
|
||||
if (grayCompare(gray, background)) {
|
||||
builderLine.append(" ")
|
||||
} else {
|
||||
builderLine.append("#")
|
||||
if (x < minXPos) {
|
||||
minXPos = x
|
||||
}
|
||||
if (x > maxXPos) {
|
||||
maxXPos = x
|
||||
}
|
||||
}
|
||||
}
|
||||
if (builderLine.toString().isBlank()) {
|
||||
continue
|
||||
}
|
||||
lines.add(builderLine)
|
||||
}
|
||||
for (line in lines) {
|
||||
append(line.substring(minXPos, maxXPos)).append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,79 +0,0 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import java.awt.Image
|
||||
import java.awt.image.BufferedImage
|
||||
import java.util.*
|
||||
import java.util.concurrent.Callable
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Convert IMAGE into Chars that could shows in terminal
|
||||
*
|
||||
* @author NaturalHG
|
||||
*/
|
||||
class CharImageConverter @JvmOverloads constructor(
|
||||
/**
|
||||
* width should depends on the width of the terminal
|
||||
*/
|
||||
private var image: BufferedImage?, private val width: Int, private val ignoreRate: Double = 0.95) : Callable<String> {
|
||||
|
||||
override fun call(): String {
|
||||
/*
|
||||
* resize Image
|
||||
* */
|
||||
val newHeight = (this.image!!.height * (width.toDouble() / this.image!!.width)).toInt()
|
||||
val tmp = image!!.getScaledInstance(width, newHeight, Image.SCALE_SMOOTH)
|
||||
val dimg = BufferedImage(width, newHeight, BufferedImage.TYPE_INT_ARGB)
|
||||
val g2d = dimg.createGraphics()
|
||||
g2d.drawImage(tmp, 0, 0, null)
|
||||
this.image = dimg
|
||||
|
||||
val background = gray(image!!.getRGB(0, 0))
|
||||
|
||||
val builder = StringBuilder()
|
||||
|
||||
val lines = ArrayList<StringBuilder>(this.image!!.height)
|
||||
|
||||
var minXPos = this.width
|
||||
var maxXPos = 0
|
||||
|
||||
for (y in 0 until image!!.height) {
|
||||
val builderLine = StringBuilder()
|
||||
for (x in 0 until image!!.width) {
|
||||
val gray = gray(image!!.getRGB(x, y))
|
||||
if (grayCompare(gray, background)) {
|
||||
builderLine.append(" ")
|
||||
} else {
|
||||
builderLine.append("#")
|
||||
if (x < minXPos) {
|
||||
minXPos = x
|
||||
}
|
||||
if (x > maxXPos) {
|
||||
maxXPos = x
|
||||
}
|
||||
}
|
||||
}
|
||||
if (builderLine.toString().isBlank()) {
|
||||
continue
|
||||
}
|
||||
lines.add(builderLine)
|
||||
}
|
||||
for (line in lines) {
|
||||
builder.append(line.substring(minXPos, maxXPos)).append("\n")
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun gray(rgb: Int): Int {
|
||||
val R = rgb and 0xff0000 shr 16
|
||||
val G = rgb and 0x00ff00 shr 8
|
||||
val B = rgb and 0x0000ff
|
||||
return (R * 30 + G * 59 + B * 11 + 50) / 100
|
||||
}
|
||||
|
||||
fun grayCompare(g1: Int, g2: Int): Boolean {
|
||||
return min(g1, g2).toDouble() / max(g1, g2) >= ignoreRate
|
||||
}
|
||||
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
|
||||
/**
|
||||
* @author NaturalHG
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun BufferedImage.createCharImg(sizeWeight: Int = 100): String {
|
||||
return CharImageConverter(this, sizeWeight).call()
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package net.mamoe.mirai.utils;
|
||||
|
||||
/**
|
||||
* @author NaturalHG
|
||||
*/
|
||||
public class EventException extends RuntimeException {
|
||||
private final Throwable cause;
|
||||
|
||||
public EventException(Throwable throwable) {
|
||||
cause = throwable;
|
||||
}
|
||||
|
||||
|
||||
public EventException() {
|
||||
cause = null;
|
||||
}
|
||||
|
||||
|
||||
public EventException(Throwable cause, String message) {
|
||||
super(message);
|
||||
this.cause = cause;
|
||||
}
|
||||
|
||||
|
||||
public EventException(String message) {
|
||||
super(message);
|
||||
cause = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable getCause() {
|
||||
return cause;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
/**
|
||||
* @author NaturalHG
|
||||
@ -29,3 +32,6 @@ object ImageNetworkUtils {
|
||||
return conn.responseCode == 200
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun BufferedImage.toByteArray(): ByteArray = ByteArrayOutputStream().use { ImageIO.write(this, "JPG", it); it.toByteArray() }
|
@ -1,60 +0,0 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author Him188moe
|
||||
*/
|
||||
interface MiraiLogger {
|
||||
companion object : MiraiLogger by defaultLogger()
|
||||
|
||||
var identity: String
|
||||
|
||||
fun info(any: Any?) = log(any)
|
||||
fun log(any: Any?)
|
||||
|
||||
fun error(any: Any?)
|
||||
|
||||
fun debug(any: Any?)
|
||||
|
||||
fun cyan(any: Any?)
|
||||
|
||||
fun purple(any: Any?)
|
||||
|
||||
fun green(any: Any?)
|
||||
|
||||
fun blue(any: Any?)
|
||||
}
|
||||
|
||||
/**
|
||||
* 由 mirai-console 或 mirai-web 等模块实现
|
||||
*/
|
||||
var defaultLogger: () -> MiraiLogger = { Console() }
|
||||
|
||||
val DEBUGGING: Boolean by lazy {
|
||||
//avoid inspections
|
||||
true
|
||||
}
|
||||
|
||||
open class Console(
|
||||
override var identity: String = "[Unknown]"
|
||||
) : MiraiLogger {
|
||||
override fun green(any: Any?) = print(any.toString(), LoggerTextFormat.GREEN)
|
||||
override fun purple(any: Any?) = print(any.toString(), LoggerTextFormat.LIGHT_PURPLE)
|
||||
override fun blue(any: Any?) = print(any.toString(), LoggerTextFormat.BLUE)
|
||||
override fun cyan(any: Any?) = print(any.toString(), LoggerTextFormat.LIGHT_CYAN)
|
||||
override fun error(any: Any?) = print(any.toString(), LoggerTextFormat.RED)
|
||||
override fun log(any: Any?) = print(any.toString(), LoggerTextFormat.LIGHT_GRAY)
|
||||
override fun debug(any: Any?) {
|
||||
if (DEBUGGING) {
|
||||
print(any.toString(), LoggerTextFormat.YELLOW)
|
||||
}
|
||||
}
|
||||
|
||||
fun print(value: String?, color: LoggerTextFormat = LoggerTextFormat.YELLOW) {
|
||||
val s = SimpleDateFormat("MM-dd HH:mm:ss").format(Date())
|
||||
|
||||
println("$color$identity $s : $value")
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user