Commit 73cda9df authored by Him188's avatar Him188

Remove TIMPC

parent bc860a2d
......@@ -33,7 +33,7 @@ kotlin {
sourceSets["main"].apply {
dependencies {
implementation(project(":mirai-core-timpc"))
implementation(project(":mirai-core-qqandroid"))
implementation(kotlin("stdlib-jdk8", kotlinVersion))
implementation(kotlin("stdlib-jdk7", kotlinVersion))
......
......@@ -24,8 +24,8 @@ fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
dependencies {
api(project(":mirai-core"))
api(project(":mirai-core-timpc"))
runtimeOnly(files("../mirai-core-timpc/build/classes/kotlin/jvm/main"))
api(project(":mirai-core-qqandroid"))
runtimeOnly(files("../mirai-core-qqandroid/build/classes/kotlin/jvm/main"))
runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main"))
api(kotlin("serialization"))
// classpath is not set correctly by IDE
......
# mirai-core-timpc
TIM PC 协议实现
## Bot
`TIMPC : BotFactory`: [`TIMPC.kt`](src/commonMain/net.mamoe.mirai.timpc/TIMPC.kt)
## Extra features
相对 `mirai-core`, TIM PC 协议额外提供:
- Android 客户端 上线/离线 `AndroidDeviceStatusChangePacket`
\ No newline at end of file
@file:Suppress("UNUSED_VARIABLE")
plugins {
kotlin("multiplatform")
id("kotlinx-atomicfu")
id("kotlinx-serialization")
`maven-publish`
id("com.jfrog.bintray") version "1.8.4-jetbrains-3" // DO NOT CHANGE THIS VERSION UNLESS YOU WANT TO WASTE YOUR TIME
}
apply(from = rootProject.file("gradle/publish.gradle"))
val kotlinVersion: String by rootProject.ext
val atomicFuVersion: String by rootProject.ext
val coroutinesVersion: String by rootProject.ext
val kotlinXIoVersion: String by rootProject.ext
val coroutinesIoVersion: String by rootProject.ext
val klockVersion: String by rootProject.ext
val ktorVersion: String by rootProject.ext
val serializationVersion: String by rootProject.ext
fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
description = "QQ protocol library"
version = rootProject.ext.get("mirai_version")!!.toString()
val isAndroidSDKAvailable: Boolean by project
kotlin {
if (isAndroidSDKAvailable) {
apply(from = rootProject.file("gradle/android.gradle"))
android("android") {
publishAllLibraryVariants()
}
} else {
println(
"""Android SDK 可能未安装.
$name 的 Android 目标编译将不会进行.
这不会影响 Android 以外的平台的编译.
""".trimIndent()
)
println(
"""Android SDK might not be installed.
Android target of $name will not be compiled.
It does no influence on the compilation of other platforms.
""".trimIndent()
)
}
jvm("jvm") {
}
sourceSets {
all {
languageSettings.enableLanguageFeature("InlineClasses")
languageSettings.useExperimentalAnnotation("kotlin.Experimental")
dependencies {
api(project(":mirai-core"))
api(kotlin("stdlib", kotlinVersion))
api(kotlin("serialization", kotlinVersion))
api("org.jetbrains.kotlinx:atomicfu:$atomicFuVersion")
api(kotlinx("io", kotlinXIoVersion))
api(kotlinx("coroutines-io", coroutinesIoVersion))
api(kotlinx("coroutines-core", coroutinesVersion))
}
}
commonMain {
dependencies {
}
}
commonTest {
dependencies {
api(kotlin("test-annotations-common"))
api(kotlin("test-common"))
}
}
if (isAndroidSDKAvailable) {
val androidMain by getting {
dependencies {
}
}
val androidTest by getting {
dependencies {
api(kotlin("test", kotlinVersion))
api(kotlin("test-junit", kotlinVersion))
api(kotlin("test-annotations-common"))
api(kotlin("test-common"))
}
}
}
val jvmMain by getting {
dependencies {
}
}
val jvmTest by getting {
dependencies {
api(kotlin("test", kotlinVersion))
api(kotlin("test-junit", kotlinVersion))
implementation("org.pcap4j:pcap4j-distribution:1.8.2")
runtimeOnly(files("build/classes/kotlin/jvm/test")) // classpath is not properly set by IDE
}
}
}
}
\ No newline at end of file
@file:Suppress("unused")
package net.mamoe.mirai.timpc
import kotlinx.io.InputStream
import kotlinx.io.streams.inputStream
import net.mamoe.mirai.BotAccount
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.utils.BotConfiguration
internal actual class TIMPCBot actual constructor(
account: BotAccount,
configuration: BotConfiguration
) : TIMPCBotBase(account, configuration) {
suspend inline fun Image.downloadAsStream(): InputStream = download().inputStream()
}
\ No newline at end of file
package net.mamoe.mirai.timpc.network
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
/**
* 包处理协程调度器.
*
* JVM: 独立的 4 thread 调度器
*/
internal actual val NetworkDispatcher: CoroutineDispatcher
get() = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
\ No newline at end of file
@file:Suppress("FunctionName", "unused", "SpellCheckingInspection")
package net.mamoe.mirai.timpc
import net.mamoe.mirai.Bot
import net.mamoe.mirai.BotAccount
import net.mamoe.mirai.BotFactory
import net.mamoe.mirai.timpc.TIMPC.Bot
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.Context
import net.mamoe.mirai.utils.MiraiInternalAPI
/**
* TIM PC 协议的 [Bot] 构造器.
*/
@UseExperimental(MiraiInternalAPI::class)
object TIMPC : BotFactory {
/**
* 使用指定的 [配置][configuration] 构造 [Bot] 实例
*/
override fun Bot(context: Context, qq: Long, password: String, configuration: BotConfiguration): Bot = TIMPCBot(BotAccount(qq, password), configuration)
/**
* 使用指定的 [配置][configuration] 构造 [Bot] 实例
*/
fun Bot(qq: Long, password: String, configuration: BotConfiguration = BotConfiguration.Default): Bot = TIMPCBot(BotAccount(qq, password), configuration)
}
/**
* 使用指定的 [配置][configuration] 构造 [Bot] 实例
*/
inline fun TIMPC.Bot(qq: Long, password: String, configuration: (BotConfiguration.() -> Unit)): Bot =
this.Bot(qq, password, BotConfiguration().apply(configuration))
\ No newline at end of file
package net.mamoe.mirai.timpc.internal
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.timpc.TIMPCBot
import net.mamoe.mirai.timpc.network.packet.action.GroupPacket
import net.mamoe.mirai.utils.LockFreeLinkedList
import net.mamoe.mirai.utils.MiraiInternalAPI
data class RawGroupInfo(
val group: Long,
val owner: Long,
val name: String,
val announcement: String,
/**
* 含群主
*/
val members: Map<Long, MemberPermission>
) : GroupPacket.InfoResponse {
@UseExperimental(MiraiInternalAPI::class)
fun parseBy(group: Group): GroupInfo = group.withBot {
this as? TIMPCBot ?: error("internal error: wrong Bot instance passed")
val memberList = LockFreeLinkedList<Member>()
members.forEach { entry: Map.Entry<Long, MemberPermission> ->
memberList.addLast(entry.key.qq().let { group.Member(it, entry.value) })
}
return GroupInfo(
group,
this@RawGroupInfo.owner.qq().let { group.Member(it, MemberPermission.OWNER) },
this@RawGroupInfo.name,
this@RawGroupInfo.announcement,
ContactList(memberList)
)
}
}
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "MemberVisibilityCanBePrivate", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.FriendNameRemark
import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.PreviousNameList
import net.mamoe.mirai.data.Profile
import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.message.data.ImageId
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.timpc.TIMPCBot
import net.mamoe.mirai.timpc.internal.RawGroupInfo
import net.mamoe.mirai.timpc.network.packet.action.*
import net.mamoe.mirai.timpc.network.packet.event.MemberJoinEventPacket
import net.mamoe.mirai.timpc.network.packet.event.MemberQuitEvent
import net.mamoe.mirai.timpc.sendPacket
import net.mamoe.mirai.timpc.utils.assertUnreachable
import net.mamoe.mirai.timpc.withTIMPCBot
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.toUHexString
import kotlin.coroutines.CoroutineContext
internal sealed class ContactImpl : Contact {
abstract override suspend fun sendMessage(message: MessageChain)
/**
* 开始监听事件, 以同步更新资料
*/
internal abstract fun startUpdater()
}
@Suppress("MemberVisibilityCanBePrivate", "CanBeParameter")
internal class GroupImpl internal constructor(bot: TIMPCBot, val groupId: GroupId, override val coroutineContext: CoroutineContext) :
ContactImpl(), Group, CoroutineScope {
override val bot: TIMPCBot by bot.unsafeWeakRef()
override val id: Long get() = groupId.value
override val internalId = GroupId(id).toInternalId()
internal lateinit var info: GroupInfo
internal lateinit var initialInfoJob: Job
override val owner: Member get() = info.owner
override val name: String get() = info.name
override val announcement: String get() = info.announcement
override val members: ContactList<Member> get() = info.members
@UseExperimental(MiraiInternalAPI::class)
override fun getMember(id: Long): Member =
members.delegate.filteringGetOrAdd({ it.id == id }) { MemberImpl(QQImpl(bot, id, coroutineContext), this, MemberPermission.MEMBER, coroutineContext) }
override suspend fun sendMessage(message: MessageChain) {
bot.sendPacket(GroupPacket.Message(bot.uin, internalId, bot.sessionKey, message))
}
override suspend fun uploadImage(image: ExternalImage): ImageId = withTIMPCBot {
val userContext = coroutineContext
val response = GroupImagePacket.RequestImageId(bot.uin, internalId, image, sessionKey).sendAndExpect<GroupImageResponse>()
withContext(userContext) {
when (response) {
is ImageUploadInfo -> response.uKey?.let { uKey ->
check(Http.postImage(
htcmd = "0x6ff0071",
uin = bot.uin,
groupId = GroupId(id),
imageInput = image.input,
inputSize = image.inputSize,
uKeyHex = uKey.toUHexString("").also { require(it.length == 128 * 2) { "Illegal uKey. expected size=256, actual size=${it.length}" } }
)) { "Group image upload failed: cannot access api" }
logger.verbose("group image uploaded")
} ?: logger.verbose("Group image upload: already exists")
// TODO: 2019/11/17 超过大小的情况
//is Overfile -> throw OverFileSizeMaxException()
else -> assertUnreachable()
}
}
return image.groupImageId
}
override suspend fun updateGroupInfo(): GroupInfo = withTIMPCBot {
GroupPacket.QueryGroupInfo(uin, internalId, sessionKey).sendAndExpect<RawGroupInfo>().parseBy(this@GroupImpl).also { info = it }
}
override suspend fun quit(): Boolean = withTIMPCBot {
GroupPacket.QuitGroup(uin, sessionKey, internalId).sendAndExpect<GroupPacket.QuitGroupResponse>().isSuccess
}
@UseExperimental(MiraiInternalAPI::class)
override fun startUpdater() {
subscribeAlways<MemberJoinEventPacket> {
members.delegate.addLast(it.member)
}
subscribeAlways<MemberQuitEvent> {
members.delegate.remove(it.member)
}
}
override fun toString(): String = "Group(${this.id})"
}
@PublishedApi
internal class QQImpl @PublishedApi internal constructor(bot: TIMPCBot, override val id: Long, override val coroutineContext: CoroutineContext) :
ContactImpl(),
QQ, CoroutineScope {
override val bot: TIMPCBot by bot.unsafeWeakRef()
override suspend fun sendMessage(message: MessageChain) =
bot.sendPacket(SendFriendMessagePacket(bot.uin, id, bot.sessionKey, message))
override suspend fun uploadImage(image: ExternalImage): ImageId = withTIMPCBot {
FriendImagePacket.RequestImageId(uin, sessionKey, id, image).sendAndExpect<FriendImageResponse>().let {
when (it) {
is FriendImageUKey -> {
check(
Http.postImage(
htcmd = "0x6ff0070",
uin = bot.uin,
groupId = null,
uKeyHex = it.uKey.toUHexString(""),
imageInput = image.input,
inputSize = image.inputSize
)
) { "Friend image upload failed: cannot access api" }
logger.verbose("friend image uploaded")
it.imageId
}
is FriendImageAlreadyExists -> it.imageId
is FriendImageOverFileSizeMax -> throw OverFileSizeMaxException()
else -> error("This shouldn't happen")
}
}
}
override val isOnline: Boolean
get() = true
override suspend fun queryProfile(): Profile = withTIMPCBot {
RequestProfileDetailsPacket(bot.uin, id, sessionKey).sendAndExpect<RequestProfileDetailsResponse>().profile
}
override suspend fun queryPreviousNameList(): PreviousNameList = withTIMPCBot {
QueryPreviousNamePacket(bot.uin, sessionKey, id).sendAndExpect()
}
override suspend fun queryRemark(): FriendNameRemark = withTIMPCBot {
QueryFriendRemarkPacket(bot.uin, sessionKey, id).sendAndExpect()
}
@PublishedApi
override fun startUpdater() {
// TODO: 2019/11/28 被删除好友事件
}
override fun toString(): String = "QQ(${this.id})"
}
/**
* 群成员
*/
@PublishedApi
internal data class MemberImpl(
private val delegate: QQ,
override val group: Group,
override val permission: MemberPermission,
override val coroutineContext: CoroutineContext
) : QQ by delegate, CoroutineScope, Member, ContactImpl() {
override fun toString(): String = "Member(id=${this.id}, group=${group.id}, permission=$permission)"
override suspend fun mute(durationSeconds: Int): Boolean = withTIMPCBot {
require(durationSeconds > 0) { "duration must be greater than 0 second" }
require(durationSeconds <= 30 * 24 * 3600) { "duration must be no more than 30 days" }
if (permission == MemberPermission.OWNER) return false
val operator = group.getMember(bot.uin)
check(operator.id != id) { "The bot is the owner of group ${group.id}, it cannot mute itself!" }
when (operator.permission) {
MemberPermission.MEMBER -> return false
MemberPermission.ADMINISTRATOR -> if (permission == MemberPermission.ADMINISTRATOR) return false
MemberPermission.OWNER -> {
}
}
GroupPacket.Mute(uin, group.internalId, sessionKey, id, durationSeconds.toUInt()).sendAndExpect<GroupPacket.MuteResponse>()
return true
}
@PublishedApi
override fun startUpdater() {
// TODO: 2019/12/6 更新群成员信息
}
override suspend fun unmute(): Unit = withTIMPCBot {
GroupPacket.Mute(uin, group.internalId, sessionKey, id, 0u).sendAndExpect<GroupPacket.MuteResponse>()
}
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.timpc.network
import net.mamoe.mirai.utils.io.hexToBytes
import net.mamoe.mirai.utils.solveIpAddress
object TIMProtocol {
val SERVER_IP: List<String> = {
//add("183.60.56.29")
arrayOf(
"sz3.tencent.com",
"sz4.tencent.com",
"sz5.tencent.com",
"sz6.tencent.com",
"sz8.tencent.com",
"sz9.tencent.com",
"sz2.tencent.com"
).map { solveIpAddress(it) } // 需 IPv4 地址
}()//不使用lazy, 在初始化时就加载.
val head = "02".hexToBytes()
val ver = "37 13".hexToBytes()// TIM 最新版中这个有时候是 38 03
val fixVer = "03 00 00 00 01 2E 01 00 00 68 52 00 00 00 00".hexToBytes()
val tail = "03".hexToBytes()
/**
* _fixVer
*/
val fixVer2 = "02 00 00 00 01 01 01 00 00 68 20".hexToBytes()
// 02 38 03 00 CD 48 68 3E 03 3F A2 02 00 00 00
val version0x02 = "02 00 00 00 01 2E 01 00 00 69 35".hexToBytes()
val version0x04 = "04 00 00 00 01 2E 01 00 00 69 35 00 00 00 00 00 00 00 00".hexToBytes()
val constantData1 = "00 18 00 16 00 01 ".hexToBytes()
val constantData2 = "00 00 04 53 00 00 00 01 00 00 15 85 ".hexToBytes()
//todo 使用 byte array
/**
* Touch 发出时写入, 并用于加密, 接受 sendTouch response 时解密.
*/
val touchKey = "A4 F1 91 88 C9 82 14 99 0C 9E 56 55 91 23 C8 3D".hexToBytes()//16
//统一替换为了 touchKey
///**
// * Redirection 发出时写入, 并用于加密, 接受 Redirection response 时解密.
// * 这个 key 似乎是可以任意的.
// */
//val redirectionKey = "A8 F2 14 5F 58 12 60 AF 07 63 97 D6 76 B2 1A 3B"//16
/**
* 并非常量. 设置为常量是为了让 [shareKey] 为常量
*/
val publicKey = "02 6D 28 41 D2 A5 6F D2 FC 3E 2A 1F 03 75 DE 6E 28 8F A8 19 3E 5F 16 49 D3".hexToBytes()//25
/**
* 并非常量. 设置为常量是为了让 [shareKey] 为常量
*
* LoginResend 和 PasswordSubmission 时写入, 但随后都使用 shareKey 加密, 收到回复也是用的 share key
*/
val key0836 = "EF 4A 36 6A 16 A8 E6 3D 2E EA BD 1F 98 C1 3C DA".hexToBytes()//16
/**
* 并非常量. 是 publicKey 与 key0836 的算法计算结果
*/
//val shareKey = "5B 6C 91 55 D9 92 F5 A7 99 85 37 76 3D 0F 08 B7"//16
val shareKey = "1A E9 7F 7D C9 73 75 98 AC 02 E0 80 5F A9 C6 AF".hexToBytes()//16//original
val key00BA = "C1 9C B8 C8 7B 8C 81 BA 9E 9E 7A 89 E1 7A EC 94".hexToBytes()
val key00BAFix = "69 20 D1 14 74 F5 B3 93 E4 D5 02 B3 71 1A CD 2A".hexToBytes()
/**
* 0836_622_fix2
*/
val passwordSubmissionTLV2 =
"00 15 00 30 00 01 01 27 9B C7 F5 00 10 65 03 FD 8B 00 00 00 00 00 00 00 00 00 00 00 00 02 90 49 55 33 00 10 15 74 C4 89 85 7A 19 F5 5E A9 C9 A3 5E 8A 5A 9B".hexToBytes()
/**
* 0836_622_fix1
*/
val passwordSubmissionTLV1 = "03 00 00 00 01 01 01 00 00 68 20 00 00 00 00 00 01 01 03".hexToBytes()//19
// 最新版 03 00 00 00 01 2E 01 00 00 69 35 00 00 00 00 00 02 01 03
// 第一版 1.0.2 03 00 00 00 01 2E 01 00 00 68 13 00 00 00 00 00 02 01 03
// 1.0.4 03 00 00 00 01 2E 01 00 00 68 27 00 00 00 00 00 02 01 03
// 1.1 03 00 00 00 01 2E 01 00 00 68 3F 00 00 00 00 00 02 01 03
// 1.2 03 00 00 00 01 2E 01 00 00 68 44 00 00 00 00 00 02 01 03
/**
* 发送/接受消息中的一个const (?)
* length=15
*/
val messageConst1 = "00 00 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91".hexToBytes()
val messageConstNewest = "22 00 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91".hexToBytes()
// TIM最新 22 00 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91
}
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.timpc.network.handler
import kotlinx.io.core.Closeable
import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.io.PlatformDatagramChannel
/**
* 网络接口.
* 发包 / 处理包.
* 仅可通过 [TIMBotNetworkHandler.socket] 得到实例.
*
* @author Him188moe
*/
interface DataPacketSocketAdapter : Closeable {
val owner: Bot
/**
* 连接的服务器的 IPv4 地址
* 在整个过程中都不会变化. 若连接丢失, [DataPacketSocketAdapter] 将会被 [close]
*/
val serverIp: String
/**
* UDP 通道
*/
@MiraiInternalAPI
val channel: PlatformDatagramChannel
/**
* 是否开启
*/
val isOpen: Boolean
override fun close()
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.handler
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.withContext
import net.mamoe.mirai.data.Packet
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.KClass
internal class TemporaryPacketHandler<P : Packet, R>(
private val expectationClass: KClass<P>,
private val deferred: CompletableDeferred<R>,
private val checkSequence: UShort? = null,
/**
* 调用者的 [CoroutineContext]. 包处理过程将会在这个 context 下运行
*/
private val callerContext: CoroutineContext,
private val handler: suspend (P) -> R
) {
internal fun filter(packet: Packet, sequenceId: UShort): Boolean =
expectationClass.isInstance(packet) && if (checkSequence != null) sequenceId == checkSequence else true
internal suspend inline fun doReceivePassingExceptionsToDeferred(packet: Packet) {
@Suppress("UNCHECKED_CAST")
val ret = try {
withContext(callerContext) {
handler(packet as P)
}
} catch (e: Throwable) {
deferred.completeExceptionally(e)
return
}
deferred.complete(ret)
}
}
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "MemberVisibilityCanBePrivate")
package net.mamoe.mirai.timpc.network.packet
import kotlinx.io.core.*
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.protobuf.ProtoBuf
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.cryptor.encryptAndWrite
import net.mamoe.mirai.utils.io.hexToBytes
import net.mamoe.mirai.utils.io.writeQQ
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmOverloads
/**
* 待发送给服务器的数据包. 它代表着一个 [ByteReadPacket],
*/
class OutgoingPacket(
name: String?,
val packetId: PacketId,
val sequenceId: UShort,
val delegate: ByteReadPacket
) {
val name: String by lazy {
name ?: packetId.toString()
}
}
/**
* 登录完成建立 session 之后发出的包.
* 均使用 sessionKey 加密
*
* @param TPacket invariant
*/
abstract class SessionPacketFactory<TPacket : Packet> : PacketFactory<TPacket, SessionKey>(
SessionKey
) {
/**
* 在 [BotNetworkHandler] 下处理这个包. 广播事件等.
*/
open suspend fun BotNetworkHandler.handlePacket(packet: TPacket) {}
}
/**
* 构造一个待发送给服务器的数据包.
*/
@UseExperimental(ExperimentalContracts::class, MiraiInternalAPI::class)
@JvmOverloads
inline fun PacketFactory<*, *>.buildOutgoingPacket0(
name: String? = null,
id: PacketId = this.id,
sequenceId: UShort = PacketFactory.atomicNextSequenceId(),
headerSizeHint: Int = 0,
head: ByteArray,
ver: ByteArray,
tail: ByteArray,
block: BytePacketBuilder.() -> Unit
): OutgoingPacket {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return OutgoingPacket(name, id, sequenceId, buildPacket(headerSizeHint) {
writeFully(head)
writeFully(ver)
writeUShort(id.value.toUShort())
writeUShort(sequenceId)
block(this)
writeFully(tail)
})
}
/**
* 构造一个待发送给服务器的会话数据包.
*/
@UseExperimental(ExperimentalContracts::class, MiraiInternalAPI::class)
@JvmOverloads
inline fun PacketFactory<*, *>.buildSessionPacket0(
bot: Long,
sessionKey: SessionKey,
name: String? = null,
id: PacketId = this.id,
sequenceId: UShort = PacketFactory.atomicNextSequenceId(),
headerSizeHint: Int = 0,
version: ByteArray, // in packet body
head: ByteArray,
ver: ByteArray, // in packet head
tail: ByteArray,
block: BytePacketBuilder.() -> Unit
): OutgoingPacket {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return buildOutgoingPacket0(
name = name,
id = id,
sequenceId = sequenceId,
headerSizeHint = headerSizeHint,
head = head,
ver = ver,
tail = tail
) {
writeQQ(bot)
writeFully(version)
encryptAndWrite(sessionKey) {
block()
}
}
}
/**
* 构造一个待发送给服务器的会话数据包.
*/
@UseExperimental(ExperimentalContracts::class, MiraiInternalAPI::class)
@JvmOverloads
fun <T> PacketFactory<*, *>.buildSessionProtoPacket0(
bot: Long,
sessionKey: SessionKey,
name: String? = null,
id: PacketId = this.id,
sequenceId: UShort = PacketFactory.atomicNextSequenceId(),
headerSizeHint: Int = 0,
version: ByteArray,
head: Any,
serializer: SerializationStrategy<T>,
protoObj: T,
packetHead: ByteArray,
ver: ByteArray, // in packet head
tail: ByteArray
): OutgoingPacket {
require(head is ByteArray || head is UByteArray || head is String) { "Illegal head type" }
return buildOutgoingPacket0(name, id, sequenceId, headerSizeHint, head = packetHead, ver = ver, tail = tail) {
writeQQ(bot)
writeFully(version)
encryptAndWrite(sessionKey) {
when (head) {
is ByteArray -> {
val proto = ProtoBuf.dump(serializer, protoObj)
writeInt(head.size)
writeInt(proto.size)
writeFully(head)
writeFully(proto)
}
is UByteArray -> {
val proto = ProtoBuf.dump(serializer, protoObj)
writeInt(head.size)
writeInt(proto.size)
writeFully(head)
writeFully(proto)
}
is String -> buildSessionProtoPacket0(
bot = bot,
sessionKey = sessionKey,
name = name,
id = id,
sequenceId = sequenceId,
headerSizeHint = headerSizeHint,
version = version,
head = head.hexToBytes(),
serializer = serializer,
protoObj = protoObj,
packetHead = packetHead,
ver = ver,
tail = tail
)
}
}
}
}
\ No newline at end of file
package net.mamoe.mirai.timpc.network.packet
import kotlinx.io.core.BytePacketBuilder
import kotlinx.serialization.SerializationStrategy
import net.mamoe.mirai.timpc.network.TIMProtocol
import net.mamoe.mirai.utils.MiraiInternalAPI
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmOverloads
/**
* 构造一个待发送给服务器的数据包.
*/
@UseExperimental(ExperimentalContracts::class, MiraiInternalAPI::class, ExperimentalUnsignedTypes::class)
@JvmOverloads
inline fun PacketFactory<*, *>.buildOutgoingPacket(
name: String? = null,
id: PacketId = this.id,
sequenceId: UShort = PacketFactory.atomicNextSequenceId(),
headerSizeHint: Int = 0,
block: BytePacketBuilder.() -> Unit
): OutgoingPacket {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return buildOutgoingPacket0(name, id, sequenceId, headerSizeHint, TIMProtocol.head, TIMProtocol.ver, TIMProtocol.tail, block)
}
/**
* 构造一个待发送给服务器的会话数据包.
*/
@UseExperimental(ExperimentalContracts::class, MiraiInternalAPI::class, ExperimentalUnsignedTypes::class)
@JvmOverloads
inline fun PacketFactory<*, *>.buildSessionPacket(
bot: Long,
sessionKey: SessionKey,
name: String? = null,
id: PacketId = this.id,
sequenceId: UShort = PacketFactory.atomicNextSequenceId(),
headerSizeHint: Int = 0,
version: ByteArray = TIMProtocol.version0x02, // in packet body
block: BytePacketBuilder.() -> Unit
): OutgoingPacket {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return buildSessionPacket0(
bot = bot,
sessionKey = sessionKey,
name = name,
id = id,
sequenceId = sequenceId,
headerSizeHint = headerSizeHint,
version = version,
head = TIMProtocol.head,
ver = TIMProtocol.ver,
tail = TIMProtocol.tail,
block = block
)
}
/**
* 构造一个待发送给服务器的会话数据包.
*/
@UseExperimental(ExperimentalContracts::class, MiraiInternalAPI::class, ExperimentalUnsignedTypes::class)
@JvmOverloads
fun <T> PacketFactory<*, *>.buildSessionProtoPacket(
bot: Long,
sessionKey: SessionKey,
name: String? = null,
id: PacketId = this.id,
sequenceId: UShort = PacketFactory.atomicNextSequenceId(),
headerSizeHint: Int = 0,
version: ByteArray = TIMProtocol.version0x04,
head: Any,
serializer: SerializationStrategy<T>,
protoObj: T
): OutgoingPacket = buildSessionProtoPacket0(
bot = bot,
sessionKey = sessionKey,
name = name,
id = id,
sequenceId = sequenceId,
headerSizeHint = headerSizeHint,
version = version,
head = head,
serializer = serializer,
protoObj = protoObj,
packetHead = TIMProtocol.head,
ver = TIMProtocol.ver,
tail = TIMProtocol.tail
)
\ No newline at end of file
package net.mamoe.mirai.timpc.network.packet
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.readBytes
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.utils.NoLog
import net.mamoe.mirai.utils.io.toUHexString
/**
* 被忽略的数据包.
*/
@NoLog
inline class IgnoredPacket(internal val id: PacketId) : Packet
/**
* 未知的包.
*/
class UnknownPacket(val id: PacketId, val body: ByteReadPacket) : Packet {
override fun toString(): String = "UnknownPacket(${id.value.toUHexString()})\nbody=${body.readBytes().toUHexString()}"
}
/**
* 仅用于替换类型应为 [Unit] 的情况
*/
object NoPacket : Packet {
override fun toString(): String = "NoPacket"
}
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet
import kotlinx.atomicfu.AtomicInt
import kotlinx.atomicfu.atomic
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readBytes
import kotlinx.io.pool.useInstance
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.protobuf.ProtoBuf
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.utils.cryptor.Decrypter
import net.mamoe.mirai.utils.cryptor.DecrypterType
import net.mamoe.mirai.utils.cryptor.readProtoMap
import net.mamoe.mirai.utils.io.ByteArrayPool
import net.mamoe.mirai.utils.io.debugPrintThis
import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.toUHexString
/**
* 一种数据包的处理工厂. 它可以解密解码服务器发来的这个包, 也可以编码加密要发送给服务器的这个包
* 应由一个 `object` 实现, 且实现 `operator fun invoke`
*
* @param TPacket 服务器回复包解析结果
* @param TDecrypter 服务器回复包解密器
*/
abstract class PacketFactory<out TPacket : Packet, TDecrypter : Decrypter>(val decrypterType: DecrypterType<TDecrypter>) {
@Suppress("PropertyName")
internal var _id: PacketId = NullPacketId
/**
* 包 ID.
*/
open val id: PacketId get() = _id
/**
* **解码**服务器的回复数据包
*/
abstract suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): TPacket
fun <T> ByteReadPacket.decodeProtoPacket(
deserializer: DeserializationStrategy<T>,
debuggingTag: String? = null
): T {
val headLength = readInt()
val protoLength = readInt()
if (debuggingTag != null) {
readBytes(headLength).debugPrintThis("$debuggingTag head")
} else {
discardExact(headLength)
}
val bytes = readBytes(protoLength)
// println(ByteReadPacket(bytes).readProtoMap())
if (debuggingTag != null) {
bytes.read { readProtoMap() }.toString().debugPrintThis("$debuggingTag proto")
}
return ProtoBuf.load(deserializer, bytes)
}
companion object {
private val sequenceId: AtomicInt = atomic(1)
fun atomicNextSequenceId(): UShort = atomicNextSequenceId0().toUShort()
private fun atomicNextSequenceId0(): Int {
val id = sequenceId.getAndAdd(1)
if (id > Short.MAX_VALUE.toInt() * 2) {
sequenceId.getAndSet(0) // do not `sequenceId.value = 0`, causes a bug
return sequenceId.getAndAdd(1)
}
return id
}
}
}
internal object UnknownPacketFactory : SessionPacketFactory<UnknownPacket>() {
override suspend fun BotNetworkHandler.handlePacket(packet: UnknownPacket) {
ByteArrayPool.useInstance {
packet.body.readAvailable(it)
bot.logger.debug("UnknownPacket(${packet.id.value.toUHexString()}) = " + it.toUHexString())
}
packet.body.close()
}
override suspend fun ByteReadPacket.decode(
id: PacketId,
sequenceId: UShort,
handler: BotNetworkHandler
): UnknownPacket {
return UnknownPacket(id, this)
}
}
internal object IgnoredPacketFactory : SessionPacketFactory<IgnoredPacket>() {
override suspend fun ByteReadPacket.decode(
id: PacketId,
sequenceId: UShort,
handler: BotNetworkHandler
): IgnoredPacket = IgnoredPacket(id)
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet
import net.mamoe.mirai.utils.io.toUHexString
/**
* 包 ID.
*/
interface PacketId {
val value: UShort
val factory: PacketFactory<*, *>
}
/**
* 通过 [value] 匹配一个 [IgnoredPacketId] 或 [KnownPacketId], 无匹配则返回一个 [UnknownPacketId].
*/
fun matchPacketId(value: UShort): PacketId =
IgnoredPacketIds.firstOrNull { it.value == value }
?: KnownPacketId.entries.firstOrNull { it.value.value == value }?.value
?: UnknownPacketId(value)
/**
* 用于代表 `null`. 调用任何属性时都将会得到一个 [error]
*/
@Suppress("unused")
object NullPacketId : PacketId {
override val factory: PacketFactory<*, *> get() = error("uninitialized")
override val value: UShort get() = error("uninitialized")
override fun toString(): String = "NullPacketId"
}
/**
* 未知的 [PacketId]
*/
inline class UnknownPacketId(override inline val value: UShort) : PacketId {
override val factory: PacketFactory<*, *> get() = UnknownPacketFactory
override fun toString(): String = "UnknownPacketId(${value.toUHexString()})"
}
object IgnoredPacketIds : List<IgnoredPacketId> by {
listOf<UShort>(
).map { IgnoredPacketId(it.toUShort()) }
}()
inline class IgnoredPacketId constructor(override val value: UShort) : PacketId {
override val factory: PacketFactory<*, *> get() = IgnoredPacketFactory
override fun toString(): String = "IgnoredPacketId(${value.toUHexString()})"
}
class KnownPacketId(override val value: UShort, override val factory: PacketFactory<*, *>) :
PacketId {
companion object : MutableMap<UShort, KnownPacketId> by mutableMapOf() {
operator fun set(key: UShort, factory: PacketFactory<*, *>) {
this[key] = KnownPacketId(key, factory)
}
inline fun <reified PF : PacketFactory<*, *>> getOrNull(): KnownPacketId? {
val clazz = PF::class
this.forEach {
if (clazz.isInstance(it.value)) {
return it.value
}
}
return null
}
inline fun <reified PF : PacketFactory<*, *>> get(): KnownPacketId = getOrNull<PF>()
?: throw NoSuchElementException()
}
override fun toString(): String = (factory::class.simpleName ?: factory::class.simpleName) + "(${value.toUHexString()})"
init {
factory._id = this
}
}
\ No newline at end of file
package net.mamoe.mirai.timpc.network.packet
import net.mamoe.mirai.utils.cryptor.DecrypterByteArray
import net.mamoe.mirai.utils.cryptor.DecrypterType
/**
* 会话密匙
*/
inline class SessionKey(override val value: ByteArray) : DecrypterByteArray {
companion object Type : DecrypterType<SessionKey>
}
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet.action
import kotlinx.io.core.*
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.data.EventPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.data.PreviousNameList
import net.mamoe.mirai.utils.PacketVersion
import net.mamoe.mirai.timpc.network.TIMProtocol
import net.mamoe.mirai.timpc.network.packet.*
import net.mamoe.mirai.utils.io.*
/**
* 查询某人与机器人账号有关的曾用名 (备注).
*
* 曾用名可能是:
* - 昵称
* - 共同群内的群名片
*/
@PacketVersion(date = "2019.11.11", timVersion = "2.3.2 (21173)")
internal object QueryPreviousNamePacket : SessionPacketFactory<PreviousNameList>() {
operator fun invoke(
bot: Long,
sessionKey: SessionKey,
target: Long
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
writeZero(2)
writeQQ(bot)
writeQQ(target)
}
// 01BC 曾用名查询. 查到的是这个人的
// 发送 00 00
// 3E 03 3F A2 //bot
// 59 17 3E 05 //目标
//
// 接受: 00 00 00 03
// [00 00 00 0C] E6 A5 BC E4 B8 8A E5 B0 8F E7 99 BD
// [00 00 00 10] 68 69 6D 31 38 38 E7 9A 84 E5 B0 8F 64 69 63 6B
// [00 00 00 0F] E4 B8 B6 E6 9A 97 E8 A3 94 E5 89 91 E9 AD 94
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): PreviousNameList {
// 00 00 00 01 00 00 00 0F E8 87 AA E5 8A A8 E9 A9 BE E9 A9 B6 31 2E 33
val count = readUInt().toInt()
return PreviousNameList(ArrayList<String>(count).apply {
repeat(count) {
discardExact(2)
add(readUShortLVString())
}
})
}
}
// 需要验证消息
// 0065 发送 03 07 57 37 E8
// 0065 接受 03 07 57 37 E8 10 40 00 00 10 14 20 00 00 00 00 00 00 00 01 00 00 00 00 00
/**
* 向服务器检查是否可添加某人为好友
*
* @author Him188moe
*/
@PacketVersion(date = "2019.11.11", timVersion = "2.3.2 (21173)")
internal object CanAddFriendPacket : SessionPacketFactory<CanAddFriendResponse>() {
operator fun invoke(
bot: Long,
qq: Long,
sessionKey: SessionKey
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
writeQQ(qq)
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): CanAddFriendResponse =
with(handler.bot) {
if (remaining > 20) {//todo check
return CanAddFriendResponse.AlreadyAdded(readQQ().qq())
}
val qq: QQ = readQQ().qq()
readUByteLVByteArray()
// debugDiscardExact(1)
return when (val state = readUByte().toUInt()) {
//09 4E A4 B1 00 03
0x00u -> CanAddFriendResponse.ReadyToAdd(qq)
0x01u -> CanAddFriendResponse.RequireVerification(qq)
0x99u -> CanAddFriendResponse.AlreadyAdded(qq)
0x03u,
0x04u -> CanAddFriendResponse.Rejected(qq)
else -> error(state.toString())
}
}
}
internal sealed class CanAddFriendResponse : EventPacket {
abstract val qq: QQ
/**
* 已经添加
*/
data class AlreadyAdded(
override val qq: QQ
) : CanAddFriendResponse()
/**
* 需要验证信息
*/
data class RequireVerification(
override val qq: QQ
) : CanAddFriendResponse()
/**
* 不需要验证信息
*/
data class ReadyToAdd(
override val qq: QQ
) : CanAddFriendResponse()
/**
* 对方拒绝添加
*/
data class Rejected(
override val qq: QQ
) : CanAddFriendResponse()
}
/*
包ID 0115, 在点击提交好友申请时
发出 03 5D 12 93 30
接受 03 00 00 00 00 01 30 5D 12 93 30 00 14 00 00 00 00 10 30 36 35 39 E4 B8 80 E7 BE 8E E5 A4 A9 E9 9D 99 02 0A 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 1E
*/
internal inline class FriendAdditionKey(val value: IoBuffer)
/**
* 请求一个 32 位 Key, 在添加好友时发出
*/
@PacketVersion(date = "2019.11.11", timVersion = "2.3.2 (21173)")
internal object RequestFriendAdditionKeyPacket : SessionPacketFactory<RequestFriendAdditionKeyPacket.Response>() {
operator fun invoke(
bot: Long,
qq: Long,
sessionKey: SessionKey
) = buildSessionPacket(bot, sessionKey) {
//01 00 01 02 B3 74 F6
writeHex("01 00 01")
writeQQ(qq)
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): Response {
//01 00 01 00 00 20 01 C2 76 47 98 38 A1 FF AB 64 04 A9 81 1F CC 2B 2B A6 29 FC 97 80 A6 90 2D 26 C8 37 EE 1D 8A FA
discardExact(4)
return Response(FriendAdditionKey(readIoBuffer(readUShort().toInt())))
}
data class Response(
val key: FriendAdditionKey
) : Packet
}
/**
* 请求添加好友
*/
internal object AddFriendPacket : SessionPacketFactory<AddFriendPacket.Response>() {
@PacketVersion(date = "2019.11.11", timVersion = "2.3.2 (21173)")
@Suppress("FunctionName")
fun RequestAdd(
bot: Long,
qq: Long,
sessionKey: SessionKey,
/**
* 验证消息
*/
message: String?,
/**
* 备注名
*/
remark: String?, //// TODO: 2019/11/15 无备注的情况
key: FriendAdditionKey
): OutgoingPacket = buildSessionPacket(bot, sessionKey, name = "AddFriendPacket.RequestAdd") {
//02 5D 12 93 30
// 00
// 00 [00 20] 3C 00 0C 44 17 C2 15 99 F9 94 96 DC 1C D5 E3 45 41 4B DB C5 B6 B6 52 85 14 D5 89 D2 06 72 BC C3
// 01 [00 1E] E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A
// 00 2A 00 01 00 01 00 00 00 1B E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A 00 05 00 00 00 00 01 00
//02 02 B3 74 F6
// 00 00
// [00 20] 06 51 61 A0 CE 33 FE 3E B1 32 41 AF 9A F0 EB FD 16 D5 3A 71 89 3A A4 5C 00 0F C4 57 31 A3 35 76
// 01 00 00 00 0F 00 01 00 01 00 00 00 00 00 05 00 00 00 00 01 00
//02 02 B3 74 F6
// 00
// 00 [00 20] 01 C2 76 47 98 38 A1 FF AB 64 04 A9 81 1F CC 2B 2B A6 29 FC 97 80 A6 90 2D 26 C8 37 EE 1D 8A FA
// 01 [00 00]
// 00 0F 00 01 00 01 00 00 00 00 00 05 00 00 00 00 01 00
writeUByte(0x02u)
writeQQ(qq)
writeByte(0)
writeByte(0); writeShort(key.value.readRemaining.toShort()); writeFully(key.value)
writeByte(1); writeShortLVString(message ?: "")
writeShortLVPacket {
//00 01 00 01 00 00 00 1B E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A E5 95 8A 00 05 00 00 00 00 01
//00 01 00 01 00 00 00 00 00 05 00 00 00 00 01
writeHex("00 01 00 01 00 00")// TODO: 2019/11/11 这里面或者下面那个hex可能包含分组信息. 这两次测试都是用的默认分组即我的好友
writeShortLVString(remark ?: "")
writeHex("00 05 00 00 00 00 01")
}
writeByte(0)
// write
}
// 03 76 E4 B8 DD
// 00 00 09 //分组
// 00 29 //有备注
// 00 09 00 02 00 00 00 00
// [00 18] E8 87 AA E5 8A A8 E9 A9 BE E9 A9 B6 31 2E 33 E5 93 88 E5 93 88 E5 93 88
// [00 05] 00 00 00 00 01
// 03 76 E4 B8 DD
// 00 00 09 00 11 00 09 00 02 00 00 00 00 //没有备注, 选择分组和上面那个一样
// 00 00 00 05 00 00 00 00 01
// 03 76 E4 B8 DD
// 00 00 00
// 00 11 //没有备注
// 00 09 00 02 00 00 00 00
// 00 00 00 05 00 00 00 00 01
@Suppress("FunctionName")
@PacketVersion(date = "2019.11.20", timVersion = "2.3.2 (21173)")
fun Approve(
bot: Long,
sessionKey: SessionKey,
/**
* 好友列表分组的组的 ID. "我的好友" 为 0
*/
friendListId: Short,
qq: Long,
/**
* 备注. 不设置则需要为 `null` TODO 需要确认是否还需发送一个设置备注包. 因为测试时若有备注则会多发一个包并且包里面有所设置的备注
*/
remark: String?
): OutgoingPacket = buildSessionPacket(bot, sessionKey, version = TIMProtocol.version0x02, name = "AddFriendPacket.Approve") {
writeByte(0x03)
writeQQ(qq)
writeZero(1)
writeUShort(friendListId.toUShort())
writeZero(1)
when (remark) {
null -> writeUByte(0x11u)
else -> writeUByte(0x29u)
}
writeHex("00 09 00 02 00 00 00 00")
when (remark) {
null -> writeZero(2)
else -> writeShortLVString(remark)
}
writeHex("00 05 00 00 00 00 01")
}
internal object Response : Packet {
override fun toString(): String = "AddFriendPacket.Response"
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): Response {
//02 02 B3 74 F6 00 //02 B3 74 F6 是QQ号
return Response
}
}
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.timpc.network.packet.action
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.timpc.network.packet.PacketId
import net.mamoe.mirai.timpc.network.packet.SessionPacketFactory
// 0001
// 已确认 查好友列表的列表
// send
// 20 01 00 00 00 00 01 00 00
// receive
// 20 00 01 03 00 00 00 15 01 01 03
// 00 0C 01 02 [09] E4 BF A1 E7 94 A8 E5 8D A1
// 00 0F 02 03 [0C] E8 BD AF E4 BB B6 E6 B3 A8 E5 86 8C
// 00 09 03 04 [06] E6 BA 90 E7 A0 81
// 00 00
internal inline class FriendListList(val delegate: List<FriendList>): Packet
internal object QueryFriendListListPacket : SessionPacketFactory<FriendList>() {
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): FriendList {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
// 0134
// 这里有好友也有群(为 internal id) 97208217
// 不太确定. 可能是查好友与群列表??
// send
// 00 00 01 5B 5D EB 58 AD 00 00 03 E8 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
// receive
// 00 00 19 00 00 01 5D 5D EB 5D C6 00 00 00 00 01 00 00 00 19 00 54 E5 06 01 00 00 00 00 00 01 00 00 00 64 B2 1D 01 00 00 00 00 00 01 00 00 00 66 7C C5 01 00 00 00 00 00 01 00 00 01 60 31 EC 01 00 00 00 00 00 01 00 00 01 B3 E6 AC 01 00 00 00 00 00 01 00 00 02 45 16 DF 01 00 00 00 00 00 01 00 00 03 37 67 20 01 00 00 00 00 00 01 00 00 05 B0 F4 6F 01 00 00 00 00 00 01 00 00 0C D9 1F 45 01 00 00 00 00 00 01 00 00 0F 0D 35 E1 01 00 00 00 00 00 01 00 00 10 18 86 83 01 00 00 00 00 00 01 00 00 11 A9 8B F7 01 00 00 00 00 00 01 00 00 31 05 12 1C 01 00 00 00 00 00 01 00 00 37 99 77 D7 01 00 00 00 00 00 01 00 00 37 C8 4D C7 04 00 00 00 00 00 00 37 E9 68 46 01 00 00 00 00 00 01 00 00 37 E9 94 CF 01 00 00 00 00 00 01 00 00 3E 03 3F A2 01 00 00 00 00 00 01 00 00 50 BA 4A 8F 01 00 00 00 00 00 01 00 00 55 7A D6 86 01 00 00 00 00 00 01 00 00 6C 78 B1 E0 01 00 00 00 00 00 01 00 00 78 59 79 87 01 00 00 00 00 00 01 00 00 79 9B 1B 59 04 00 00 00 00 00 00 A6 81 A4 9D 01 00 00 00 00 00 01 00 00 A6 A0 EE EF 01 00 00 00 00 00 01 00 00 00 00
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet.action
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.writeFully
import kotlinx.io.core.writeUByte
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.utils.NoLog
import net.mamoe.mirai.timpc.network.TIMProtocol
import net.mamoe.mirai.timpc.network.packet.*
import net.mamoe.mirai.utils.cryptor.encryptAndWrite
import net.mamoe.mirai.utils.io.writeQQ
/**
* 获取升级天数等.
*
* @author Him188moe
*/
internal object RequestAccountInfoPacket : SessionPacketFactory<RequestAccountInfoPacket.Response>() {
operator fun invoke(
qq: Long,
sessionKey: SessionKey
): OutgoingPacket = buildOutgoingPacket {
writeQQ(qq)
writeFully(TIMProtocol.fixVer2)
encryptAndWrite(sessionKey) {
writeUByte(0x88u)
writeQQ(qq)
writeByte(0x00)
}
}
@NoLog
object Response : Packet {
override fun toString(): String = "RequestAccountInfoPacket.Response"
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): Response = Response
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "RUNTIME_ANNOTATION_NOT_SUPPORTED")
package net.mamoe.mirai.timpc.network.packet.action
import kotlinx.io.core.ByteReadPacket
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import net.mamoe.mirai.contact.GroupInternalId
import net.mamoe.mirai.message.data.ImageId0x03
import net.mamoe.mirai.message.data.requireLength
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.data.EventPacket
import net.mamoe.mirai.data.ImageLink
import net.mamoe.mirai.timpc.network.packet.*
import net.mamoe.mirai.utils.PacketVersion
import net.mamoe.mirai.timpc.utils.assertUnreachable
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.io.toUHexString
internal interface GroupImageResponse : EventPacket
// endregion
@Suppress("unused")
@Serializable
class GroupImageLink(
@SerialId(3) val errorCode: Int = 0, // 0 for success
@SerialId(4) val errorMessage: String? = null, // 感动中国
@SerialId(10) private val _port: List<Byte>? = null,
@SerialId(11) private val _host: String? = null,
@SerialId(12) private val _thumbnail: String? = null,
@SerialId(13) private val _original: String? = null,
@SerialId(14) private val _compressed: String? = null
) : GroupImageResponse, ImageLink {
private inline val port: List<Byte> get() = _port!!
private inline val host: String get() = "http://" + _host!!
val thumbnail: String get() = host + ":" + port.first() + _thumbnail!!
override val original: String get() = host + ":" + port.first() + _original!!
val compressed: String get() = host + ":" + port.first() + _compressed!!
override fun toString(): String = "ImageDownloadInfo(${_original?.let { original } ?: errorMessage ?: "unknown"})"
}
@Suppress("NOTHING_TO_INLINE")
internal inline fun GroupImageLink.requireSuccess(): GroupImageLink {
require(this.errorCode == 0) { this.errorMessage ?: "null" }
return this
}
@Serializable
internal class ImageUploadInfo(
@SerialId(8) val uKey: ByteArray? = null
) : GroupImageResponse {
override fun toString(): String = "ImageUploadInfo(uKey=${uKey?.toUHexString()})"
}
/**
* 获取 Image Id 和上传用的一个 uKey
*/
@PacketVersion(date = "2019.11.22", timVersion = "2.3.2 (21173)")
internal object GroupImagePacket : SessionPacketFactory<GroupImageResponse>() {
private val constValue3 = byteArrayOf(
0x28, 0x00, 0x5A, 0x00, 0x53, 0x00, 0x41, 0x00, 0x58, 0x00, 0x40, 0x00, 0x57,
0x00, 0x4B, 0x00, 0x52, 0x00, 0x4A, 0x00, 0x5A, 0x00, 0x31, 0x00, 0x7E, 0x00
)
@Suppress("unused")
@Serializable
private class RequestIdProto(
@SerialId(2) val unknown4: Byte = 1,
@SerialId(3) var body: Body
) {
/*
"uint64_group_code"
"uint64_dst_uin"
"uint64_fileid"
"bytes_file_md5"
"uint32_url_flag"
"uint32_url_type"
"uint32_req_term"
"uint32_req_platform_type"
"uint32_inner_ip"
"uint32_bu_type"
"bytes_build_ver"
"uint64_file_id"
"uint64_file_size"
"uint32_original_pic"
"uint32_retry_req"
"uint32_file_height"
"uint32_file_width"
"uint32_pic_type"
"uint32_pic_up_timestamp"
"uint32_req_transfer_type"
*/
@Serializable
internal class Body(
@SerialId(1) val group: Int,
@SerialId(2) val bot: Int,
@SerialId(3) val const1: Byte = 0,
@SerialId(4) val md5: ByteArray,
@SerialId(5) val const2: Short = 0x0F2D,
@SerialId(6) val const3: ByteArray = constValue3,
@SerialId(7) val const4: Byte = 1,
// 8 is missing
@SerialId(9) val const5: Byte = 1,
@SerialId(10) val width: Int,
@SerialId(11) val height: Int,
@SerialId(12) val const6: Byte = 4,
@SerialId(13) val const7: ByteArray = constValue7,
@SerialId(14) val const8: Byte = 0,
@SerialId(15) val const9: Byte = 3,
@SerialId(16) val const10: Byte = 0
)
}
@Suppress("unused")
@Serializable
private class RequestLinkProto(
@SerialId(2) val unknown4: Byte = 2,
@SerialId(4) var body: Body
) {
@Serializable
internal class Body(
@SerialId(1) val group: Int,
@SerialId(2) val bot: Int,
@SerialId(3) val uniqueId: Int,
@SerialId(4) val md5: ByteArray,
@SerialId(5) val const2: Byte = 4,
@SerialId(6) val const3: Byte = 2,
@SerialId(7) val const4: Byte = 32,
@SerialId(8) val const14: Int = 255,
@SerialId(9) val const5: Byte = 0,
@SerialId(10) val unknown5: Int = 1,
@SerialId(11) val const7: ByteArray = constValue7,
@SerialId(12) val unknown6: Byte = 0,
@SerialId(13) val const6: Byte = 0,
@SerialId(14) val const8: Byte = 0,
@SerialId(15) val const9: Byte = 0,
@SerialId(16) val height: Int,
@SerialId(17) val width: Int,
@SerialId(18) val const12: Int = 1003, //?? 有时候还是1000, 1004
// 19 is missing
@SerialId(20) val const13: Byte = 1
)
}
private val constValue7: ByteArray = byteArrayOf(0x32, 0x36, 0x39, 0x33, 0x33)
private val requestImageIdHead = ubyteArrayOf(0x12u, 0x03u, 0x98u, 0x01u, 0x01u)
@Suppress("FunctionName")
fun RequestImageId(
bot: Long,
groupInternalId: GroupInternalId,
image: ExternalImage,
sessionKey: SessionKey
): OutgoingPacket = buildSessionProtoPacket(
bot, sessionKey, name = "GroupImagePacket.RequestImageId",
head = requestImageIdHead,
serializer = RequestIdProto.serializer(),
protoObj = RequestIdProto(
body = RequestIdProto.Body(
bot = bot.toInt(),
group = groupInternalId.value.toInt(),
md5 = image.md5,
height = image.height,
width = image.width
)
)
)
private val requestImageLinkHead = ubyteArrayOf(0x08u, 0x01u, 0x12u, 0x03u, 0x98u, 0x01u, 0x2u)
@Suppress("FunctionName")
fun RequestImageLink(
bot: Long,
sessionKey: SessionKey,
imageId: ImageId0x03
): OutgoingPacket {
imageId.requireLength()
//require(imageId.value.length == 37) { "ImageId.value.length must == 37" }
//[00 00 00 07] [00 00 00 52] (08 01 12 03 98 01 02) 10 02 22 4E 08 A0 89 F7 B6 03 10 A2 FF 8C F0 03 18 BB 92 94 BF 08 22 10 64 CF BB 65 00 13 8D B5 58 E2 45 1E EA 65 88 E1 28 04 30 02 38 20 40 FF 01 48 00 50 01 5A 05 32 36 39 33 33 60 00 68 00 70 00 78 00 80 01 97 04 88 01 ED 03 90 01 04 A0 01 01
// head 长度 proto 长度 head proto
return buildSessionProtoPacket(
bot,
sessionKey,
name = "GroupImagePacket.RequestImageLink",
head = requestImageLinkHead,
serializer = RequestLinkProto.serializer(),
protoObj = RequestLinkProto(
body = RequestLinkProto.Body(
bot = bot.toInt(), // same bin representation, so will be decoded correctly as a unsigned value in the server
group = bot.toInt(), // it's no need to pass a real group (internal) id
uniqueId = imageId.uniqueId.toInt(),
md5 = imageId.md5,
height = imageId.height,
width = imageId.width
)
)
)
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): GroupImageResponse {
@Serializable
data class GroupImageResponseProto(
@SerialId(3) val imageUploadInfoPacket: ImageUploadInfo? = null,
@SerialId(4) val groupImageLink: GroupImageLink? = null
)
val proto = decodeProtoPacket(GroupImageResponseProto.serializer())
return when {
proto.imageUploadInfoPacket != null -> proto.imageUploadInfoPacket
proto.groupImageLink != null -> proto.groupImageLink
else -> assertUnreachable()
}
}
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.timpc.network.packet.action
import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.URLProtocol
import io.ktor.http.content.OutgoingContent
import io.ktor.http.userAgent
import kotlinx.coroutines.io.ByteWriteChannel
import kotlinx.io.core.Input
import kotlinx.io.core.readAvailable
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.contact.GroupId
import net.mamoe.mirai.utils.io.ByteArrayPool
import net.mamoe.mirai.utils.io.debugPrintThis
@Suppress("SpellCheckingInspection")
internal suspend inline fun HttpClient.postImage(
htcmd: String,
uin: Long,
groupId: GroupId?,
imageInput: Input,
inputSize: Long,
uKeyHex: String
): Boolean = try {
post<HttpStatusCode> {
url {
protocol = URLProtocol.HTTP
host = "htdata2.qq.com"
path("cgi-bin/httpconn")
parameters["htcmd"] = htcmd
parameters["uin"] = uin.toString()
if (groupId != null) parameters["groupcode"] = groupId.value.toString()
parameters["term"] = "pc"
parameters["ver"] = "5603"
parameters["filesize"] = inputSize.toString()
parameters["range"] = 0.toString()
parameters["ukey"] = uKeyHex
userAgent("QQClient")
buildString().debugPrintThis("URL")
}
body = object : OutgoingContent.WriteChannelContent() {
override val contentType: ContentType = ContentType.Image.Any
override val contentLength: Long = inputSize
override suspend fun writeTo(channel: ByteWriteChannel) {
ByteArrayPool.useInstance { buffer: ByteArray ->
var size: Int
while (imageInput.readAvailable(buffer).also { size = it } != 0) {
channel.writeFully(buffer, 0, size)
}
}
}
}
} == HttpStatusCode.OK
} finally {
imageInput.close()
}
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "NO_REFLECTION_IN_CLASS_PATH")
package net.mamoe.mirai.timpc.network.packet.action
/*
/**
* 似乎没有必要. 服务器的返回永远都是 01 00 00 00 02 00 00
*/
@Deprecated("Useless packet")
@AnnotatedId(KnownPacketId.SUBMIT_IMAGE_FILE_NAME)
@PacketVersion(date = "2019.10.26", timVersion = "2.3.2 (21173)")
object SubmitImageFilenamePacket : PacketFactory {
operator fun invoke(
bot: Long,
target: Long,
filename: String,
sessionKey: SessionKey
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
writeFully(TIMProtocol.fixVer2)//?
//writeHex("04 00 00 00 01 2E 01 00 00 69 35")
encryptAndWrite(sessionKey) {
writeByte(0x01)
writeQQ(bot)
writeQQ(target)
writeZero(2)
writeUByte(0x02u)
writeRandom(1)
writeHex("00 0A 00 01 00 01")
val name = "UserDataImage:$filename"
writeShort(name.length.toShort())
writeStringUtf8(name)
writeHex("00 00")
writeRandom(2)//这个也与是哪个好友有关?
writeHex("00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2E 01")//35 02? 最后这个值是与是哪个好友有关
//this.debugPrintThis("SubmitImageFilenamePacket")
}
//解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1A 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 28 5A 53 41 58 40 57 4B 52 4A 5A 31 7E 33 59 4F 53 53 4C 4D 32 4B 49 2E 6A 70 67 00 00 06 E2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
//解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1B 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 28 5A 53 41 58 40 57 4B 52 4A 5A 31 7E 33 59 4F 53 53 4C 4D 32 4B 49 2E 6A 70 67 00 00 06 E2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
//解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1C 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 29 37 42 53 4B 48 32 44 35 54 51 28 5A 35 7D 35 24 56 5D 32 35 49 4E 2E 6A 70 67 00 00 03 73 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
}
@PacketVersion(date = "2019.10.19", timVersion = "2.3.2 (21173)")
class Response {
override fun decode() = with(input) {
require(readBytes().contentEquals(expecting))
}
companion object {
private val expecting = byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00)
}
}
}*/
// regiion GroupImageResponse
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.timpc.network.packet.action
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.data.FriendNameRemark
import net.mamoe.mirai.timpc.network.packet.*
import net.mamoe.mirai.utils.PacketVersion
import net.mamoe.mirai.utils.io.readUShortLVString
import net.mamoe.mirai.utils.io.writeQQ
import net.mamoe.mirai.utils.io.writeZero
internal object QueryFriendRemarkPacket : SessionPacketFactory<FriendNameRemark>() {
/**
* 查询好友的备注
*/
@PacketVersion(date = "2019.11.27", timVersion = "2.3.2 (21173)")
operator fun invoke(
bot: Long,
sessionKey: SessionKey,
target: Long
): OutgoingPacket = buildSessionPacket(
bot, sessionKey
) {
writeByte(0x0D)
writeQQ(target)
writeZero(1)
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): FriendNameRemark {
//0D 00 5D DA 3D 0F 59 17 3E 05 00 00 06 E6 9F 90 E4 B9 90 00 00 00 00 00 00
discardExact(11)
return FriendNameRemark(readUShortLVString())
}
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.timpc.network.packet.action
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.timpc.network.TIMProtocol
import net.mamoe.mirai.timpc.network.packet.*
import net.mamoe.mirai.utils.PacketVersion
import net.mamoe.mirai.utils.io.writeZero
class FriendList : Packet
internal object RequestFriendListPacket : SessionPacketFactory<FriendList>() {
@PacketVersion(date = "2019.11.24", timVersion = "2.3.2 (21173)")
operator fun invoke(
bot: Long,
sessionKey: SessionKey
): OutgoingPacket = buildSessionPacket(
bot, sessionKey, version = TIMProtocol.version0x02
) {
writeByte(0x02)
writeZero(4)
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): FriendList {
TODO()
}
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.timpc.network.packet.action
import kotlinx.io.core.*
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.timpc.message.internal.toPacket
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.utils.NoLog
import net.mamoe.mirai.utils.PacketVersion
import net.mamoe.mirai.timpc.network.TIMProtocol
import net.mamoe.mirai.timpc.network.packet.*
import net.mamoe.mirai.utils.io.*
import net.mamoe.mirai.utils.md5
@PacketVersion(date = "2019.10.19", timVersion = "2.3.2 (21173)")
internal object SendFriendMessagePacket : SessionPacketFactory<SendFriendMessagePacket.Response>() {
operator fun invoke(
botQQ: Long,
targetQQ: Long,
sessionKey: SessionKey,
message: MessageChain
): OutgoingPacket = buildSessionPacket(botQQ, sessionKey) {
writeQQ(botQQ)
writeQQ(targetQQ)
writeHex("00 00 00 08 00 01 00 04 00 00 00 00")
writeHex("38 03")
writeQQ(botQQ)
writeQQ(targetQQ)
writeFully(md5(buildPacket { writeQQ(targetQQ); writeFully(sessionKey.value) }.readBytes()))
writeHex("00 0B")
writeRandom(2)
writeTime()
writeHex("01 1D 00 00 00 00")
//消息过多要分包发送
//如果只有一个
writeByte(0x01)
writeByte(0)//第几个包
writeUByte(0x00u)
//如果大于一个,
//writeByte(0x02)//数量
//writeByte(0)//第几个包
//writeByte(0x91)//why?
writeHex("00 01 4D 53 47 00 00 00 00 00")
writeTime()
writeRandom(4)
writeHex("00 00 00 00 0C 00 86")
writeFully(TIMProtocol.messageConstNewest)
writeZero(2)
writePacket(message.toPacket())
/*
//Plain text
val bytes = event.toPacket()
it.writeByte(0x01)
it.writeShort(bytes.size + 3)
it.writeByte(0x01)
it.writeShort(bytes.size)
it.write(bytes)*/
}
@NoLog
internal object Response : Packet {
override fun toString(): String = "SendFriendMessagePacket.Response"
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): Response = Response
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.utils.PacketVersion
import net.mamoe.mirai.utils.io.readBoolean
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")
data class AndroidDeviceStatusChangePacket(val kind: Kind) : Packet {
enum class Kind {
ONLINE,
OFFLINE
}
}
/**
* Android 客户端在线状态改变
*/
@PacketVersion(date = "2019.10.31", timVersion = "2.3.2 (21173)")
internal object AndroidDeviceOnlineStatusChangedEventFactory : KnownEventParserAndHandler<AndroidDeviceStatusChangePacket>(0x00C4u) {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): AndroidDeviceStatusChangePacket {
discardExact(13)
return AndroidDeviceStatusChangePacket(
if (readBoolean()) AndroidDeviceStatusChangePacket.Kind.OFFLINE else AndroidDeviceStatusChangePacket.Kind.ONLINE
)
}
}
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readBytes
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.ConnectionOccupiedEvent
import net.mamoe.mirai.utils.io.encodeToString
internal object ConnectionOccupiedPacketHandler : KnownEventParserAndHandler<ConnectionOccupiedEvent>(0x0030u) {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): ConnectionOccupiedEvent {
discardExact(6)
return ConnectionOccupiedEvent(readBytes((remaining - 8).toInt()).encodeToString())
}
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.*
import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.utils.NoLog
import net.mamoe.mirai.timpc.network.TIMPCBotNetworkHandler
import net.mamoe.mirai.timpc.network.packet.*
import net.mamoe.mirai.utils.io.readIoBuffer
/**
* 事件的识别 ID. 在 ACK 时使用
*/
internal class EventPacketIdentity(
val from: Long,//对于好友消息, 这个是发送人
val to: Long,//对于好友消息, 这个是bot
internal val uniqueId: IoBuffer//8
) {
override fun toString(): String = "($from->$to)"
}
internal fun BytePacketBuilder.writeEventPacketIdentity(identity: EventPacketIdentity) = with(identity) {
writeUInt(from.toUInt())
writeUInt(to.toUInt())
writeFully(uniqueId)
}
@Suppress("FunctionName")
internal fun matchEventPacketFactory(value: UShort): EventParserAndHandler<*> =
KnownEventParserAndHandler.firstOrNull { it.id == value } ?: IgnoredEventIds.firstOrNull { it.id == value } ?: UnknownEventParserAndHandler(value)
/**
* 事件包, 它将会分析事件 ID 并解析事件为 [Packet]
*/
@NoLog
@Suppress("FunctionName")
internal object EventPacketFactory : PacketFactory<Packet, SessionKey>(SessionKey) {
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): Packet {
val eventIdentity = EventPacketIdentity(
from = readUInt().toLong(), // clear semantic, don't readQQ() or readGroup()
to = readUInt().toLong(), // clear semantic
uniqueId = readIoBuffer(8)
)
(handler as TIMPCBotNetworkHandler).socket.sendPacket(EventPacketFactory(id, sequenceId, handler.bot.uin, handler.sessionKey, eventIdentity))
discardExact(2) // 1F 40
return with(matchEventPacketFactory(readUShort())) { parse(handler.bot, eventIdentity) }.also {
if (it is EventParserAndHandler<*>) {
@Suppress("UNCHECKED_CAST")
with(it as EventParserAndHandler<in Packet>) {
with(handler) {
handlePacket(it)
}
}
}
}
}
operator fun invoke(
id: PacketId,
sequenceId: UShort,
bot: Long,
sessionKey: SessionKey,
identity: EventPacketIdentity
): OutgoingPacket = buildSessionPacket(name = "EventPacket", id = id, sequenceId = sequenceId, bot = bot, sessionKey = sessionKey) {
writeEventPacketIdentity(identity)
}
}
internal interface EventParserAndHandler<TPacket : Packet> {
val id: UShort
suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): TPacket
/**
* 在 [BotNetworkHandler] 下处理这个包. 广播事件等.
*/
suspend fun BotNetworkHandler.handlePacket(packet: TPacket) {}
}
internal abstract class KnownEventParserAndHandler<TPacket : Packet>(override val id: UShort) : EventParserAndHandler<TPacket> {
companion object FactoryList : MutableList<KnownEventParserAndHandler<*>> by mutableListOf(
AndroidDeviceOnlineStatusChangedEventFactory,
FriendConversationInitializedEventParserAndHandler,
GroupFileUploadEventFactory,
GroupMemberPermissionChangedEventFactory,
GroupMessageEventParserAndHandler,
FriendMessageEventParserAndHandler,
FriendAddRequestEventPacket,
MemberGoneEventPacketHandler,
ConnectionOccupiedPacketHandler,
MemberJoinPacketHandler,
MemberMuteEventPacketParserAndHandler
)
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.events.ReceiveFriendAddRequestEvent
import net.mamoe.mirai.utils.PacketVersion
import net.mamoe.mirai.utils.io.readQQ
import net.mamoe.mirai.utils.io.readUShortLVString
@PacketVersion(date = "2019.11.20", timVersion = "2.3.2 (21173)")
internal object FriendAddRequestEventPacket : KnownEventParserAndHandler<ReceiveFriendAddRequestEvent>(0x02DFu) {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): ReceiveFriendAddRequestEvent = with(bot) {
// 00 00 00 08 00 0A 00 04 01 00
// 00 00 00 01
// 76 E4 B8 DD
// 00 00 00 01
// 2D 5C 53 A6
// 76 E4 B8 DD
// 02 00 00
// 00 0B BC 00 0B 5D D5 2E A3 04 7C 00 02 00 0C E6 88 91 E6 98 AF E6 A2 A8 E5 A4 B4 00 00
// 有验证消息
// 00 00 00 08 00 0A 00 04 01 00
// 00 00 00 01
// 76 E4 B8 DD
// 00 00 00 01
// 2D 5C 53 A6
// 76 E4 B8 DD
// 02 00 00
// 09 0B BD 00 02 5D D5 32 50 04 7C 00 02 00 00 00 00
// 无验证消息
// 00 00 00 08 00 0A 00 04 01 00
// 00 00 00 01
// 76 E4 B8 DD
// 00 00 00 01
// 2D 5C 53 A6
// 76 E4 B8 DD
// 02 00 00
// 09 0B BD 00 02 5D D5 33 0C 04 7C 00 02 00 0C E6 88 91 E6 98 AF E6 A2 A8 E5 A4 B4 00 00
// 有验证消息
/*
Mirai 20:35:23 : Packet received: UnknownEventPacket(id=02 10, identity=(761025446->1994701021))
= 00 00 00 08 00 0A 00 04 01 00 00 00 00 00 00 06 00 00 00 4C 08 02 1A 02 08 23 0A 4A 08 DD F1 92 B7 07 10 A6 A7 F1 EA 02 18 02 20 00 28 01 30 09 38 BD 17 40 02 48 8C E6 D4 EE 05 52 0C E6 88 91 E6 98 AF E6 A2 A8 E5 A4 B4 5A 0F E6 9D A5 E8 87 AA E8 AE A8 E8 AE BA E7 BB 84 62 00 6A 06 08 A5 CE 85 8A 06 72 00
Mirai 20:35:23 : Packet received: UnknownEventPacket(id=02 DF, identity=(761025446->1994701021))
= 00 00 00 08 00 0A 00 04 01 00 00 00 00 01 76 E4 B8 DD 00 00 00 01 2D 5C 53 A6 76 E4 B8 DD 02 00 00 09 0B BD 00 02 5D D5 33 0C 04 7C 00 02 00 0C E6 88 91 E6 98 AF E6 A2 A8 E5 A4 B4 00 00
Mirai 20:35:23 : Packet received: UnknownEventPacket(id=00 BB, identity=(761025446->1994701021))
= 00 00 00 08 00 0A 00 04 01 00 00 00 01 0C E6 88 91 E6 98 AF E6 A2 A8 E5 A4 B4 01 0B BD 00 02 00 00 00 5E 00 00 00 00 00 00 00 00 01 04 03 EF 00 06 08 A5 CE 85 8A 06 03 F0 00 02 08 01 03 F2 00 14 00 00 00 82 00 00 00 6D 2F AF 0B ED 20 02 EB 94 00 00 00 00 03 ED 00 28 08 01 12 18 68 69 6D 31 38 38 E7 9A 84 E8 80 81 E5 85 AC E7 9A 84 E6 9B BF E8 BA AB 18 00 22 06 E6 A2 A8 E5 A4 B4 28 01
*/
//Mirai 20:32:15 : Packet received: UnknownEventPacket(id=02 DF, identity=(761025446->1994701021))
// = 00 00 00 08 00 0A 00 04 01 00 00 00 00 01 76 E4 B8 DD 00 00 00 01 2D 5C 53 A6 76 E4 B8 DD 02 00 00 09 0B BD 00 02 5D D5 32 50 04 7C 00 02 00 00 00 00
//Mirai 20:32:15 : Packet received: UnknownEventPacket(id=02 10, identity=(761025446->1994701021))
// = 00 00 00 08 00 0A 00 04 01 00 00 00 00 00 00 06 00 00 00 40 08 02 1A 02 08 23 0A 3E 08 DD F1 92 B7 07 10 A6 A7 F1 EA 02 18 02 20 00 28 01 30 09 38 BD 17 40 02 48 D0 E4 D4 EE 05 52 00 5A 0F E6 9D A5 E8 87 AA E8 AE A8 E8 AE BA E7 BB 84 62 00 6A 06 08 A5 CE 85 8A 06 72 00
//Mirai 20:32:15 : Packet received: UnknownEventPacket(id=00 BB, identity=(761025446->1994701021))
// = 00 00 00 08 00 0A 00 04 01 00 00 00 01 00 01 0B BD 00 02 00 00 00 5E 00 00 00 00 00 00 00 00 01 04 03 EF 00 06 08 A5 CE 85 8A 06 03 F0 00 02 08 01 03 F2 00 14 00 00 00 82 00 00 00 6D 2F AF 0B ED 20 02 EB 94 00 00 00 00 03 ED 00 28 08 01 12 18 68 69 6D 31 38 38 E7 9A 84 E8 80 81 E5 85 AC E7 9A 84 E6 9B BF E8 BA AB 18 00 22 06 E6 A2 A8 E5 A4 B4 28 01
discardExact(10 + 4) // 00 00 00 08 00 0A 00 04 01 00 00 00 00 01
discardExact(4) // bot account uint
discardExact(4) // 00 00 00 01
val qq = readQQ().qq()
discardExact(4) // bot account uint
discardExact(3) // 02 00 00 恒定
discardExact(11) // 不确定. 以下为可能的值
// 00 00 01 00 01 5D D5 3C 57 00 A8 , 1994701021 添加 761025446
// 09 0B BD 00 02 5D D5 33 0C 04 7C 有验证, 761025446 添加 1994701021
// 09 0B BD 00 02 5D D5 32 50 04 7C 无验证, 761025446 添加 1994701021
// 00 0B BC 00 0B 5D D5 2E A3 04 7C 有验证
val message = readUShortLVString()
discardExact(2) // 00 01
return ReceiveFriendAddRequestEvent(qq, message)
}
}
/*
1994701021 向 761025446 发出好友请求, 761025446 收到 0x02DF 事件, body=
00 00 00 08 00 0A 00 04 01 00
00 00 00 01
2D 5C 53 A6
00 00 00 01
76 E4 B8 DD
2D 5C 53 A6
02 00 00
00 00 01 00 01 5D D5 3C 57 00 A8 00 02 00 00 00 00
*/
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.EventPacket
import net.mamoe.mirai.utils.PacketVersion
import net.mamoe.mirai.utils.io.readQQ
data class FriendConversationInitialize(
val qq: Long
) : EventPacket
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")
internal object FriendConversationInitializedEventParserAndHandler : KnownEventParserAndHandler<FriendConversationInitialize>(0x0079u) {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): FriendConversationInitialize {
discardExact(4)// 00 00 00 00
return FriendConversationInitialize(readQQ())
}
}
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "JoinDeclarationAndAssignment")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUByte
import net.mamoe.mirai.data.OnlineStatus
import net.mamoe.mirai.event.events.FriendStatusChanged
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.timpc.network.packet.PacketId
import net.mamoe.mirai.timpc.network.packet.SessionPacketFactory
import net.mamoe.mirai.utils.io.readQQ
/**
* 好友在线状态改变
*/
internal object FriendOnlineStatusChangedPacket : SessionPacketFactory<FriendStatusChanged>() {
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): FriendStatusChanged {
val qq = readQQ()
discardExact(8)
val statusId = readUByte()
val status = OnlineStatus.ofIdOrNull(statusId.toInt()) ?: OnlineStatus.UNKNOWN
return FriendStatusChanged(handler.bot.getQQ(qq), status)
}
//在线 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
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.EventPacket
import net.mamoe.mirai.utils.PacketVersion
import net.mamoe.mirai.utils.io.debugPrintThis
data class GroupFileUploadPacket(inline val xmlMessage: String) : EventPacket
@PacketVersion(date = "2019.7.1", timVersion = "2.3.2 (21173)")
internal object GroupFileUploadEventFactory : KnownEventParserAndHandler<GroupFileUploadPacket>(0x002Du) {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): GroupFileUploadPacket {
this.debugPrintThis("GroupFileUploadPacket")
return GroupFileUploadPacket("")
/*
discardExact(60)
val size = readShort().toInt()
discardExact(3)
return GroupFileUploadPacket(xmlMessage = readString(size))*/
}
}
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.EventPacket
import net.mamoe.mirai.utils.io.toUHexString
internal inline class IgnoredEventPacket(val id: UShort) : EventPacket {
override fun toString(): String = "IgnoredEventPacket(id=0x${id.toUHexString("")})"
}
internal object IgnoredEventIds : List<IgnoredEventParserAndHandler> by {
listOf(
//0x0021u, // 与群成员加入有关
0x0210u // 新朋友等字符串通知
).map { IgnoredEventParserAndHandler(it.toUShort()) }
}()
internal inline class IgnoredEventParserAndHandler(override val id: UShort) : EventParserAndHandler<IgnoredEventPacket> {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): IgnoredEventPacket = IgnoredEventPacket(id)
}
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE", "unused", "MemberVisibilityCanBePrivate")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.Subscribable
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.timpc.TIMPCBot
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.io.discardExact
import net.mamoe.mirai.utils.io.readQQ
/**
* 成员加入前的事件. 群的成员列表中还没有这个人
*/
@UseExperimental(MiraiInternalAPI::class)
inline class PreMemberJoinEvent constructor(private val packet: MemberJoinEventPacket) : MemberJoinEvent {
override val member: Member get() = packet.member
override val group: Group get() = packet.member.group
override val inviter: Member get() = packet.inviter ?: error("The new member is not a invitee")
override val isInvitee: Boolean get() = packet.inviter != null
}
/**
* 成员加入后的事件. 群的成员列表中已经有这个人
*/
@UseExperimental(MiraiInternalAPI::class)
inline class PostMemberJoinEvent constructor(private val packet: MemberJoinEventPacket) : MemberJoinEvent {
override val member: Member get() = packet.member
override val group: Group get() = packet.member.group
override val inviter: Member get() = packet.inviter ?: error("The new member is not a invitee")
override val isInvitee: Boolean get() = packet.inviter != null
}
interface MemberJoinEvent : Subscribable {
val member: Member
val group: Group
val inviter: Member
val isInvitee: Boolean
}
/**
* 新成员加入. 此时这个人还没被添加到群列表
*
* 仅内部使用
*/
@MiraiInternalAPI
class MemberJoinEventPacket(
val member: Member,
val inviter: Member?
) : MemberListChangedEvent // only for internal subscribing
@UseExperimental(MiraiInternalAPI::class)
internal object MemberJoinPacketHandler : KnownEventParserAndHandler<MemberJoinEventPacket>(0x0021u) {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): MemberJoinEventPacket {
//由 1040400290 邀请的新成员加入
//00 00 00 08 00 0A 00 04 01 00 00
// 00 32 DC FC C8
// 01 2D 5C 53 A6
// 03 3E 03 3F A2
// 06 B4 B4 BD A8 D5 DF
// 00 30 44 31 43 37 36 30 41 43 33 42 46 37 32 39 38 36 41 42 43 44 33 37 41 37 46 30 35 35 46 37 32 39 46 31 31 36 36 37 42 35 45 33 37 43 37 46 44 37
discardExact(11) //00 00 00 08 00 0A 00 04 01 00 00
discardExact(1) // 00
val group = bot.getGroup(readQQ())
discardExact(1) // 01
val qq = bot.getQQ(readQQ())
val member = with(bot as? TIMPCBot ?: error("wrong Bot type passed")) {
group.Member(qq, MemberPermission.MEMBER)
}
return if (readByte().toInt() == 0x03) {
MemberJoinEventPacket(member, null)
} else {
MemberJoinEventPacket(member, group.getMember(readQQ()))
}
}
override suspend fun BotNetworkHandler.handlePacket(packet: MemberJoinEventPacket) {
PreMemberJoinEvent(packet).broadcast()
packet.broadcast()
PostMemberJoinEvent(packet).broadcast()
}
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.readUByte
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.data.EventPacket
import net.mamoe.mirai.utils.io.*
/**
* 群成员列表变动事件.
*
* 可为成员增多, 或减少.
*/
interface MemberListChangedEvent : EventPacket
/**
* 成员主动离开群
*/
@Suppress("unused")
data class MemberQuitEvent(
val member: Member,
private val _operator: Member?
) : MemberListChangedEvent {
/**
* 是否是被管理员或群主踢出
*/
val isKick: Boolean get() = _operator != null
/**
* 被踢出时的操作人. 若是主动退出则为 `null`
*/
val operator: Member get() = _operator ?: error("The action is not a kick")
}
/**
* 机器人被踢出
*/
data class BeingKickEvent(val group: Group, val operator: Member) : MemberListChangedEvent
/**
* 成员退出. 可能是被踢出也可能是主动退出
*/
internal object MemberGoneEventPacketHandler : KnownEventParserAndHandler<MemberListChangedEvent>(0x0022u) {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): MemberListChangedEvent {
discardExact(11)
discardExact(1)
val group = bot.getGroup(readGroup())
discardExact(1)
val id = readQQ()
if (id == bot.uin) {
discardExact(1)
return BeingKickEvent(group, group.getMember(readQQ()))
}
val member = group.getMember(id)
return when (val type = readUByte().toInt()) {
0x02 -> MemberQuitEvent(member, _operator = null)
0x03 -> MemberQuitEvent(member, _operator = group.getMember(readQQ()))
else -> error("Unsupported type " + type.toUHexString())
}
// 某群员主动离开, 群号 853343432
// 00 00 00 08 00 0A 00 04 01 00 00
// 00 (32 DC FC C8)
// 01 (2D 5C 53 A6)
// 02
// 00 30 44 43 31 45 31 38 43 38 31 44 31 34 39 39 41 44 36 44 37 32 42 41 35 43 45 44 30 33 35 42 39 31 45 31 42 43 41 44 42 35 33 33 46 39 31 45 37 31
// 某群员被群主踢出, 群号 853343432
// 00 00 00 08 00 0A 00 04 01 00 00
// 00 (32 DC FC C8)
// 01 (2D 5C 53 A6)
// 03 (3E 03 3F A2)
// 06 B4 B4 BD A8 D5 DF
// 00 30 45 43 41 34 35 44 34 33 30 34 30 35 35 39 42 46 44 45 35 32 46 31 42 33 46 36 38 30 33 37 42 44 43 30 44 37 36 37 34 39 41 39 37 32 39 33 32 36
// 机器人被踢出
// 00 00 00 08 00 0A 00 04 01 00 00
// 00 (32 DC FC C8)
// 01 (2D 5C 53 A6)
// 03 (3E 03 3F A2)
// 06 B4 B4 BD A8 D5 DF
// 00 30 32 33 32 63 32 39 36 65 36 35 64 62 64 64 64 64 65 35 62 33 34 64 36 62 34 33 32 61 30 64 61 65 32 30 37 35 38 34 37 34 32 65 32 39 63 35 63 64
}
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE", "MemberVisibilityCanBePrivate")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readBytes
import kotlinx.io.core.readUInt
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.data.*
import net.mamoe.mirai.utils.io.debugIfFail
import net.mamoe.mirai.utils.io.readQQ
import net.mamoe.mirai.utils.io.readRemainingBytes
import net.mamoe.mirai.utils.io.toUHexString
internal class Unknown0x02DCPacketFlag0x0EMaybeMutePacket(
val remaining: ByteArray
) : EventOfMute() {
override val operator: Member get() = error("Getting a field from Unknown0x02DCPacketFlag0x0EMaybeMutePacket")
override val group: Group get() = error("Getting a field from Unknown0x02DCPacketFlag0x0EMaybeMutePacket")
override fun toString(): String = "Unknown0x02DCPacketFlag0x0EMaybeMutePacket(remaining=${remaining.toUHexString()})"
}
// TODO: 2019/12/14 这可能不只是禁言的包.
internal object MemberMuteEventPacketParserAndHandler : KnownEventParserAndHandler<EventOfMute>(0x02DCu) {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): EventOfMute {
//取消
//00 00 00 11 00
// 0A 00 04 01 00 00 00 00 0C 00 05 00 01 00
// 01 01
// 22 96 29 7B
// 0C 01
// 3E 03 3F A2
// 5D E5 12 EB
// 00 01
// 76 E4 B8 DD
// 00 00 00 00
// 禁言
//00 00 00 11 00
// 0A 00 04 01 00 00 00 00 0C 00 05 00 01 00
// 01
// 01
// 22 96 29 7B
// 0C
// 01
// 3E 03 3F A2
// 5D E5 07 85
// 00
// 01
// 76 E4 B8 DD
// 00 27 8D 00
discardExact(3)
return when (val flag = readByte().toUInt()) {
0x0Eu -> {
//00 00 00 0E 00 08 00 02 00 01 00
// 0A 00 04 01 00 00 00 35 DB 60 A2 11 00 3E 08 07 20 A2 C1 ED AE 03 5A 34 08 A2 FF 8C F0 03 1A 19 08 F4 0E 10 FE 8C D3 EF 05 18 84 A1 F8 F9 06 20 00 28 00 30 A2 FF 8C F0 03 2A 0D 08 00 12 09 08 F4 0E 10 00 18 01 20 00 30 00 38 00
Unknown0x02DCPacketFlag0x0EMaybeMutePacket(readRemainingBytes())
}
0x11u -> debugIfFail("解析禁言包(0x02DC)时"){ // 猜测这个失败是撤回??
// 00 0A 00 04 01 00 00 00 00 0C 00 05 00 01 00 01 01 27 0B 60 E7 11 00 33 08 07 20 E7 C1 AD B8 02 5A 29 08 A6 FE C0 A4 0A 1A 19 08 BC 15 10 C1 95 BC F0 05 18 CA CA 8F DE 04 20 00 28 00 30 A6 FE C0 A4 0A 2A 02 08 00 30 00 38 00
// 失败
discardExact(15)
discardExact(2)
val group = bot.getGroup(readQQ())
discardExact(2)
val operator = group.getMember(readQQ())
discardExact(4) //time
discardExact(2)
val memberQQ = readQQ()
val durationSeconds = readUInt().toInt()
if (durationSeconds == 0) {
if (memberQQ == bot.uin) {
BeingUnmutedEvent(operator)
} else {
MemberUnmuteEvent(group.getMember(memberQQ), operator)
}
} else {
if (memberQQ == bot.uin) {
BeingMutedEvent(durationSeconds, operator)
} else {
MemberMuteEvent(group.getMember(memberQQ), durationSeconds, operator)
}
}
}
else -> error("Unsupported flag in 0x02DC packet. flag=$flag, remainning=${readBytes().toUHexString()}")
}
}
}
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.data.EventPacket
import net.mamoe.mirai.utils.PacketVersion
import net.mamoe.mirai.utils.io.readQQ
data class MemberPermissionChangePacket(
val member: Member,
val kind: Kind
) : EventPacket {
val group: Group get() = member.group
enum class Kind {
/**
* 变成管理员
*/
BECOME_OPERATOR,
/**
* 不再是管理员
*/
NO_LONGER_OPERATOR,
} // TODO: 2019/11/2 变成群主的情况
}
@PacketVersion(date = "2019.11.1", timVersion = "2.3.2 (21173)")
internal object GroupMemberPermissionChangedEventFactory : KnownEventParserAndHandler<MemberPermissionChangePacket>(0x002Cu) {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): MemberPermissionChangePacket {
// 群里一个人变成管理员:
// 00 00 00 08 00 0A 00 04 01 00 00 00 22 96 29 7B 01 01 76 E4 B8 DD 01
// 取消管理员
// 00 00 00 08 00 0A 00 04 01 00 00 00 22 96 29 7B 01 00 76 E4 B8 DD 00
discardExact(remaining - 5)
val group = bot.getGroup(identity.from)
val qq = readQQ()
val kind = when (readByte().toInt()) {
0x00 -> MemberPermissionChangePacket.Kind.NO_LONGER_OPERATOR
0x01 -> MemberPermissionChangePacket.Kind.BECOME_OPERATOR
else -> error("Could not determine permission change kind")
}
return MemberPermissionChangePacket(group.getMember(qq), kind)
}
}
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.timpc.message.internal.readMessageChain
import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.utils.PacketVersion
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.io.*
@UseExperimental(ExperimentalUnsignedTypes::class)
internal object GroupMessageEventParserAndHandler : KnownEventParserAndHandler<GroupMessage>(0x0052u) {
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): GroupMessage {
discardExact(31)
val groupNumber = readGroup()
discardExact(1)
val qq = readQQ()
discardExact(48)
readUShortLVByteArray()
discardExact(2)//2个0x00
//debugPrintIfFail {
val message = readMessageChain()
var senderPermission: MemberPermission = MemberPermission.MEMBER
var senderName = ""
val map = readTLVMap(true, 1)
if (map.containsKey(18)) {
map.getValue(18).read {
val tlv = readTLVMap(true, 1)
senderPermission = when (tlv.takeIf { it.containsKey(0x04) }?.get(0x04)?.getOrNull(3)?.toInt()) {
null -> MemberPermission.MEMBER
0x08 -> MemberPermission.OWNER
0x10 -> MemberPermission.ADMINISTRATOR
else -> {
tlv.printTLVMap("TLV(tag=18) Map")
MiraiLogger.warning("Could not determine member permission, default permission MEMBER is being used")
MemberPermission.MEMBER
}
}
senderName = when {
tlv.containsKey(0x01) -> kotlinx.io.core.String(tlv.getValue(0x01))//这个人的qq昵称
tlv.containsKey(0x02) -> kotlinx.io.core.String(tlv.getValue(0x02))//这个人的群名片
else -> {
tlv.printTLVMap("TLV(tag=18) Map")
MiraiLogger.warning("Could not determine senderName")
"null"
}
}
}
}
val group = bot.getGroup(groupNumber)
return GroupMessage(
bot = bot,
group = group,
senderName = senderName,
permission = senderPermission,
sender = group.getMember(qq),
message = message
)
}
}
// endregion
// region friend message
@Suppress("unused")
@UseExperimental(ExperimentalUnsignedTypes::class)
internal object FriendMessageEventParserAndHandler : KnownEventParserAndHandler<FriendMessage>(0x00A6u) {
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): FriendMessage {
discardExact(2)
val l1 = readShort()
discardExact(1)//0x00
val previous = readByte().toInt() == 0x08
discardExact(l1.toInt() - 2)
//java.io.EOFException: Only 49 bytes were discarded of 69 requested
//抖动窗口消息
discardExact(69)
readUShortLVByteArray()//font
discardExact(2)//2个0x00
val message = readMessageChain()
return FriendMessage(
bot = bot,
previous = previous,
sender = bot.getQQ(identity.from),
message = message
)
}
}
// endregion
\ No newline at end of file
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.timpc.network.packet.event
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.readBytes
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.data.EventPacket
import net.mamoe.mirai.utils.io.ByteArrayPool
import net.mamoe.mirai.utils.io.toUHexString
internal data class UnknownEventPacket(
val id: UShort,
val identity: EventPacketIdentity,
val body: ByteReadPacket
) : EventPacket {
override fun toString(): String = "UnknownEventPacket(id=${id.toUHexString()}, identity=$identity)\n = ${body.readBytes().toUHexString()}"
}
/*
被好友拉入群 (已经进入)
Mirai 21:54:15 : Packet received: UnknownEventPacket(id=00 57, identity=(920503456->1994701021))
= 00 00 00 08 00 0A 00 04 01 00 00 00 36 DD C4 A0 01 04 00 00 00 00 3E 03 3F A2 00 00 20 E5 96 01 BC 23 AE 03 C7 B8 9F BE B3 E5 E4 77 A9 0E FD 2B 7C 64 8B C0 5F 29 8B D7 DC 85 7E 44 7B 00 30 33 65 62 61 62 31 31 66 63 63 61 34 63 38 39 31 36 31 33 37 37 65 65 62 36 63 32 39 37 31 33 34 32 35 62 64 30 34 66 62 31 61 31 65 37 31 63 33
*/
//TODO This class should be declared with `inline`, but a CompilationException will be thrown
internal class UnknownEventParserAndHandler(override val id: UShort) : EventParserAndHandler<UnknownEventPacket> {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): UnknownEventPacket {
// MiraiLogger.debug("UnknownEventPacket(${id.toHexString()}) = ${readBytes().toHexString()}")
return UnknownEventPacket(id, identity, this) //TODO the cause is that `this` reference.
}
override suspend fun BotNetworkHandler.handlePacket(packet: UnknownEventPacket) {
ByteArrayPool.useInstance {
packet.body.readAvailable(it)
bot.logger.debug("Unknown packet(${packet.id}) data = " + it.toUHexString())
}
}
}
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.timpc.network.packet.login
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.writeFully
import kotlinx.io.core.writeUByte
import net.mamoe.mirai.data.OnlineStatus
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.timpc.network.TIMProtocol
import net.mamoe.mirai.timpc.network.packet.*
import net.mamoe.mirai.utils.cryptor.NoDecrypter
import net.mamoe.mirai.utils.cryptor.encryptAndWrite
import net.mamoe.mirai.utils.io.writeHex
import net.mamoe.mirai.utils.io.writeQQ
/**
* 改变在线状态: "我在线上", "隐身" 等
*/
internal object ChangeOnlineStatusPacket : PacketFactory<ChangeOnlineStatusPacket.ChangeOnlineStatusResponse, NoDecrypter>(NoDecrypter) {
operator fun invoke(
bot: Long,
sessionKey: SessionKey,
loginStatus: OnlineStatus
): OutgoingPacket = buildOutgoingPacket {
writeQQ(bot)
writeFully(TIMProtocol.fixVer2)
encryptAndWrite(sessionKey) {
writeHex("01 00")
writeUByte(loginStatus.id.toUByte())
writeHex("00 01 00 01 00 04 00 00 00 00")
}
}
internal object ChangeOnlineStatusResponse : Packet {
override fun toString(): String = this::class.simpleName!!
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): ChangeOnlineStatusResponse =
ChangeOnlineStatusResponse
}
\ No newline at end of file
package net.mamoe.mirai.timpc.utils
/**
* 表示这里是不可到达的位置.
*/
@Suppress("NOTHING_TO_INLINE")
internal inline fun assertUnreachable(): Nothing = error("This clause should not be reached")
package net.mamoe.mirai.timpc.utils
import kotlinx.io.core.toByteArray
private const val GTK_BASE_VALUE: Int = 5381
fun getGTK(sKey: String): Int {
var value = GTK_BASE_VALUE
for (c in sKey.toByteArray()) {
value += (value shl 5) + c.toInt()
}
value = value and Int.MAX_VALUE
return value
}
This diff is collapsed.
This diff is collapsed.
<?xml version="1.0" encoding="utf-8"?>
<manifest package="net.mamoe.mirai.timpc">
</manifest>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment