Multiplatform

This commit is contained in:
Him188 2019-10-13 20:19:54 +08:00
parent c09d44f45a
commit d89f8acc43
110 changed files with 3101 additions and 3081 deletions

View File

@ -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"
}

View File

@ -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
}

View File

@ -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'
}*/
}

View File

@ -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)
}
}

View File

@ -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())
}

View File

@ -1,9 +1,5 @@
package net.mamoe.mirai
//expect fun s(): String
/**
* @author Him188moe
*/
object Mirai {
const val VERSION: String = "1.0.0"
}

View File

@ -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 {//求你别出错

View File

@ -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)
}

View File

@ -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

View File

@ -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 "停止"
* }
* }
* ```

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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
}
}
}

View File

@ -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`

View File

@ -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),

View File

@ -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)

View File

@ -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")
}
})
}
}

View File

@ -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())
}
}

View File

@ -0,0 +1,3 @@
package net.mamoe.mirai.network
class HandlerClosedException : Exception()

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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()
}

View File

@ -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() {

View File

@ -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()
}

View File

@ -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
}

View File

@ -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")

View File

@ -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

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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

View File

@ -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(

View File

@ -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 {

View File

@ -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
}

View File

@ -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
值都是一样的.
*/

View File

@ -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() {
}
}

View File

@ -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))
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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")
}
}

View File

@ -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()
}

View File

@ -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,
/**
* 超时
*/

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -1,8 +1,6 @@
package net.mamoe.mirai.utils
/**
* @author Him188moe
*/
data class BotAccount(
val qqNumber: Long,//实际上是 UInt
val password: String//todo 不保存 password?

View File

@ -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)

View File

@ -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)
}

View File

@ -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())

View File

@ -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?

View File

@ -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()

View File

@ -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())
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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 }
}
}

View File

@ -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?
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()
}

View File

@ -1,3 +0,0 @@
package net.mamoe.mirai.utils
//todo

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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>)
}
}
}

View File

@ -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
}
}
}

View File

@ -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) {

View File

@ -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")
}
})
}
}

View File

@ -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()
}

View File

@ -1,6 +0,0 @@
package net.mamoe.mirai.network
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
object NetworkScope : CoroutineScope by CoroutineScope(Dispatchers.Default)

View File

@ -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
}
}
}

View File

@ -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())
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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() {
}
}

View File

@ -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)

View File

@ -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() {
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
*/

View File

@ -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)
}
}
}

View File

@ -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")
}
}
}

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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;
}
}

View File

@ -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() }

View File

@ -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