mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-21 15:17:03 +08:00
Merge remote-tracking branch 'origin/master'
# Conflicts: # mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt # mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt
This commit is contained in:
commit
6b698ef4d4
@ -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
|
||||
|
@ -3,8 +3,11 @@ package net.mamoe.mirai.qqandroid
|
||||
import net.mamoe.mirai.BotAccount
|
||||
import net.mamoe.mirai.utils.BotConfiguration
|
||||
import net.mamoe.mirai.utils.Context
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
|
||||
internal actual class QQAndroidBot actual constructor(
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
internal actual class QQAndroidBot
|
||||
actual constructor(
|
||||
context: Context,
|
||||
account: BotAccount,
|
||||
configuration: BotConfiguration
|
||||
|
@ -2,10 +2,9 @@ package net.mamoe.mirai.qqandroid
|
||||
|
||||
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.message.data.ImageId
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
|
||||
import net.mamoe.mirai.utils.*
|
||||
@ -29,7 +28,7 @@ internal class QQImpl(bot: QQAndroidBot, override val coroutineContext: Coroutin
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun uploadImage(image: ExternalImage): ImageId {
|
||||
override suspend fun uploadImage(image: ExternalImage): Image {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
@ -115,7 +114,7 @@ internal class GroupImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun uploadImage(image: ExternalImage): ImageId {
|
||||
override suspend fun uploadImage(image: ExternalImage): Image {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
package net.mamoe.mirai.qqandroid
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.BotAccount
|
||||
import net.mamoe.mirai.BotImpl
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.data.AddFriendResult
|
||||
import net.mamoe.mirai.data.ImageLink
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
|
||||
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
|
||||
import net.mamoe.mirai.qqandroid.utils.ImageIdQQA
|
||||
import net.mamoe.mirai.utils.BotConfiguration
|
||||
import net.mamoe.mirai.utils.Context
|
||||
import net.mamoe.mirai.utils.LockFreeLinkedList
|
||||
@ -56,12 +55,15 @@ internal abstract class QQAndroidBotBase constructor(
|
||||
return groups.delegate.getOrNull(id) ?: throw NoSuchElementException("Can not found group $id")
|
||||
}
|
||||
|
||||
override suspend fun Image.getLink(): ImageLink {
|
||||
require(this.id is ImageIdQQA) { "image.id must be ImageIdQQA" }
|
||||
return (this.id as ImageIdQQA).link
|
||||
override suspend fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override suspend fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult {
|
||||
override suspend fun Image.download(): ByteReadPacket {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override suspend fun Image.downloadAsByteArray(): ByteArray {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
|
@ -22,16 +22,15 @@ import net.mamoe.mirai.qqandroid.QQAndroidBot
|
||||
import net.mamoe.mirai.qqandroid.QQImpl
|
||||
import net.mamoe.mirai.qqandroid.event.ForceOfflineEvent
|
||||
import net.mamoe.mirai.qqandroid.event.PacketReceivedEvent
|
||||
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgSvc
|
||||
import net.mamoe.mirai.qqandroid.network.protocol.packet.*
|
||||
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
|
||||
import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
|
||||
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.LoginPacket
|
||||
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.StatSvc
|
||||
import net.mamoe.mirai.utils.LockFreeLinkedList
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.cryptor.contentToString
|
||||
import net.mamoe.mirai.utils.getValue
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.utils.unsafeWeakRef
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.jvm.Volatile
|
||||
@ -110,6 +109,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
|
||||
}
|
||||
|
||||
override suspend fun init() {
|
||||
// delay(5000)
|
||||
MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendWithoutExpect()
|
||||
|
||||
this@QQAndroidBotNetworkHandler.subscribeAlways<ForceOfflineEvent> {
|
||||
if (this@QQAndroidBotNetworkHandler.bot == this.bot) {
|
||||
close()
|
||||
|
@ -145,7 +145,7 @@ internal class MsgComm : ProtoBuf {
|
||||
@SerialId(1) val lastReadTime: Int = 0,
|
||||
@SerialId(2) val peerUin: Long = 0L,
|
||||
@SerialId(3) val msgCompleted: Int = 0,
|
||||
@SerialId(4) val msg: List<Msg>,
|
||||
@SerialId(4) val msg: List<Msg>? = null,
|
||||
@SerialId(5) val unreadMsgNum: Int = 0,
|
||||
@SerialId(8) val c2cType: Int = 0,
|
||||
@SerialId(9) val serviceType: Int = 0,
|
||||
|
@ -3,14 +3,15 @@ package net.mamoe.mirai.qqandroid.network.protocol.data.proto
|
||||
import kotlinx.serialization.SerialId
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.mamoe.mirai.qqandroid.io.ProtoBuf
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
|
||||
@Serializable
|
||||
class SyncCookie(
|
||||
@SerialId(1) val time1: Long? = null, // 1580277992
|
||||
@SerialId(2) val time: Long, // 1580277992
|
||||
@SerialId(3) val unknown1: Long = Random.nextLong(),// 678328038
|
||||
@SerialId(4) val unknown2: Long = Random.nextLong(), // 1687142153
|
||||
@SerialId(3) val unknown1: Long = Random.nextLong().absoluteValue,// 678328038
|
||||
@SerialId(4) val unknown2: Long = Random.nextLong().absoluteValue, // 1687142153
|
||||
@SerialId(5) val const1: Long = const1_, // 1458467940
|
||||
@SerialId(11) val const2: Long = const2_, // 2683038258
|
||||
@SerialId(12) val unknown3: Long = 0x1d,
|
||||
@ -18,8 +19,8 @@ class SyncCookie(
|
||||
@SerialId(14) val unknown4: Long = 0
|
||||
) : ProtoBuf
|
||||
|
||||
private val const1_: Long = Random.nextLong()
|
||||
private val const2_: Long = Random.nextLong()
|
||||
private val const1_: Long = Random.nextLong().absoluteValue
|
||||
private val const2_: Long = Random.nextLong().absoluteValue
|
||||
/*
|
||||
|
||||
@Serializable
|
||||
|
@ -4,6 +4,7 @@ import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import net.mamoe.mirai.data.MultiPacket
|
||||
import net.mamoe.mirai.data.Packet
|
||||
import net.mamoe.mirai.event.BroadcastControllable
|
||||
import net.mamoe.mirai.message.FriendMessage
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.qqandroid.QQAndroidBot
|
||||
@ -29,7 +30,6 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.utils.cryptor.contentToString
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
import net.mamoe.mirai.utils.io.hexToBytes
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
|
||||
@ -39,14 +39,14 @@ internal class MessageSvc {
|
||||
*/
|
||||
internal object PushNotify : IncomingPacketFactory<RequestPushNotify>("MessageSvc.PushNotify") {
|
||||
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): RequestPushNotify {
|
||||
discardExact(4)
|
||||
discardExact(4) // don't remove
|
||||
|
||||
return decodeUniPacket(RequestPushNotify.serializer())
|
||||
}
|
||||
|
||||
override suspend fun QQAndroidBot.handle(packet: RequestPushNotify, sequenceId: Int): OutgoingPacket? {
|
||||
network.run {
|
||||
return PbGetMsg(client, MsgSvc.SyncFlag.START, packet.stMsgInfo?.uMsgTime ?: 0)
|
||||
return PbGetMsg(client, MsgSvc.SyncFlag.START, packet.stMsgInfo?.uMsgTime ?: currentTimeSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -57,9 +57,6 @@ internal class MessageSvc {
|
||||
*/
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
internal object PbGetMsg : OutgoingPacketFactory<PbGetMsg.Response>("MessageSvc.PbGetMsg") {
|
||||
val EXTRA_DATA =
|
||||
"08 00 12 33 6D 6F 64 65 6C 3A 78 69 67 6F 6D 69 20 36 3B 6F 73 3A 32 32 3B 76 65 72 73 69 6F 6E 3A 76 32 6D 61 6E 3A 78 69 61 6F 6D 69 73 79 73 3A 4C 4D 59 34 38 5A 18 E4 E1 A4 FF FE 2D 20 E9 E1 A4 FF FE 2D 28 A8 E1 A4 FF FE 2D 30 99 E1 A4 FF FE 2D".hexToBytes()
|
||||
|
||||
operator fun invoke(
|
||||
client: QQAndroidClient,
|
||||
syncFlag: MsgSvc.SyncFlag = MsgSvc.SyncFlag.START,
|
||||
@ -67,7 +64,7 @@ internal class MessageSvc {
|
||||
): OutgoingPacket = buildOutgoingUniPacket(
|
||||
client
|
||||
) {
|
||||
println("syncCookie=${client.c2cMessageSync.syncCookie?.toUHexString()}")
|
||||
//println("syncCookie=${client.c2cMessageSync.syncCookie?.toUHexString()}")
|
||||
writeProtoBuf(
|
||||
MsgSvc.PbGetMsgReq.serializer(),
|
||||
MsgSvc.PbGetMsgReq(
|
||||
@ -96,7 +93,11 @@ internal class MessageSvc {
|
||||
* 不要直接 expect 这个 class. 它可能
|
||||
*/
|
||||
@MiraiInternalAPI
|
||||
open class Response(internal val syncFlagFromServer: MsgSvc.SyncFlag, delegate: MutableList<FriendMessage>) : MultiPacket<FriendMessage>(delegate) {
|
||||
open class Response(internal val syncFlagFromServer: MsgSvc.SyncFlag, delegate: MutableList<FriendMessage>) : MultiPacket<FriendMessage>(delegate),
|
||||
BroadcastControllable {
|
||||
override val shouldBroadcast: Boolean
|
||||
get() = syncFlagFromServer == MsgSvc.SyncFlag.STOP
|
||||
|
||||
override fun toString(): String {
|
||||
return "MessageSvc.PbGetMsg.Response($syncFlagFromServer=$syncFlagFromServer, messages=List(size=${this.size}))"
|
||||
}
|
||||
@ -120,7 +121,7 @@ internal class MessageSvc {
|
||||
return GetMsgSuccess(mutableListOf())
|
||||
}
|
||||
|
||||
val messages = resp.uinPairMsgs.asSequence().flatMap { it.msg.asSequence() }.mapNotNull {
|
||||
val messages = resp.uinPairMsgs.asSequence().filterNot { it.msg == null }.flatMap { it.msg!!.asSequence() }.mapNotNull {
|
||||
when (it.msgHead.msgType) {
|
||||
166 -> {
|
||||
FriendMessage(
|
||||
@ -134,7 +135,7 @@ internal class MessageSvc {
|
||||
}
|
||||
}.toMutableList()
|
||||
if (resp.syncFlag == MsgSvc.SyncFlag.STOP) {
|
||||
return GetMsgSuccess(messages)
|
||||
return GetMsgSuccess(mutableListOf(messages.last()))
|
||||
}
|
||||
return Response(resp.syncFlag, messages)
|
||||
}
|
||||
@ -146,7 +147,7 @@ internal class MessageSvc {
|
||||
|
||||
MsgSvc.SyncFlag.CONTINUE -> {
|
||||
network.run {
|
||||
PbGetMsg(client, MsgSvc.SyncFlag.CONTINUE, currentTimeSeconds)
|
||||
PbGetMsg(client, MsgSvc.SyncFlag.CONTINUE, currentTimeSeconds).sendWithoutExpect()
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -199,8 +200,7 @@ internal class MessageSvc {
|
||||
),
|
||||
msgSeq = client.atomicNextMessageSequenceId(),
|
||||
msgRand = Random.nextInt().absoluteValue,
|
||||
syncCookie = client.c2cMessageSync.syncCookie?.takeIf { it.isNotEmpty() }
|
||||
?: SyncCookie(time = currentTimeSeconds).toByteArray(SyncCookie.serializer())
|
||||
syncCookie = SyncCookie(time = currentTimeSeconds).toByteArray(SyncCookie.serializer())
|
||||
// msgVia = 1
|
||||
)
|
||||
)
|
||||
@ -217,21 +217,22 @@ internal class MessageSvc {
|
||||
|
||||
///writeFully("0A 08 0A 06 08 89 FC A6 8C 0B 12 06 08 01 10 00 18 00 1A 1F 0A 1D 12 08 0A 06 0A 04 F0 9F 92 A9 12 11 AA 02 0E 88 01 00 9A 01 08 78 00 F8 01 00 C8 02 00 20 9B 7A 28 F4 CA 9B B8 03 32 34 08 92 C2 C4 F1 05 10 92 C2 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 89 84 F9 A2 06 48 DE 8C EA E5 0E 58 D9 BD BB A0 09 60 1D 68 92 C2 C4 F1 05 70 00 40 01".hexToBytes())
|
||||
|
||||
val seq = client.atomicNextMessageSequenceId()
|
||||
///return@buildOutgoingUniPacket
|
||||
writeProtoBuf(
|
||||
MsgSvc.PbSendMsgReq.serializer(), MsgSvc.PbSendMsgReq(
|
||||
routingHead = MsgSvc.RoutingHead(grp = MsgSvc.Grp(groupCode = groupId)), // TODO: 2020/1/30 确认这里是 id 还是 internalId
|
||||
contentHead = MsgComm.ContentHead(pkgNum = 1),
|
||||
contentHead = MsgComm.ContentHead(pkgNum = 1, divSeq = seq),
|
||||
msgBody = ImMsgBody.MsgBody(
|
||||
richText = ImMsgBody.RichText(
|
||||
elems = message.toRichTextElems()
|
||||
)
|
||||
),
|
||||
msgSeq = client.atomicNextMessageSequenceId(),
|
||||
//msgRand = Random.nextInt() and 0x7FFF,
|
||||
syncCookie = SyncCookie(time = currentTimeSeconds).toByteArray(SyncCookie.serializer())
|
||||
//SyncCookie(currentTimeSeconds, Random.nextLong().absoluteValue, Random.nextLong().absoluteValue).toByteArray(SyncCookie.serializer())
|
||||
// msgVia = 1
|
||||
msgSeq = seq,
|
||||
msgRand = Random.nextInt().absoluteValue,
|
||||
syncCookie = "08 A0 C2 C4 F1 05 10 A0 C2 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 E4 C2 B1 95 03 48 A1 9F E0 C7 08 58 D3 C2 8F A0 09 60 1D 68 A0 C2 C4 F1 05 70 00".hexToBytes()
|
||||
?: SyncCookie(time = currentTimeSeconds + client.timeDifference).toByteArray(SyncCookie.serializer()),
|
||||
msgVia = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -3,11 +3,10 @@
|
||||
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.message.GroupMessage
|
||||
import net.mamoe.mirai.qqandroid.QQAndroidBot
|
||||
import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport
|
||||
import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
|
||||
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
|
||||
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgOnlinePush
|
||||
import net.mamoe.mirai.qqandroid.network.protocol.packet.IncomingPacketFactory
|
||||
@ -22,7 +21,7 @@ internal class OnlinePush {
|
||||
@UseExperimental(ExperimentalStdlibApi::class)
|
||||
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): GroupMessage {
|
||||
// 00 00 02 E4 0A D5 05 0A 4F 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 52 20 00 28 BC 3D 30 8C 82 AB F1 05 38 D2 80 E0 8C 80 80 80 80 02 4A 21 08 E7 C1 AD B8 02 10 01 18 BA 05 22 09 48 69 6D 31 38 38 6D 6F 65 30 06 38 02 42 05 4D 69 72 61 69 50 01 58 01 60 00 88 01 08 12 06 08 01 10 00 18 00 1A F9 04 0A F6 04 0A 26 08 00 10 87 82 AB F1 05 18 B7 B4 BF 30 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 E6 03 42 E3 03 12 2A 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 22 00 2A 04 03 00 00 00 32 60 15 36 20 39 36 6B 45 31 41 38 35 32 32 39 64 63 36 39 38 34 37 39 37 37 62 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 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 31 32 31 32 41 38 C6 BB 8A A9 08 40 FB AE 9E C2 09 48 50 50 41 5A 00 60 01 6A 10 4E 18 58 22 0E 7B F8 0F C5 B1 34 48 83 74 D3 9C 72 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 31 39 38 3F 74 65 72 6D 3D 32 82 01 57 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 30 3F 74 65 72 6D 3D 32 B0 01 4D B8 01 2E C8 01 FF 05 D8 01 4D E0 01 2E FA 01 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 34 30 30 3F 74 65 72 6D 3D 32 80 02 4D 88 02 2E 12 45 AA 02 42 50 03 60 00 68 00 9A 01 39 08 09 20 BF 50 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 08 01 90 04 80 80 80 10 B8 04 00 C0 04 00 12 06 4A 04 08 00 40 01 12 14 82 01 11 0A 09 48 69 6D 31 38 38 6D 6F 65 18 06 20 08 28 03 10 8A CA 9D A1 07 1A 00
|
||||
val pbPushMsg = ProtoBufWithNullableSupport.load(MsgOnlinePush.PbPushMsg.serializer(), readBytes())
|
||||
val pbPushMsg = readProtoBuf(MsgOnlinePush.PbPushMsg.serializer())
|
||||
|
||||
val extraInfo: ImMsgBody.ExtraInfo? = pbPushMsg.msg.msgBody.richText.elems.firstOrNull { it.extraInfo != null }?.extraInfo
|
||||
|
||||
|
@ -5,62 +5,123 @@ import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
|
||||
import net.mamoe.mirai.utils.io.hexToBytes
|
||||
|
||||
internal fun NotOnlineImageFromFile.toJceData(): ImMsgBody.NotOnlineImage {
|
||||
return ImMsgBody.NotOnlineImage(
|
||||
filePath = this.filepath,
|
||||
resId = this.resourceId,
|
||||
oldPicMd5 = false,
|
||||
picMd5 = this.md5,
|
||||
fileLen = this.fileLength,
|
||||
picHeight = this.height,
|
||||
picWidth = this.width,
|
||||
bizType = this.bizType,
|
||||
imgType = this.imageType,
|
||||
downloadPath = this.downloadPath
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
notOnlineImage=NotOnlineImage#2050019814 {
|
||||
filePath=41AEF2D4B5BD24CF3791EFC5FEB67D60.jpg
|
||||
fileLen=0x00000350(848)
|
||||
downloadPath=/f2b7e5c0-acb3-4e83-aa5c-c8383840cc91
|
||||
oldVerSendFile=<Empty ByteArray>
|
||||
imgType=0x000003E8(1000)
|
||||
previewsImage=<Empty ByteArray>
|
||||
picMd5=41 AE F2 D4 B5 BD 24 CF 37 91 EF C5 FE B6 7D 60
|
||||
picHeight=0x00000032(50)
|
||||
picWidth=0x00000033(51)
|
||||
resId=/f2b7e5c0-acb3-4e83-aa5c-c8383840cc91
|
||||
flag=<Empty ByteArray>
|
||||
thumbUrl=
|
||||
original=0x00000000(0)
|
||||
bigUrl=
|
||||
origUrl=
|
||||
bizType=0x00000005(5)
|
||||
result=0x00000000(0)
|
||||
index=0x00000000(0)
|
||||
opFaceBuf=<Empty ByteArray>
|
||||
oldPicMd5=false
|
||||
thumbWidth=0x00000000(0)
|
||||
thumbHeight=0x00000000(0)
|
||||
fileId=0x00000000(0)
|
||||
showLen=0x00000000(0)
|
||||
downloadLen=0x00000000(0)
|
||||
_400Url=
|
||||
_400Width=0x00000000(0)
|
||||
_400Height=0x00000000(0)
|
||||
pbReserve=08 01 10 00 32 00 42 0E 5B E5 8A A8 E7 94 BB E8 A1 A8 E6 83 85 5D 50 00 78 05
|
||||
}
|
||||
*/
|
||||
internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> {
|
||||
val elems = mutableListOf<ImMsgBody.Elem>()
|
||||
val elements = mutableListOf<ImMsgBody.Elem>()
|
||||
|
||||
this.forEach {
|
||||
when (it) {
|
||||
is PlainText -> {
|
||||
elems.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue)))
|
||||
elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue)))
|
||||
}
|
||||
is At -> {
|
||||
|
||||
}
|
||||
is Image -> {
|
||||
elems.add(
|
||||
ImMsgBody.Elem(
|
||||
notOnlineImage = ImMsgBody.NotOnlineImage(
|
||||
filePath = it.id.value, // 错了, 应该是 2B23D705CAD1F2CF3710FE582692FCC4.jpg
|
||||
fileLen = 1149, // 假的
|
||||
downloadPath = it.id.value,
|
||||
imgType = 1000, // 不确定
|
||||
picMd5 = "2B 23 D7 05 CA D1 F2 CF 37 10 FE 58 26 92 FC C4".hexToBytes(),
|
||||
picHeight = 66,
|
||||
picWidth = 66,
|
||||
resId = it.id.value,
|
||||
bizType = 5,
|
||||
pbReserve = ImMsgBody.PbReserve.DEFAULT // 可能还可以改变 `[动画表情]`
|
||||
)
|
||||
)
|
||||
)
|
||||
is NotOnlineImageFromServer -> {
|
||||
elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate))
|
||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 90 01 01 F8 01 00 A0 02 00 C8 02 00".hexToBytes())))
|
||||
}
|
||||
is NotOnlineImageFromFile -> {
|
||||
elements.add(ImMsgBody.Elem(notOnlineImage = it.toJceData()))
|
||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 90 01 01 F8 01 00 A0 02 00 C8 02 00".hexToBytes())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return elems
|
||||
return elements
|
||||
}
|
||||
|
||||
|
||||
internal class NotOnlineImageFromServer(
|
||||
internal val delegate: ImMsgBody.NotOnlineImage
|
||||
) : NotOnlineImage() {
|
||||
override val resourceId: String
|
||||
get() = delegate.resId
|
||||
override val md5: ByteArray
|
||||
get() = delegate.picMd5
|
||||
override val filepath: String
|
||||
get() = delegate.filePath
|
||||
override val fileLength: Int
|
||||
get() = delegate.fileLen
|
||||
override val height: Int
|
||||
get() = delegate.picHeight
|
||||
override val width: Int
|
||||
get() = delegate.picWidth
|
||||
override val bizType: Int
|
||||
get() = delegate.bizType
|
||||
override val imageType: Int
|
||||
get() = delegate.imgType
|
||||
override val downloadPath: String
|
||||
get() = delegate.downloadPath
|
||||
|
||||
}
|
||||
|
||||
internal fun ImMsgBody.RichText.toMessageChain(): MessageChain {
|
||||
val message = MessageChain(initialCapacity = elems.size)
|
||||
|
||||
elems.forEach {
|
||||
when {
|
||||
it.notOnlineImage != null -> message.add(
|
||||
Image(
|
||||
ImageIdQQA(
|
||||
it.notOnlineImage.resId,
|
||||
it.notOnlineImage.origUrl
|
||||
)
|
||||
)
|
||||
NotOnlineImageFromServer(it.notOnlineImage)
|
||||
)
|
||||
it.customFace != null -> message.add(
|
||||
Image(
|
||||
ImageIdQQA(
|
||||
NotOnlineImageFromFile(
|
||||
it.customFace.filePath,
|
||||
it.customFace.origUrl
|
||||
)
|
||||
it.customFace.md5,
|
||||
it.customFace.origUrl,
|
||||
it.customFace.downloadLen,
|
||||
it.customFace.height,
|
||||
it.customFace.width,
|
||||
it.customFace.bizType,
|
||||
it.customFace.imageType,
|
||||
it.customFace.filePath
|
||||
)
|
||||
)
|
||||
it.text != null -> message.add(it.text.str.toMessage())
|
||||
@ -70,12 +131,5 @@ internal fun ImMsgBody.RichText.toMessageChain(): MessageChain {
|
||||
return message
|
||||
}
|
||||
|
||||
internal class ImageIdQQA(
|
||||
override val value: String,
|
||||
originalLink: String
|
||||
) : ImageId {
|
||||
val link: ImageLink =
|
||||
ImageLinkQQA("http://gchat.qpic.cn$originalLink")
|
||||
}
|
||||
|
||||
internal inline class ImageLinkQQA(override val original: String) : ImageLink
|
@ -1,9 +0,0 @@
|
||||
# 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`
|
@ -1,118 +0,0 @@
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
@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()
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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()
|
@ -1,33 +0,0 @@
|
||||
@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))
|
@ -1,239 +0,0 @@
|
||||
package net.mamoe.mirai.timpc
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.BotAccount
|
||||
import net.mamoe.mirai.BotImpl
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.data.AddFriendResult
|
||||
import net.mamoe.mirai.data.ImageLink
|
||||
import net.mamoe.mirai.data.Packet
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.ImageId0x03
|
||||
import net.mamoe.mirai.message.data.ImageId0x06
|
||||
import net.mamoe.mirai.timpc.internal.RawGroupInfo
|
||||
import net.mamoe.mirai.timpc.network.GroupImpl
|
||||
import net.mamoe.mirai.timpc.network.MemberImpl
|
||||
import net.mamoe.mirai.timpc.network.QQImpl
|
||||
import net.mamoe.mirai.timpc.network.TIMPCBotNetworkHandler
|
||||
import net.mamoe.mirai.timpc.network.handler.TemporaryPacketHandler
|
||||
import net.mamoe.mirai.timpc.network.packet.KnownPacketId
|
||||
import net.mamoe.mirai.timpc.network.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.SessionKey
|
||||
import net.mamoe.mirai.timpc.network.packet.action.*
|
||||
import net.mamoe.mirai.timpc.network.packet.event.EventPacketFactory
|
||||
import net.mamoe.mirai.timpc.network.packet.event.FriendOnlineStatusChangedPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.login.*
|
||||
import net.mamoe.mirai.timpc.utils.assertUnreachable
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.io.logStacktrace
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
|
||||
internal expect class TIMPCBot constructor(
|
||||
account: BotAccount,
|
||||
configuration: BotConfiguration
|
||||
) : TIMPCBotBase
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
internal abstract class TIMPCBotBase constructor(
|
||||
account: BotAccount,
|
||||
configuration: BotConfiguration
|
||||
) : BotImpl<TIMPCBotNetworkHandler>(account, configuration) {
|
||||
|
||||
@UseExperimental(ExperimentalUnsignedTypes::class)
|
||||
companion object {
|
||||
init {
|
||||
KnownPacketId[0x0825u] = TouchPacket
|
||||
KnownPacketId[0x0828u] = RequestSessionPacket
|
||||
KnownPacketId[0x0836u] = SubmitPasswordPacket
|
||||
KnownPacketId[0x00BAu] = CaptchaPacket
|
||||
KnownPacketId[0x00CEu] = EventPacketFactory
|
||||
KnownPacketId[0x0017u] = EventPacketFactory
|
||||
KnownPacketId[0x0081u] = FriendOnlineStatusChangedPacket
|
||||
KnownPacketId[0x00ECu] = ChangeOnlineStatusPacket
|
||||
|
||||
KnownPacketId[0x0058u] = HeartbeatPacket
|
||||
KnownPacketId[0x001Du] = RequestSKeyPacket
|
||||
KnownPacketId[0x005Cu] = RequestAccountInfoPacket
|
||||
KnownPacketId[0x0002u] = GroupPacket
|
||||
KnownPacketId[0x00CDu] = SendFriendMessagePacket
|
||||
KnownPacketId[0x00A7u] = CanAddFriendPacket
|
||||
KnownPacketId[0x00A8u] = AddFriendPacket
|
||||
KnownPacketId[0x00AEu] = RequestFriendAdditionKeyPacket
|
||||
KnownPacketId[0x0388u] = GroupImagePacket
|
||||
KnownPacketId[0x0352u] = FriendImagePacket
|
||||
|
||||
KnownPacketId[0x0031u] = RequestProfileAvatarPacket
|
||||
KnownPacketId[0x003Cu] = RequestProfileDetailsPacket
|
||||
KnownPacketId[0x0126u] = QueryNicknamePacket
|
||||
|
||||
KnownPacketId[0x01BCu] = QueryPreviousNamePacket
|
||||
|
||||
KnownPacketId[0x003Eu] = QueryFriendRemarkPacket
|
||||
// 031F 查询 "新朋友" 记录
|
||||
|
||||
// @Suppress("DEPRECATION")
|
||||
// inline SUBMIT_IMAGE_FILE_NAME(0x01BDu, SubmitImageFilenamePacket),
|
||||
}
|
||||
}
|
||||
|
||||
inline val sessionKey: SessionKey get()= network.sessionKey
|
||||
|
||||
override fun createNetworkHandler(coroutineContext: CoroutineContext): TIMPCBotNetworkHandler {
|
||||
return TIMPCBotNetworkHandler(coroutineContext, this as TIMPCBot)
|
||||
}
|
||||
|
||||
final override suspend fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult {
|
||||
return when (CanAddFriendPacket(uin, id, sessionKey).sendAndExpect<CanAddFriendResponse>()) {
|
||||
is CanAddFriendResponse.AlreadyAdded -> AddFriendResult.ALREADY_ADDED
|
||||
is CanAddFriendResponse.Rejected -> AddFriendResult.REJECTED
|
||||
|
||||
is CanAddFriendResponse.ReadyToAdd,
|
||||
is CanAddFriendResponse.RequireVerification -> {
|
||||
val key = RequestFriendAdditionKeyPacket(uin, id, sessionKey).sendAndExpect<RequestFriendAdditionKeyPacket.Response>().key
|
||||
AddFriendPacket.RequestAdd(uin, id, sessionKey, message, remark, key).sendAndExpect<AddFriendPacket.Response>()
|
||||
AddFriendResult.WAITING_FOR_APPROVAL
|
||||
} //这个做的是需要验证消息的情况, 不确定 ReadyToAdd 的是啥
|
||||
|
||||
// 似乎 RequireVerification 和 ReadyToAdd 判断错了. 需要重新检查一下
|
||||
|
||||
// TODO: 2019/11/11 需要验证问题的情况
|
||||
|
||||
/*is CanAddFriendResponse.ReadyToAdd -> {
|
||||
// TODO: 2019/11/11 这不需要验证信息的情况
|
||||
|
||||
//AddFriendPacket(qqAccount, id, sessionKey, ).sendAndExpectAsync<AddFriendPacket.Response>().await()
|
||||
TODO()
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
final override suspend fun approveFriendAddRequest(id: Long, remark: String?) {
|
||||
AddFriendPacket.Approve(uin, sessionKey, 0, id, remark).sendAndExpect<AddFriendPacket.Response>()
|
||||
}
|
||||
|
||||
|
||||
// region contacts
|
||||
|
||||
final override val groups: ContactList<Group> = ContactList(LockFreeLinkedList())
|
||||
final override val qqs: ContactList<QQ> = ContactList(LockFreeLinkedList())
|
||||
|
||||
/**
|
||||
* 线程安全地获取缓存的 QQ 对象. 若没有对应的缓存, 则会创建一个.
|
||||
*/
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
@JvmSynthetic
|
||||
final override fun getQQ(id: Long): QQ = qqs.delegate.getOrAdd(id) { QQ(id) }
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class, ExperimentalUnsignedTypes::class)
|
||||
final override suspend fun getGroup(id: GroupId): Group = groups.delegate.getOrNull(id.value) ?: inline {
|
||||
val info: RawGroupInfo = try {
|
||||
when (val response =
|
||||
GroupPacket.QueryGroupInfo(uin, id.toInternalId(), sessionKey).sendAndExpect<GroupPacket.InfoResponse>()) {
|
||||
is RawGroupInfo -> response
|
||||
is GroupNotFound -> throw GroupNotFoundException("id=${id.value}")
|
||||
else -> assertUnreachable()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("Cannot obtain group info for id ${id.value}", e)
|
||||
}
|
||||
|
||||
return groups.delegate.getOrAdd(id.value) { Group(id, info) }
|
||||
}
|
||||
|
||||
final override suspend fun getGroup(internalId: GroupInternalId): Group =
|
||||
getGroup0(internalId.toId().value)
|
||||
|
||||
private suspend inline fun getGroup0(id: Long): Group =
|
||||
groups.delegate.getOrNull(id) ?: inline {
|
||||
val info: RawGroupInfo = try {
|
||||
GroupPacket.QueryGroupInfo(uin, GroupId(id).toInternalId(), sessionKey).sendAndExpect()
|
||||
} catch (e: Exception) {
|
||||
e.logStacktrace()
|
||||
error("Cannot obtain group info for id $id")
|
||||
}
|
||||
|
||||
return groups.delegate.getOrAdd(id) { Group(GroupId(id), info) }
|
||||
}
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
final override suspend fun getGroup(id: Long): Group = getGroup0(id.coerceAtLeastOrFail(0))
|
||||
|
||||
|
||||
internal suspend inline fun <reified P : Packet, R> OutgoingPacket.sendAndExpectAsync(
|
||||
checkSequence: Boolean = true,
|
||||
noinline handler: suspend (P) -> R
|
||||
): Deferred<R> {
|
||||
val deferred: CompletableDeferred<R> = CompletableDeferred(coroutineContext[Job])
|
||||
network.temporaryPacketHandlers.addLast(
|
||||
TemporaryPacketHandler(
|
||||
expectationClass = P::class,
|
||||
deferred = deferred,
|
||||
checkSequence = if (checkSequence) this.sequenceId else null,
|
||||
callerContext = coroutineContext + deferred,
|
||||
handler = handler
|
||||
)
|
||||
)
|
||||
network.socket.sendPacket(this)
|
||||
return deferred
|
||||
}
|
||||
|
||||
internal suspend inline fun <reified P : Packet> OutgoingPacket.sendAndExpectAsync(checkSequence: Boolean = true): Deferred<P> =
|
||||
sendAndExpectAsync<P, P>(checkSequence) { it }
|
||||
|
||||
internal suspend inline fun <reified P : Packet, R> OutgoingPacket.sendAndExpect(
|
||||
checkSequence: Boolean = true,
|
||||
timeoutMillis: Long = 5.secondsToMillis,
|
||||
crossinline mapper: (P) -> R
|
||||
): R = withTimeout(timeoutMillis) { sendAndExpectAsync<P, R>(checkSequence) { mapper(it) }.await() }
|
||||
|
||||
internal suspend inline fun <reified P : Packet> OutgoingPacket.sendAndExpect(
|
||||
checkSequence: Boolean = true,
|
||||
timeoutMillis: Long = 5.secondsToMillis
|
||||
): P = withTimeout(timeoutMillis) { sendAndExpectAsync<P, P>(checkSequence) { it }.await() }
|
||||
|
||||
internal suspend inline fun OutgoingPacket.send() = network.socket.sendPacket(this)
|
||||
|
||||
|
||||
final override suspend fun Image.getLink(): ImageLink = when (val id = this.id) {
|
||||
is ImageId0x03 -> GroupImagePacket.RequestImageLink(uin, sessionKey, id).sendAndExpect<GroupImageLink>().requireSuccess()
|
||||
is ImageId0x06 -> FriendImagePacket.RequestImageLink(uin, sessionKey, id).sendAndExpect<FriendImageLink>()
|
||||
else -> assertUnreachable()
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@PublishedApi
|
||||
internal fun CoroutineScope.Group(groupId: GroupId, info: RawGroupInfo): Group {
|
||||
return GroupImpl(this@TIMPCBotBase as TIMPCBot, groupId, coroutineContext).apply { this.info = info.parseBy(this); launch { startUpdater() } }
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@PublishedApi
|
||||
internal fun Group.Member(qq: QQ, permission: MemberPermission): Member {
|
||||
return MemberImpl(qq, this, permission, coroutineContext).apply { launch { startUpdater() } }
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
internal fun CoroutineScope.QQ(id: Long): QQ =
|
||||
QQImpl(this@TIMPCBotBase as TIMPCBot, id, coroutineContext).apply { launch { startUpdater() } }
|
||||
}
|
||||
|
||||
internal inline fun <R> inline(block: () -> R): R = block()
|
||||
|
||||
internal suspend inline fun TIMPCBot.sendPacket(toSend: OutgoingPacket) = this.network.socket.sendPacket(toSend)
|
||||
|
||||
/**
|
||||
* 以 [Bot] 作为接收器 (receiver) 并调用 [block], 返回 [block] 的返回值.
|
||||
* 这个方法将能帮助使用在 [Bot] 中定义的一些扩展方法
|
||||
*/
|
||||
@UseExperimental(ExperimentalContracts::class)
|
||||
internal inline fun <R> Contact.withTIMPCBot(block: TIMPCBot.() -> R): R {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return (bot as TIMPCBot).run(block)
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
@ -1,368 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.timpc.message.internal
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.message.MessageType
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.utils.unzip
|
||||
|
||||
internal fun IoBuffer.parseMessageFace(): Face {
|
||||
debugIfFail("Analyzing Face") {
|
||||
discardExact(1)
|
||||
|
||||
//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
|
||||
|
||||
//00 01 0D FF 00 02 14 4E
|
||||
|
||||
// FIXME: 2019/11/20 EMOJI 表情会解析失败
|
||||
val id1 = FaceId(readLVNumber().toInt().toUByte())//可能这个是id, 也可能下面那个
|
||||
// discardExact(readByte().toLong()) // -1
|
||||
// readLVNumber()//某id?
|
||||
discard()
|
||||
return Face(id1)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun IoBuffer.parsePlainTextOrAt(): Message {
|
||||
// 06 00 0D 00 01 00 00 00 08 00 76 E4 B8 DD 00 00
|
||||
discardExact(1)// 0x01 whenever plain or at
|
||||
val msg = readUShortLVString()
|
||||
return if (this.readRemaining == 0) {
|
||||
PlainText(msg)
|
||||
} else {
|
||||
discardExact(10)
|
||||
At(readQQ())
|
||||
}
|
||||
}
|
||||
|
||||
internal fun IoBuffer.parseLongText0x19(): PlainText {
|
||||
discardExact(1)//0x01
|
||||
discard()
|
||||
//AA 02 33 50 00 60 00 68 00 9A 01 2A 08 0A 20 C1 50 78 A7 C0 04 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 00 B0 03 00 C0 03 00 D0 03 00 E8 03 00
|
||||
//AA 02 33 50 00 60 00 68 00 9A 01 2A 08 09 20 CB 50 78 80 80 04 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 D3 02 A0 03 10 B0 03 00 C0 03 AF 9C 01 D0 03 00 E8 03 00
|
||||
//AA 02 30 50 00 60 00 68 00 9A 01 27 08 0A 78 A7 C0 04 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 00 B0 03 00 C0 03 00 D0 03 00 E8 03 00
|
||||
// 应该是手机发送时的字体或气泡之类的
|
||||
// println("parseLongText0x19.raw=${raw.toHexString()}")
|
||||
return PlainText("")
|
||||
}
|
||||
|
||||
internal fun IoBuffer.parseMessageImage0x06(): Image {
|
||||
discardExact(1)
|
||||
//MiraiLogger.debug(this.toHexString())
|
||||
val filenameLength = readShort()
|
||||
|
||||
discardExact(filenameLength.toInt())
|
||||
discardExact(8)//03 00 04 00 00 02 9C 04
|
||||
val length = readShort()//=27
|
||||
return Image(ImageId0x06(readString(length)))
|
||||
//return Image("{${readString(length)}}.$suffix")
|
||||
}
|
||||
|
||||
internal fun IoBuffer.parseMessageXML(): XMLMessage {
|
||||
discardExact(1)
|
||||
discardExact(readUShort())
|
||||
discardExact(1)
|
||||
/*
|
||||
网易云音乐分享
|
||||
19
|
||||
[01 BB]
|
||||
01
|
||||
[01 B8] 9A 03 B4 03 0A B1 03 01 78 9C 6D 51 CB 8A 14 31 14 FD 95 21 2B 85 A6 AA D2 F5 EA 34 88 B8 92 41 DD F9 58 88 34 31 95 2A E3 74 25 21 8F 19 A6 87 DE 54 83 C8 AC 1C 74 E1 6E 36 EE 86 01 C1 8D D3 20 7E 4C B5 0E FE 85 37 D5 4E 83 28 59 84 7B EE C9 B9 E7 9E 9C 20 AA 35 9A 22 A6 DA C8 71 C9 B8 74 91 75 C6 33 D7 DA 06 8D 00 97 B5 68 D0 F4 04 51 EF 94 15 0B 8E A6 D0 E6 D0 71 A2 85 02 E7 65 91 25 05 C1 78 84 6A 65 8E A8 A9 6E 18 4E 1D 70 09 DA A4 CE D3 62 5C BE 24 78 4C 38 AD 6B 56 67 39 CB 26 39 A1 09 61 A4 A4 30 C6 1D 6B D0 42 52 99 96 CE D1 72 84 2A 6E 19 00 BF CE BF 6C AE DE 01 A1 E5 8E 06 17 AD B7 82 0D 76 60 BE 0A EA D0 A4 B2 32 4A 54 33 7D D0 CC 24 0D AE 06 54 EB D9 56 17 0F 85 00 63 38 49 32 92 27 93 7C 37 E1 81 58 78 49 F7 EE ED C7 9B AF 97 D7 1F 3E F7 AB 4F 7D 77 D5 77 DF FB EE 02 44 5E FB 56 3F 31 73 20 BE 72 4E 4F E3 78 30 10 E1 22 8D 20 B2 D8 2A D9 C4 38 9D A4 24 4B 49 3A 8E EF 7A CB 8D A8 EE A4 E3 2C 29 8B 34 29 83 F5 F0 E2 2F 0D 6F E6 11 93 71 4E 26 CF 16 4F 1F 03 45 1B 7E 28 F8 D1 BF 8C E3 E6 7E FA B0 05 86 55 DE 30 FE C8 36 FB B0 05 4A 76 C8 4C B0 9B 14 FE 00 7E 98 14 32 A5 F0 6F E8 FA DB D9 CF 8F EF 37 EB B3 5D 94 4E B8 79 88 A8 EF 2E FB 6E DD AF 4E FB D5 F9 DE 2D A6 0E B9 B9 8D 96 CB E0 46 B5 DA 01 E3 F9 8F B7 6F 36 EB 8B 17 FF 65 8E 10 5C C1 4B 14 0E 0E F5 76 85 ED 17 2D 7F 03 2B B9 D5 0A
|
||||
01
|
||||
[00 8B]
|
||||
01
|
||||
[00 88] 5B E5 88 86 E4 BA AB 5D E3 82 AD E3 82 BA E3 83 8A E3 83 9F 20 28 63 6F 76 65 72 29 0A 4B 69 7A 75 6E 61 20 41 49 2F E4 B8 AD E7 94 B0 E3 83 A4 E3 82 B9 E3 82 BF E3 82 AB 0A 68 74 74 70 3A 2F 2F 6D 75 73 69 63 2E 31 36 33 2E 63 6F 6D 2F 73 6F 6E 67 2F 31 33 38 33 39 34 33 39 33 32 2F 3F 75 73 65 72 69 64 3D 33 32 34 30 37 36 33 30 37 0A E6 9D A5 E8 87 AA 3A 20 E7 BD 91 E6 98 93 E4 BA 91 E9 9F B3 E4 B9 90
|
||||
|
||||
19
|
||||
[00 41]
|
||||
01
|
||||
[00 3E] AA 02 3B 08 00 50 03 60 00 68 00 88 01 00 9A 01 2D 08 09 20 CB 50 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 90 04 00
|
||||
0E
|
||||
[00 0E]
|
||||
01
|
||||
[00 04] 00 00 00 09
|
||||
0A
|
||||
[00 04] 00 00 00 00
|
||||
12
|
||||
[00 25]
|
||||
05 00 04 00 00 00 03 08 00 04 00 00 00 04 01 00
|
||||
[09] 48 69 6D 31 38 38 6D 6F 65
|
||||
03 00 01 01 04 00 04 00 00 00 08
|
||||
|
||||
*/
|
||||
return XMLMessage(readBytes().unzip().encodeToString())
|
||||
}
|
||||
|
||||
//00 1B filenameLength
|
||||
// 43 37 46 29 5F 34 32 34 4E 33 55 37 7B 4C 47 36 7D 4F 25 5A 51 58 51 2E 6A 70 67 get suffix
|
||||
// 03 00 04 00 00 02 9C 04
|
||||
// 00 25 2F 32 65 37 61 65 33 36 66 2D 61 39 31 63 2D 34 31 32 39 2D 62 61 34 32 2D 37 65 30 31 32 39 37 37 35 63 63 38 14
|
||||
// 00 04 03 00 00 00 18
|
||||
// 00 25 2F 32 65 37 61 65 33 36 66 2D 61 39 31 63 2D 34 31 32 39 2D 62 61 34 32 2D 37 65 30 31 32 39 37 37 35 63 63 38 19
|
||||
// 00 04 00 00 00 2E 1A 00 04 00 00 00 2E FF
|
||||
// 00 63 16 20 20 39 39 31 30 20 38 38 31 43 42 20 20 20 20 20 20 20 36 36 38 65 35 43 36 38 45 36 42 44 32 46 35 38 34 31 42 30 39 37 39 45 37 46 32 35 34 33 38 38 31 33 43 33 2E 6A 70 67 66 2F 32 65 37 61 65 33 36 66 2D 61 39 31 63 2D 34 31 32 39 2D 62 61 34 32 2D 37 65 30 31 32 39 37 37 35 63 63 38 41
|
||||
|
||||
internal fun IoBuffer.parseMessageImage0x03(): Image {
|
||||
//discardExact(1)
|
||||
val tlv = readTLVMap(expectingEOF = true, tagSize = 1)
|
||||
//tlv.printTLVMap("parseMessageImage0x03")
|
||||
// 02 [ ] IMAGE ID
|
||||
// 04 [00 04] 8B 88 95 C1
|
||||
// 05 [00 04] 3B 24 59 FC
|
||||
// 06 [00 04] 00 00 01 BB
|
||||
// 07 [00 01] 42
|
||||
// 08 [00 10] 32 55 6D 52 33 4E 53 64 41 68 44 78 71 56 79 41
|
||||
// 09 [00 01] 01
|
||||
// 0A [00 10] F5 C3 E6 86 70 EA 03 3E D3 30 1D 2A B5 2E D6 5C
|
||||
// 15 [00 04] 00 00 03 76
|
||||
// 16 [00 04] 00 00 07 80
|
||||
// 17 [00 02] 00 65
|
||||
// 18 [00 04] 00 01 A6 F2
|
||||
// 19 [00 01] 00
|
||||
// 1A [00 04] 00 00 81 5C
|
||||
// 1B [00 04] 00 01 3D 27
|
||||
// 1C [00 04] 00 00 03 EB
|
||||
// FF [00 56] 15 36 20 38 36 65 41 31 42 38 62 38 38 39 35 63 31 33 62 32 34 35 39 66 63 20 20 20 20 20 31 62 62 32 55 6D 52 33 4E 53 64 41 68 44 78 71 56 79 41 46 35 43 33 45 36 38 36 37 30 45 41 30 33 33 45 44 33 33 30 31 44 32 41 42 35 32 45 44 36 35 43 2E 6A 70 67 41
|
||||
// return if (tlv.containsKey(0x0Au)) {
|
||||
return Image(
|
||||
ImageId0x03(
|
||||
String(tlv[0x02]!!).adjustImageId(),
|
||||
// tlv[0x0Au],
|
||||
uniqueId = tlv[0x04]!!.read { readUInt() },
|
||||
height = tlv[0x16]!!.toUInt().toInt(),
|
||||
width = tlv[0x15]!!.toUInt().toInt()
|
||||
)//.also { debugPrintln("ImageId: $it") }
|
||||
)
|
||||
//} else {
|
||||
// Image(
|
||||
// ImageId0x06(
|
||||
// String(tlv[0x02u]!!).adjustImageId()
|
||||
// )
|
||||
// )
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
private operator fun String.get(range: IntRange) = this.substring(range)
|
||||
|
||||
// 有些时候会得到 724D95122B54EEAC1E214AAAC37259DF.gif
|
||||
// 需要调整 {724D9512-2B54-EEAC-1E21-4AAAC37259DF}.gif
|
||||
|
||||
private fun String.adjustImageId() =
|
||||
if (this.first() == '{') this
|
||||
else "{${this[0..7]}-${this[8..11]}-${this[12..15]}-${this[16..19]}-${this[20..31]}}.${this.substringAfterLast(".")}"
|
||||
|
||||
internal fun ByteReadPacket.readMessage(): Message? {
|
||||
val messageType = this.readByte().toInt()
|
||||
val sectionData = this.readIoBuffer(this.readShort().toInt())
|
||||
|
||||
return try {
|
||||
when (messageType) {
|
||||
0x01 -> sectionData.parsePlainTextOrAt()
|
||||
0x02 -> sectionData.parseMessageFace()
|
||||
0x03 -> sectionData.parseMessageImage0x03()
|
||||
0x06 -> sectionData.parseMessageImage0x06()
|
||||
|
||||
|
||||
0x19 -> {
|
||||
//19 00 5C / 01 00 59 AA 02 56 30 01 3A 40 6E 35 46 4F 62 68 75 4B 6F 65 31 4E 63 45 41 6B 77 4B 51 5A 5A 4C 47 54 57 43 68 30 4B 56 7A 57 44 38 67 58 70 37 62 77 6A 67 51 69 66 66 53 4A 63 4F 69 78 4F 75 37 36 49 49 4F 37 48 32 55 63 9A 01 0F 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 14 01 75 01 01 6B 01 78 9C CD 92 BB 4E C3 30 14 86 77 9E C2 32 73 DA A4 21 24 48 4E AA F4 06 A5 B4 51 55 A0 A8 0B 4A 5D 27 35 E4 82 72 69 4B B7 6E 08 06 C4 C0 06 42 48 30 20 21 60 62 EB E3 34 F4 31 70 4A 11 23 23 FC 96 2C F9 D8 BF CF F1 77 8C F2 23 D7 01 03 12 84 D4 F7 54 28 64 78 08 88 87 FD 1E F5 6C 15 C6 91 C5 29 30 AF AD 00 26 E4 86 36 E8 06 94 58 2A CC FC 73 41 E0 1E 5A D4 21 0D D3 25 2A 2C 55 0A 1B D2 BA 5E E0 24 91 D7 B9 B5 72 41 E1 74 B9 5C E0 78 25 27 8B 92 28 14 45 45 FF 76 B4 E8 98 39 18 05 13 47 0B 24 03 4A 86 F5 D8 89 68 3D B4 21 B0 1C 93 71 11 21 08 49 30 A0 98 54 4B 6C 25 A5 E6 80 84 B4 A7 42 4F AA 18 DD 7E 5C F3 89 D0 C0 65 FD 78 58 6B 76 3A 3B 9B BB ED 62 9F AF ED 8F DB 25 C5 3E 38 91 BB C3 23 BB 49 2D AB B5 8D 0D 3A 32 62 79 BD 5A 35 E4 AD DC 1E 86 40 03 88 46 C4 05 8E 79 EA C7 11 EB 09 64 91 88 46 0E D1 C0 5F 73 FD 4D 00 65 97 95 02 D4 0F 34 94 65 D3 B2 78 80 7D C7 0F 54 B8 AA F0 E9 60 8F 4A EE 1E 3F 6E 2E 84 E4 F6 7E 3E 7D 9E 5D 5E 25 EF 67 C9 E4 15 FC DC 81 B2 29 08 0D 85 7E 1C 60 02 BC 45 33 E7 93 F3 D9 C3 D3 FC E5 6D 36 BD 86 2C C3 D7 66 7A 98 FD 4F ED 13 9B C7 C1 78 02 00 04 00 00 00 23 0E 00 07 01 00 04 00 00 00 09
|
||||
//1000个 "." 被压缩为上面这条消息?
|
||||
|
||||
//bot手机自己跟自己发消息会出这个
|
||||
//似乎手机发消息就会有这个?
|
||||
//sectionData: 01 00 1C AA 02 19 08 00 88 01 00 9A 01 11 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00
|
||||
// 01 00 1C AA 02 19 08 00 88 01 00 9A 01 11 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00
|
||||
//return null
|
||||
|
||||
return null
|
||||
// sectionData.parseLongText0x19()
|
||||
}
|
||||
|
||||
// XML
|
||||
/*
|
||||
* 19 [01 AD] 01 01 AA 9A 03 A6 03 0A A3 03 01 78 9C A5 91 CB 4E E3 30 14 86 5F A5 F2 BA 4A 93 A6 49 DA EE 59 B1 06 69 34 42 95 9B B8 25 D0 D8 C6 71 5A 31 A8 52 C5 02 CA C0 86 E1 BA E0 22 16 08 21 2A 71 D9 50 31 C3 D3 10 37 E5 2D 38 4E 87 D9 CC 12 59 B2 74 FE F3 FB 3B FF 91 37 10 E6 1C D5 91 CF 22 43 12 EA 13 2A 8D 58 8A C4 97 51 DC 46 45 D0 69 2B 6C A3 FA 06 C2 89 64 71 F8 83 A0 3A B4 09 74 64 18 41 61 39 9E 63 7B AE 57 AE 14 51 8B 89 1E 16 C1 A7 43 B2 55 42 81 6D D7 1C DF B3 3C DB 2A BB 2D AB EA 60 D7 0D AA 15 B3 59 29 37 2B 6E 15 9B 4D 18 23 D7 39 B0 10 65 22 C2 1D D4 2F A2 80 C4 3E 08 93 E3 87 F7 93 DF 60 88 88 C4 3A 05 25 BD 38 4F 03 E3 99 86 43 0F D3 40 B0 30 68 F0 D5 76 83 62 1D 2A 57 39 6F CC B0 56 5E 84 90 CB 32 CD 9A 63 79 9E FB 6F 80 7A 7E 9A DE 6C 66 67 BB B6 63 9A 46 2D 1D 6F 4F 46 57 F0 7A 25 89 F8 82 E8 80 63 59 4A 5E 2F 95 12 D1 31 7C 5A 72 F8 5C 3C 9F AC 80 83 0B D2 0D 49 EF 7F 87 DB B5 BF 2D AE 81 23 66 89 F0 49 23 F4 3F 83 FE 15 92 9C AB B7 C6 6D 1D E1 E0 50 1D DC CE 6E 2D 86 B2 A3 57 78 1B FC 54 C3 81 DA D9 CF AE 2F D4 E5 65 21 1B 3E A6 E3 D7 74 7C 9E DD 6F 17 D4 9F CD EC 68 6F 32 3C 4D 5F 76 DE 06 BB D9 68 54 9D DE 9F 17 A6 57 03 F5 6B 4F ED BF A2 7E 5F 27 64 11 97 80 FA AE 86 5B E9 CB DD D2 D7 90 45 D4 25 02 68 A6 A1 8F A5 EB D9 FE F9 9F F4 3F 00 FB FE EF 17
|
||||
*
|
||||
* 01 00 89 01 00 86 5B E5 88 86 E4 BA AB 5D E3 80 8A E5 88 80 E5 89 91 E7 A5 9E E5 9F 9F 20 E7 88 B1 E4 B8 BD E4 B8 9D E7 AF 87 20 E5 BC 82 E7 95 8C E6 88 98 E4 BA 89 E3 80 8B E7 AC AC 38 E8 AF 9D 20 E8 A1 80 E5 92 8C E5 91 BD 0A E5 B7 B2 E8 A7 82 E7 9C 8B 33 35 30 30 2E 39 E4 B8 87 E6 AC A1 0A 68 74 74 70 3A 2F 2F 75 72 6C 2E 63 6E 2F 35 70 45 73 4B 75 6A 0A E6 9D A5 E8 87 AA 3A 20 E5 93 94 E5 93 A9 E5 93 94 E5 93 A9
|
||||
*
|
||||
* 19 00 42 01 00 3F AA 02 3C 08 00 50 03 60 00 68 00 88 01 00 9A 01 2E 08 09 20 BF 50 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 90 04 80 0B 0E 00 0E 01 00 04 00 00 00 09 0A 00 04 00 00 00 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 10
|
||||
*/
|
||||
0x14 -> sectionData.parseMessageXML()
|
||||
|
||||
0x0E -> {
|
||||
null
|
||||
}
|
||||
|
||||
else -> {
|
||||
println("未知的messageType=0x${messageType.toByte().toUHexString()}")
|
||||
println("后文=${this.readBytes().toUHexString()}")
|
||||
null
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sectionData.release(IoBuffer.Pool)
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteReadPacket.readMessageChain(): MessageChain {
|
||||
val chain = MessageChain()
|
||||
do {
|
||||
if (this.remaining == 0L) {
|
||||
return chain
|
||||
}
|
||||
} while (this.readMessage().takeIf { it != null }?.let { chain.followedBy(it) } != null)
|
||||
return chain
|
||||
}
|
||||
|
||||
fun MessageChain.toPacket(): ByteReadPacket = buildPacket {
|
||||
this@toPacket.forEach { message ->
|
||||
writePacket(with(message) {
|
||||
when (this) {
|
||||
is XMLMessage -> buildPacket {
|
||||
writeUByte(MessageType.XML.value)
|
||||
writeShortLVPacket {
|
||||
writeByte(0x01)
|
||||
//writeUByte()
|
||||
}
|
||||
}
|
||||
|
||||
is Face -> buildPacket {
|
||||
writeUByte(MessageType.FACE.value)
|
||||
|
||||
writeShortLVPacket {
|
||||
writeShort(1)
|
||||
writeUByte(id.value)
|
||||
|
||||
writeHex("0B 00 08 00 01 00 04 52 CC F5 D0 FF")
|
||||
|
||||
writeShort(2)
|
||||
writeByte(0x14)//??
|
||||
writeUByte((id.value + 65u).toUByte())
|
||||
}
|
||||
}
|
||||
|
||||
is At -> buildPacket {
|
||||
writeUByte(MessageType.AT.value)
|
||||
|
||||
writeShortLVPacket {
|
||||
writeByte(0x01)
|
||||
writeShortLVString(message.toString()) // 这个应该是 "@群名", 手机上面会显示这个消息, 电脑会显示下面那个
|
||||
// 06 00 0D 00 01 00 00 00 08 00 76 E4 B8 DD 00 00
|
||||
writeHex("06 00 0D 00 01 00 00 00 08 00")
|
||||
writeQQ(target)
|
||||
writeZero(2)
|
||||
}
|
||||
}
|
||||
|
||||
is Image -> buildPacket {
|
||||
when (id.value.length) {
|
||||
// "{F61593B5-5B98-1798-3F47-2A91D32ED2FC}.jpg"
|
||||
// 41 ???
|
||||
41, 42 -> {
|
||||
writeUByte(MessageType.IMAGE_42.value)
|
||||
|
||||
//00 00 03 00 CB 02 00 2A 7B 46 36 31 35 39 33 42 35 2D 35 42 39 38 2D 31 37 39 38 2D 33 46 34 37 2D 32 41 39 31 44 33 32 45 44 32 46 43 7D 2E 6A 70 67
|
||||
// 04 00 04 87 E5 09 3B 05 00 04 D2 C4 C0 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 01 ED 16 00 04 00 00 02 17 18 00 04 00 00 EB 34 FF 00 5C 15 36 20 39 32 6B 41 31 43 38 37 65 35 30 39 33 62 64 32 63 34 63 30 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 46 36 31 35 39 33 42 35 2D 35 42 39 38 2D 31 37 39 38 2D 33 46 34 37 2D 32 41 39 31 44 33 32 45 44 32 46 43 7D 2E 6A 70 67 41
|
||||
/*
|
||||
* 00 00 06 00 F3 02 00 1B 28 52 49 5F 36 31 28 32 52 59 4B 59 43 40 37 59 29 58 29 39 29 42 49 2E 67 69 66 03 00 04 00 00 11 90 04 00 25 2F 35 37 34 63 34 32 34 30 2D 30 31 33 66 2D 34 35 39 38 2D 61 37 32 34 2D 30 36 65 66 35 36 39 39 30 64 30 62 14 00 04 03 00 00 00 0B 00 00 18 00 25 2F 35 37 34 63 34 32 34 30 2D 30 31 33 66 2D 34 35 39 38 2D 61 37 32 34 2D 30 36 65 66 35 36 39 39 30 64 30 62 19 00 04 00 00 00 2D 1A 00 04 00 00 00 2D FF 00 63 16 20 20 39 39 31 30 20 38 38 31 44 42 20 20 20 20 20 20 34 34 39 36 65 33 39 46 37 36 35 33 32 45 31 41 42 35 43 41 37 38 36 44 37 41 35 31 33 38 39 32 32 35 33 38 35 2E 67 69 66 66 2F 35 37 34 63 34 32 34 30 2D 30 31 33 66 2D 34 35 39 38 2D 61 37 32 34 2D 30 36 65 66 35 36 39 39 30 64 30 62 41
|
||||
* 00 00 06 00 F3 02 00 1B 46 52 25 46 60 30 59 4F 4A 5A 51 48 31 46 4A 53 4C 51 4C 4A 33 46 31 2E 6A 70 67 03 00 04 00 00 02 A2 04 00 25 2F 37 33 38 33 35 38 36 37 2D 38 64 65 31 2D 34 65 30 66 2D 61 33 36 35 2D 34 39 62 30 33 39 63 34 61 39 31 66 14 00 04 03 00 00 00 0B 00 00 18 00 25 2F 37 33 38 33 35 38 36 37 2D 38 64 65 31 2D 34 65 30 66 2D 61 33 36 35 2D 34 39 62 30 33 39 63 34 61 39 31 66 19 00 04 00 00 00 4E 1A 00 04 00 00 00 23 FF 00 63 16 20 20 39 39 31 30 20 38 38 31 43 42 20 20 20 20 20 20 20 36 37 34 65 31 46 42 34 43 32 35 45 42 34 46 45 31 32 45 34 46 33 42 42 38 31 39 31 33 37 42 44 39 39 30 39 2E 6A 70 67 66 2F 37 33 38 33 35 38 36 37 2D 38 64 65 31 2D 34 65 30 66 2D 61 33 36 35 2D 34 39 62 30 33 39 63 34 61 39 31 66 41
|
||||
*/
|
||||
|
||||
writeShortLVPacket {
|
||||
writeUByte(0x02u)
|
||||
writeShortLVString(id.value)
|
||||
writeHex("04 00 04 87 E5 09 3B 05 00 04 D2 C4 C0 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 01 ED 16 00 04 00 00 02 17 18 00 04 00 00 EB 34 FF 00 5C 15 36 20 39 32 6B 41 31 43 38 37 65 35 30 39 33 62 64 32 63 34 63 30 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")
|
||||
writeStringUtf8(id.value)
|
||||
writeUByte(0x41u)
|
||||
}
|
||||
}
|
||||
|
||||
// "/01ee6426-5ff1-4cf0-8278-e8634d2909ef"
|
||||
37 -> {
|
||||
writeUByte(MessageType.IMAGE_37.value)
|
||||
|
||||
// 00 00 06 00 F3 02
|
||||
// 00 1B 24 5B 56 54 4A 38 60 4C 5A 4E 46 7D 53 39 4F 52 36 25 45 60 42 55 53 2E 6A 70 67
|
||||
// 03 00 04 00 01 41 1B 04
|
||||
// 00 25 2F 65 61 37 30 30 61 38 33 2D 38 38 38 62 2D 34 66 37 31 2D 61 62 64 31 2D 63 33 38 64 63 62 64 31 61 65 36 31
|
||||
// 14 00 04 00 00 00 00 0B 00 00 18
|
||||
// 00 25 2F 65 61 37 30 30 61 38 33 2D 38 38 38 62 2D 34 66 37 31 2D 61 62 64 31 2D 63 33 38 64 63 62 64 31 61 65 36 31
|
||||
// 19 00 04 00 00 03 74 1A 00 04 00 00 02 BE FF 00 63 16 20 20 39 39 31 30 20 38 38 31 43 42 20 20 20 20 20
|
||||
///38 32 32 30 33 65 39 36 30 46 42 35 44 37 46 42 33 39 46 34 39 39 31 37 46 34 37 33 44 41 45 31 37 30 32 46 44 31 2E 6A 70 67 66
|
||||
// 2F 65 61 37 30 30 61 38 33 2D 38 38 38 62 2D 34 66 37 31 2D 61 62 64 31 2D 63 33 38 64 63 62 64 31 61 65 36 31 41
|
||||
|
||||
// 00 00 06 00 F3 02
|
||||
// 00 1B 7B 48 29 47 52 53 31 29 50 24 5A 42 28 4F 35 43 44 4B 45 31 35 7B 37 2E 70 6E 67
|
||||
// 03 00 04 00 00 6F 36 04
|
||||
// 00 25 2F 35 65 63 32 34 63 37 62 2D 34 30 32 39 2D 34 61 39 33 2D 62 63 66 35 2D 34 31 38 34 35 32 65 39 32 33 31 64
|
||||
// 14 00 04 00 00 00 00 0B 00 00 18
|
||||
// 00 25 2F 35 65 63 32 34 63 37 62 2D 34 30 32 39 2D 34 61 39 33 2D 62 63 66 35 2D 34 31 38 34 35 32 65 39 32 33 31 64
|
||||
// 19 00 04 00 00 04 14 1A 00 04 00 00 02 77 FF 00 63 16 20 20 39 39 31 30 20 38 38 31 41 42 20 20 20 20 20
|
||||
///32 38 34 37 30 65 35 42 37 44 45 37 36 41 34 41 44 44 31 37 43 46 39 32 39 38 33 43 46 30 41 43 45 35 42 34 33 39 2E 70 6E 67 66
|
||||
// 2F 35 65 63 32 34 63 37 62 2D 34 30 32 39 2D 34 61 39 33 2D 62 63 66 35 2D 34 31 38 34 35 32 65 39 32 33 31 64 41
|
||||
/*
|
||||
* 00 00 06 00 F3 02
|
||||
* 00 1B 46 52 25 46 60 30 59 4F 4A 5A 51 48 31 46 4A 53 4C 51 4C 4A 33 46 31 2E 6A 70 67
|
||||
* 03 00 04 00 00 02 A2 04
|
||||
* 00 25 2F 37 33 38 33 35 38 36 37 2D 38 64 65 31 2D 34 65 30 66 2D 61 33 36 35 2D 34 39 62 30 33 39 63 34 61 39 31 66
|
||||
* 14 00 04 03 00 00 00 0B 00 00 18
|
||||
* 00 25 2F 37 33 38 33 35 38 36 37 2D 38 64 65 31 2D 34 65 30 66 2D 61 33 36 35 2D 34 39 62 30 33 39 63 34 61 39 31 66
|
||||
* 19 00 04 00 00 00 4E 1A 00 04 00 00 00 23 FF 00 63 16 20 20 39 39 31 30 20 38 38 31 43 42 20 20 20 20 20 20 20
|
||||
**36 37 34 65 31 46 42 34 43 32 35 45 42 34 46 45 31 32 45 34 46 33 42 42 38 31 39 31 33 37 42 44 39 39 30 39 2E 6A 70 67 66
|
||||
* 2F 37 33 38 33 35 38 36 37 2D 38 64 65 31 2D 34 65 30 66 2D 61 33 36 35 2D 34 39 62 30 33 39 63 34 61 39 31 66 41
|
||||
*/
|
||||
writeShortLVPacket {
|
||||
writeByte(0x02)
|
||||
//"46 52 25 46 60 30 59 4F 4A 5A 51 48 31 46 4A 53 4C 51 4C 4A 33 46 31 2E 6A 70 67".hexToBytes().encodeToString()
|
||||
// writeShortLVString(filename)//图片文件名 FR%F`0YOJZQH1FJSLQLJ3F1.jpg
|
||||
writeShortLVString(id.value.substring(1..24) + ".gif")// 图片文件名. 后缀不影响. 但无后缀会导致 PC QQ 无法显示这个图片
|
||||
writeHex("03 00 04 00 00 02 A2 04")
|
||||
writeShortLVString(id.value)
|
||||
writeHex("14 00 04 03 00 00 00 0B 00 00 18")
|
||||
writeShortLVString(id.value)
|
||||
|
||||
writeHex("19 00 04 00 00 00 4E 1A 00 04 00 00 00 23 FF 00 63 16 20 20 39 39 31 30 20 38 38 31 43 42 20 20 20 20 20 20 20 ")
|
||||
|
||||
writeStringUtf8("674e")// 没有 "e" 服务器就不回复
|
||||
|
||||
writeStringUtf8(id.value.substring(1..id.value.lastIndex - 4))//这一串文件名决定手机 QQ 保存的图片. 可以随意
|
||||
writeStringUtf8(".gif")// 后缀似乎必须要有
|
||||
writeUByte(0x66u)
|
||||
|
||||
//有时候 PC QQ 看不到发这些消息, 但手机可以. 可能是 ID 过期了, 手机有缓存而电脑没有
|
||||
|
||||
/*
|
||||
* writeHex("19 00 04 00 00 00 4E 1A 00 04 00 00 00 23 FF 00 63 16 20 20 39 39 31 30 20 38 38 31 43 42 20 20 20 20 20 20 20 ")
|
||||
|
||||
writeStringUtf8(id.value.subSequence(1..id.value.lastIndex))//674e1FB4C25EB4FE12E4F3BB819137BD9909.jpg
|
||||
writeStringUtf8(".jpg")// "36 37 34 65 31 46 42 34 43 32 35 45 42 34 46 45 31 32 45 34 46 33 42 42 38 31 39 31 33 37 42 44 39 39 30 39 2E 6A 70 67
|
||||
|
||||
writeUByte(0x66u)
|
||||
* */
|
||||
|
||||
//36 37 34 65 31 46 42 34 43 32 35 45 42 34 46 45 31 32 45 34 46 33 42 42 38 31 39 31 33 37 42 44 39 39 30 39 2E 6A 70 67 66
|
||||
writeStringUtf8(id.value)
|
||||
writeUByte(0x41u)
|
||||
}
|
||||
}
|
||||
else -> error("Illegal ImageId: ${id.value}")
|
||||
}
|
||||
}
|
||||
|
||||
is PlainText -> buildPacket {
|
||||
writeUByte(MessageType.PLAIN_TEXT.value)
|
||||
|
||||
writeShortLVPacket {
|
||||
writeByte(0x01)
|
||||
writeShortLVString(stringValue)
|
||||
}
|
||||
}
|
||||
|
||||
else -> throw UnsupportedOperationException("${this::class.simpleName} is not supported. Do NOT implement Message manually. Full MessageChain=${this@toPacket}")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,203 +0,0 @@
|
||||
@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>()
|
||||
}
|
||||
}
|
@ -1,487 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.timpc.network
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.data.LoginResult
|
||||
import net.mamoe.mirai.data.OnlineStatus
|
||||
import net.mamoe.mirai.data.Packet
|
||||
import net.mamoe.mirai.event.BroadcastControllable
|
||||
import net.mamoe.mirai.event.Cancellable
|
||||
import net.mamoe.mirai.event.Subscribable
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.BotLoginSucceedEvent
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.timpc.TIMPCBot
|
||||
import net.mamoe.mirai.timpc.network.handler.DataPacketSocketAdapter
|
||||
import net.mamoe.mirai.timpc.network.handler.TemporaryPacketHandler
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.timpc.network.packet.login.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.cryptor.Decrypter
|
||||
import net.mamoe.mirai.utils.cryptor.NoDecrypter
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* 包处理协程调度器.
|
||||
*
|
||||
* JVM: 独立的 4 thread 调度器
|
||||
*/
|
||||
internal expect val NetworkDispatcher: CoroutineDispatcher
|
||||
|
||||
/**
|
||||
* [BotNetworkHandler] 的 TIM PC 协议实现
|
||||
*
|
||||
* @see BotNetworkHandler
|
||||
*/
|
||||
internal class TIMPCBotNetworkHandler internal constructor(coroutineContext: CoroutineContext, bot: TIMPCBot) :
|
||||
BotNetworkHandler(), CoroutineScope {
|
||||
override val bot: TIMPCBot by bot.unsafeWeakRef()
|
||||
override val supervisor: CompletableJob = SupervisorJob(coroutineContext[Job])
|
||||
|
||||
override val coroutineContext: CoroutineContext =
|
||||
coroutineContext + NetworkDispatcher + CoroutineExceptionHandler { context, e ->
|
||||
bot.logger.error("An exception was thrown in ${context[CoroutineName]?.let { "coroutine $it" }
|
||||
?: "an unnamed coroutine"} under TIMBotNetworkHandler", e)
|
||||
} + supervisor
|
||||
|
||||
lateinit var socket: BotSocketAdapter
|
||||
private set
|
||||
|
||||
internal val temporaryPacketHandlers = LockFreeLinkedList<TemporaryPacketHandler<*, *>>()
|
||||
|
||||
private var heartbeatJob: Job? = null
|
||||
|
||||
@MiraiInternalAPI
|
||||
override suspend fun login() {
|
||||
|
||||
TIMProtocol.SERVER_IP.shuffled().forEach { ip ->
|
||||
bot.logger.info("Connecting server $ip")
|
||||
try {
|
||||
withTimeout(3000) {
|
||||
socket = BotSocketAdapter(ip)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw LoginFailedException(LoginResult.NETWORK_UNAVAILABLE)
|
||||
}
|
||||
|
||||
loginResult = CompletableDeferred()
|
||||
|
||||
val result = socket.resendTouch() ?: return // success
|
||||
result.takeIf { it != LoginResult.TIMEOUT }?.let { throw LoginFailedException(it) }
|
||||
|
||||
bot.logger.warning("Timeout. Retrying next server")
|
||||
|
||||
socket.close()
|
||||
}
|
||||
throw LoginFailedException(LoginResult.TIMEOUT)
|
||||
}
|
||||
|
||||
internal var loginResult: CompletableDeferred<LoginResult?> = CompletableDeferred()
|
||||
|
||||
//private | internal
|
||||
|
||||
private var _sessionKey: SessionKey? = null
|
||||
internal val sessionKey: SessionKey get() = _sessionKey ?: error("sessionKey is not yet initialized")
|
||||
|
||||
override suspend fun awaitDisconnection() {
|
||||
heartbeatJob?.join()
|
||||
}
|
||||
|
||||
override fun close(cause: Throwable?) {
|
||||
super.close(cause)
|
||||
|
||||
this.heartbeatJob?.cancel(CancellationException("handler closed"))
|
||||
this.heartbeatJob = null
|
||||
|
||||
if (!this.loginResult.isCompleted && !this.loginResult.isCancelled) {
|
||||
this.loginResult.cancel(CancellationException("socket closed"))
|
||||
}
|
||||
|
||||
this.socket.close()
|
||||
}
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
internal inner class BotSocketAdapter(override val serverIp: String) :
|
||||
DataPacketSocketAdapter {
|
||||
|
||||
override val channel: PlatformDatagramChannel = PlatformDatagramChannel(serverIp, 8000)
|
||||
|
||||
override val isOpen: Boolean get() = channel.isOpen
|
||||
|
||||
private var loginHandler: LoginHandler? = null
|
||||
|
||||
private suspend inline fun processReceive() {
|
||||
while (channel.isOpen) {
|
||||
val input: ByteReadPacket = try {
|
||||
channel.read()// JVM: withContext(IO)
|
||||
} catch (e: ClosedChannelException) {
|
||||
close()
|
||||
return
|
||||
} catch (e: ReadPacketInternalException) {
|
||||
if (e.message != "java.nio.channels.AsynchronousCloseException") {
|
||||
bot.logger.error("Socket channel read failed: ${e.message}")
|
||||
}
|
||||
continue
|
||||
} catch (e: CancellationException) {
|
||||
return
|
||||
} catch (e: Throwable) {
|
||||
bot.logger.error("Caught unexpected exceptions", e)
|
||||
continue
|
||||
}
|
||||
|
||||
//buffer.resetForRead()
|
||||
launch(CoroutineName("handleServerPacket")) {
|
||||
// `.use`: Ensure that the packet is consumed **totally**
|
||||
// so that all the buffers are released
|
||||
input.use {
|
||||
try {
|
||||
input.discardExact(3)
|
||||
|
||||
val id = matchPacketId(input.readUShort())
|
||||
val sequenceId = input.readUShort()
|
||||
|
||||
input.discardExact(7)//4 for qq number, 3 for 0x00 0x00 0x00
|
||||
|
||||
val packet = try {
|
||||
with(id.factory) {
|
||||
loginHandler!!.provideDecrypter(id.factory)
|
||||
.decrypt(input, 0, (input.remaining - 1).toInt()) // tail
|
||||
.decode(id, sequenceId, this@TIMPCBotNetworkHandler)
|
||||
}
|
||||
} finally {
|
||||
input.close()
|
||||
}
|
||||
|
||||
|
||||
handlePacket0(sequenceId, packet, id.factory)
|
||||
} catch (e: Exception) {
|
||||
bot.logger.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun resendTouch(): LoginResult? /* = coroutineScope */ {
|
||||
loginHandler?.close()
|
||||
|
||||
loginHandler = LoginHandler()
|
||||
|
||||
|
||||
expectingTouchResponse = Job(supervisor)
|
||||
try {
|
||||
launch(CoroutineName("Packet Receiver")) { processReceive() }
|
||||
launch(CoroutineName("Timout checker")) {
|
||||
if (withTimeoutOrNull(bot.configuration.touchTimeoutMillis) { expectingTouchResponse!!.join() } == null) {
|
||||
loginResult.complete(LoginResult.TIMEOUT)
|
||||
}
|
||||
}
|
||||
sendPacket(TouchPacket(bot.uin, serverIp, false))
|
||||
|
||||
return loginResult.await()
|
||||
} finally {
|
||||
expectingTouchResponse = null
|
||||
}
|
||||
}
|
||||
|
||||
private var expectingTouchResponse: CompletableJob? = null
|
||||
|
||||
private suspend inline fun <TPacket : Packet> handlePacket0(
|
||||
sequenceId: UShort,
|
||||
packet: TPacket,
|
||||
factory: PacketFactory<TPacket, *>
|
||||
) {
|
||||
if (packet is TouchPacket.TouchResponse) {
|
||||
expectingTouchResponse?.complete()
|
||||
}
|
||||
|
||||
if (!packet::class.annotations.filterIsInstance<NoLog>().any()) {
|
||||
if ((packet as? BroadcastControllable)?.shouldBroadcast != false) {
|
||||
bot.logger.verbose("Packet received: ${packet.toString()
|
||||
.replace("\n\r", """\n""")
|
||||
.replace("\n", """\n""")
|
||||
.replace("\r", """\n""")}")
|
||||
}
|
||||
}
|
||||
|
||||
if (packet is Subscribable) {
|
||||
if (packet is BroadcastControllable) {
|
||||
if (packet.shouldBroadcast) packet.broadcast()
|
||||
} else {
|
||||
packet.broadcast()
|
||||
}
|
||||
|
||||
if (packet is Cancellable && packet.cancelled) return
|
||||
}
|
||||
|
||||
temporaryPacketHandlers.forEach {
|
||||
if (it.filter(packet, sequenceId) && temporaryPacketHandlers.remove(it)) {
|
||||
it.doReceivePassingExceptionsToDeferred(packet)
|
||||
}
|
||||
}
|
||||
|
||||
if (factory is SessionPacketFactory<*>) {
|
||||
with(factory as SessionPacketFactory<TPacket>) {
|
||||
handlePacket(packet)
|
||||
}
|
||||
}
|
||||
|
||||
loginHandler?.onPacketReceived(packet)
|
||||
}
|
||||
|
||||
internal suspend fun sendPacket(packet: OutgoingPacket): Unit = withContext(coroutineContext + CoroutineName("sendPacket")) {
|
||||
check(channel.isOpen) { "channel is not open" }
|
||||
|
||||
packet.delegate.use {
|
||||
try {
|
||||
check(channel.send(it) ) { "Send packet failed" }
|
||||
} catch (e: SendPacketInternalException) {
|
||||
if (e.cause !is CancellationException) {
|
||||
bot.logger.error("Caught SendPacketInternalException: ${e.cause?.message}")
|
||||
}
|
||||
delay(bot.configuration.firstReconnectDelayMillis)
|
||||
bot.tryReinitializeNetworkHandler(e)
|
||||
return@withContext
|
||||
}
|
||||
}
|
||||
|
||||
packet.takeUnless { _ ->
|
||||
packet.packetId is KnownPacketId && packet.packetId.factory.let {
|
||||
it::class.annotations.filterIsInstance<NoLog>().any()
|
||||
}
|
||||
}?.let {
|
||||
bot.logger.verbose("Packet sent: ${it.name}")
|
||||
}
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
override val owner: Bot get() = this@TIMPCBotNetworkHandler.bot
|
||||
|
||||
override fun close() {
|
||||
loginHandler?.close()
|
||||
loginHandler = null
|
||||
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: PrivateKey = PrivateKey(getRandomByteArray(16))
|
||||
|
||||
private var sessionResponseDecryptionKey: SessionResponseDecryptionKey? = null
|
||||
|
||||
private var captchaSectionId: Int = 1
|
||||
private var captchaCache: IoBuffer? = null
|
||||
//set 为 null 时自动 release; get 为 null 时自动 borrow
|
||||
get() {
|
||||
if (field == null) field = IoBuffer.Pool.borrow()
|
||||
return field
|
||||
}
|
||||
set(value) {
|
||||
if (value == null) {
|
||||
field?.release(IoBuffer.Pool)
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
|
||||
internal fun <D : Decrypter> provideDecrypter(factory: PacketFactory<*, D>): D =
|
||||
when (factory.decrypterType) {
|
||||
TouchKey -> TouchKey
|
||||
CaptchaKey -> CaptchaKey
|
||||
ShareKey -> ShareKey
|
||||
|
||||
NoDecrypter -> NoDecrypter
|
||||
|
||||
SessionResponseDecryptionKey -> sessionResponseDecryptionKey!!
|
||||
SubmitPasswordResponseDecrypter -> SubmitPasswordResponseDecrypter(privateKey)
|
||||
PrivateKey -> privateKey
|
||||
SessionKey -> sessionKey
|
||||
|
||||
else -> error("No decrypter is found")
|
||||
} as? D ?: error("Internal error: could not cast decrypter which is found for factory to class Decrypter")
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
suspend fun onPacketReceived(packet: Any) {//complex function, but it doesn't matter
|
||||
when (packet) {
|
||||
is TouchPacket.TouchResponse.OK -> {
|
||||
loginIP = packet.loginIP
|
||||
loginTime = packet.loginTime
|
||||
token0825 = packet.token0825
|
||||
|
||||
socket.sendPacket(
|
||||
SubmitPasswordPacket(
|
||||
bot = bot.uin,
|
||||
passwordMd5 = bot.account.passwordMd5,
|
||||
loginTime = loginTime,
|
||||
loginIP = loginIP,
|
||||
privateKey = privateKey,
|
||||
token0825 = token0825,
|
||||
token00BA = null,
|
||||
randomDeviceName = bot.configuration.randomDeviceName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is TouchPacket.TouchResponse.Redirection -> {
|
||||
socket.close()
|
||||
bot.logger.info("Redirecting to ${packet.serverIP}")
|
||||
socket = BotSocketAdapter(packet.serverIP)
|
||||
loginResult.complete(socket.resendTouch())
|
||||
}
|
||||
|
||||
is SubmitPasswordPacket.LoginResponse.Failed -> {
|
||||
loginResult.complete(packet.result)
|
||||
return
|
||||
}
|
||||
|
||||
is CaptchaPacket.CaptchaResponse.Correct -> {
|
||||
this.privateKey = PrivateKey(getRandomByteArray(16))//似乎是必须的
|
||||
this.token00BA = packet.token00BA
|
||||
|
||||
socket.sendPacket(
|
||||
SubmitPasswordPacket(
|
||||
bot = bot.uin,
|
||||
passwordMd5 = bot.account.passwordMd5,
|
||||
loginTime = loginTime,
|
||||
loginIP = loginIP,
|
||||
privateKey = privateKey,
|
||||
token0825 = token0825,
|
||||
token00BA = packet.token00BA,
|
||||
randomDeviceName = bot.configuration.randomDeviceName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is SubmitPasswordPacket.LoginResponse.CaptchaInit -> {
|
||||
//[token00BA]来源之一: 验证码
|
||||
this.token00BA = packet.token00BA
|
||||
this.captchaCache = packet.captchaPart1
|
||||
|
||||
this.captchaSectionId = 1
|
||||
socket.sendPacket(CaptchaPacket.RequestTransmission(bot.uin, this.token0825, this.captchaSectionId++, packet.token00BA))
|
||||
}
|
||||
|
||||
is CaptchaPacket.CaptchaResponse.Transmission -> {
|
||||
//packet is ServerCaptchaWrongPacket
|
||||
if (this.captchaSectionId == 0) {
|
||||
bot.logger.warning("验证码错误, 请重新输入")
|
||||
this.captchaSectionId = 1
|
||||
this.captchaCache = null
|
||||
}
|
||||
|
||||
this.captchaCache!!.writeFully(packet.captchaSectionN)
|
||||
this.token00BA = packet.token00BA
|
||||
|
||||
val configuration = bot.configuration
|
||||
if (packet.transmissionCompleted) {
|
||||
if (configuration.failOnCaptcha) {
|
||||
loginResult.complete(LoginResult.CAPTCHA)
|
||||
close()
|
||||
return
|
||||
}
|
||||
val code = configuration.loginSolver.onSolvePicCaptcha(bot, captchaCache!!)
|
||||
|
||||
this.captchaCache = null
|
||||
if (code == null || code.length != 4) {
|
||||
this.captchaSectionId = 1//意味着正在刷新验证码
|
||||
socket.sendPacket(CaptchaPacket.Refresh(bot.uin, token0825))
|
||||
} else {
|
||||
this.captchaSectionId = 0//意味着已经提交验证码
|
||||
socket.sendPacket(CaptchaPacket.Submit(bot.uin, token0825, code, packet.captchaToken))
|
||||
}
|
||||
} else {
|
||||
socket.sendPacket(CaptchaPacket.RequestTransmission(bot.uin, token0825, captchaSectionId++, packet.token00BA))
|
||||
}
|
||||
}
|
||||
|
||||
is SubmitPasswordPacket.LoginResponse.Success -> {
|
||||
this.sessionResponseDecryptionKey = packet.sessionResponseDecryptionKey
|
||||
socket.sendPacket(RequestSessionPacket(bot.uin, socket.serverIp, packet.token38, packet.token88, packet.encryptionKey))
|
||||
}
|
||||
|
||||
//是ClientPasswordSubmissionPacket之后服务器回复的可能之一
|
||||
is SubmitPasswordPacket.LoginResponse.KeyExchange -> {
|
||||
this.privateKey = packet.privateKeyUpdate
|
||||
|
||||
socket.sendPacket(
|
||||
SubmitPasswordPacket(
|
||||
bot = bot.uin,
|
||||
passwordMd5 = bot.account.passwordMd5,
|
||||
loginTime = loginTime,
|
||||
loginIP = loginIP,
|
||||
privateKey = privateKey,
|
||||
token0825 = token0825,
|
||||
token00BA = packet.tokenUnknown ?: token00BA,
|
||||
randomDeviceName = bot.configuration.randomDeviceName,
|
||||
tlv0006 = packet.tlv0006
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is RequestSessionPacket.SessionKeyResponse -> {
|
||||
_sessionKey = packet.sessionKey
|
||||
bot.logger.info("sessionKey = ${packet.sessionKey.value.toUHexString()}")
|
||||
|
||||
setOnlineStatus(OnlineStatus.ONLINE)//required
|
||||
}
|
||||
|
||||
is ChangeOnlineStatusPacket.ChangeOnlineStatusResponse -> {
|
||||
BotLoginSucceedEvent(bot).broadcast()
|
||||
|
||||
val configuration = bot.configuration
|
||||
heartbeatJob = this@TIMPCBotNetworkHandler.launch(CoroutineName("Heartbeat Job")) {
|
||||
while (socket.isOpen) {
|
||||
delay(configuration.heartbeatPeriodMillis)
|
||||
with(bot) {
|
||||
class HeartbeatTimeoutException : CancellationException("heartbeat timeout")
|
||||
|
||||
if (withTimeoutOrNull(configuration.heartbeatTimeoutMillis) {
|
||||
// FIXME: 2019/11/26 启动被挤掉线检测
|
||||
|
||||
HeartbeatPacket(bot.uin, sessionKey).sendAndExpect<HeartbeatPacketResponse>()
|
||||
} == null) {
|
||||
|
||||
// retry one time
|
||||
if (withTimeoutOrNull(configuration.heartbeatTimeoutMillis) {
|
||||
HeartbeatPacket(bot.uin, sessionKey).sendAndExpect<HeartbeatPacketResponse>()
|
||||
} == null) {
|
||||
bot.logger.warning("Heartbeat timed out")
|
||||
|
||||
delay(configuration.firstReconnectDelayMillis)
|
||||
bot.tryReinitializeNetworkHandler(HeartbeatTimeoutException())
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bot.logger.info("Successfully logged in")
|
||||
loginResult.complete(null)
|
||||
this.close()//The LoginHandler is useless since then
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
suspend fun setOnlineStatus(status: OnlineStatus) {
|
||||
socket.sendPacket(ChangeOnlineStatusPacket(bot.uin, sessionKey, status))
|
||||
}
|
||||
|
||||
fun close() {
|
||||
this.captchaCache = null
|
||||
}
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
@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
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
@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()
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
@ -1,174 +0,0 @@
|
||||
@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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
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
|
||||
)
|
@ -1,27 +0,0 @@
|
||||
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"
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
@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)
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
@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
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
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>
|
||||
}
|
@ -1,282 +0,0 @@
|
||||
@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
|
||||
}
|
||||
}
|
@ -1,311 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import kotlinx.io.charsets.Charsets
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.data.EventPacket
|
||||
import net.mamoe.mirai.data.ImageLink
|
||||
import net.mamoe.mirai.message.data.ImageId
|
||||
import net.mamoe.mirai.message.data.ImageId0x06
|
||||
import net.mamoe.mirai.message.data.requireLength
|
||||
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.ExternalImage
|
||||
import net.mamoe.mirai.utils.PacketVersion
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
|
||||
|
||||
// region FriendImageResponse
|
||||
|
||||
internal interface FriendImageResponse : EventPacket
|
||||
|
||||
/**
|
||||
* 图片数据地址.
|
||||
*/
|
||||
// TODO: 2019/11/15 应该为 inline class, 但 kotlin 有 bug
|
||||
internal data class FriendImageLink(override inline val original: String) : FriendImageResponse, ImageLink {
|
||||
override fun toString(): String = "FriendImageLink($original)"
|
||||
}
|
||||
|
||||
/**
|
||||
* 访问 HTTP API 时使用的 uKey
|
||||
*/
|
||||
internal class FriendImageUKey(inline val imageId: ImageId, inline val uKey: ByteArray) : FriendImageResponse {
|
||||
override fun toString(): String = "FriendImageUKey(imageId=${imageId.value}, uKey=${uKey.toUHexString()})"
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片 ID 已存在
|
||||
* 发送消息时使用的 id
|
||||
*/
|
||||
internal inline class FriendImageAlreadyExists(inline val imageId: ImageId) : FriendImageResponse {
|
||||
override fun toString(): String = "FriendImageAlreadyExists(imageId=${imageId.value})"
|
||||
}
|
||||
|
||||
/**
|
||||
* 超过文件大小上限
|
||||
*/
|
||||
internal object FriendImageOverFileSizeMax : FriendImageResponse {
|
||||
override fun toString(): String = "FriendImageOverFileSizeMax"
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* 请求上传图片. 将发送图片的 md5, size, width, height.
|
||||
* 服务器返回以下之一:
|
||||
* - 服务器已经存有这个图片
|
||||
* - 服务器未存有, 返回一个 key 用于客户端上传
|
||||
*/
|
||||
@PacketVersion(date = "2019.11.16", timVersion = "2.3.2 (21173)")
|
||||
internal object FriendImagePacket : SessionPacketFactory<FriendImageResponse>() {
|
||||
@Suppress("FunctionName")
|
||||
fun RequestImageId(
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
target: Long,
|
||||
image: ExternalImage
|
||||
): OutgoingPacket = buildSessionPacket(
|
||||
bot,
|
||||
sessionKey,
|
||||
version = TIMProtocol.version0x04,
|
||||
name = "FriendImagePacket.RequestPacketId"
|
||||
) {
|
||||
writeHex("00 00 00 07 00 00")
|
||||
|
||||
|
||||
// TODO: 2019/11/22 should be ProtoBuf
|
||||
|
||||
writeShortLVPacket(lengthOffset = { it - 7 }) {
|
||||
writeUByte(0x08u)
|
||||
writeTV(0x01_12u)
|
||||
writeTV(0x03_98u)
|
||||
writeTV(0x01_01u)
|
||||
writeTV(0x08_01u)
|
||||
|
||||
writeUVarIntLVPacket(tag = 0x12u, lengthOffset = { it + 1 }) {
|
||||
writeTUVarint(0x08u, bot.toUInt())
|
||||
writeTUVarint(0x10u, target.toUInt())
|
||||
writeTV(0x18_00u)
|
||||
writeTLV(0x22u, image.md5)
|
||||
writeTUVarint(0x28u, image.inputSize.toUInt())
|
||||
writeUVarIntLVPacket(tag = 0x32u) {
|
||||
writeTV(0x28_00u)
|
||||
writeTV(0x46_00u)
|
||||
writeTV(0x51_00u)
|
||||
writeTV(0x56_00u)
|
||||
writeTV(0x4B_00u)
|
||||
writeTV(0x41_00u)
|
||||
writeTV(0x49_00u)
|
||||
writeTV(0x25_00u)
|
||||
writeTV(0x4B_00u)
|
||||
writeTV(0x24_00u)
|
||||
writeTV(0x55_00u)
|
||||
writeTV(0x30_00u)
|
||||
writeTV(0x24_00u)
|
||||
}
|
||||
writeTV(0x38_01u)
|
||||
writeTV(0x48_00u)
|
||||
writeTUVarint(0x70u, image.width.toUInt())
|
||||
writeTUVarint(0x78u, image.height.toUInt())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun RequestImageLink(
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
imageId: ImageId
|
||||
): OutgoingPacket {
|
||||
imageId.requireLength()
|
||||
require(imageId.value.length == 37) { "ImageId.value.length must == 37 but given length=${imageId.value.length} value=${imageId.value}" }
|
||||
|
||||
// 00 00 00 07 00 00 00
|
||||
// [4B]
|
||||
// 08
|
||||
// 01 12
|
||||
// 03 98
|
||||
// 01 02
|
||||
// 08 02
|
||||
//
|
||||
// 1A [47]
|
||||
// 08 [A2 FF 8C F0 03] UVarInt
|
||||
// 10 [DD F1 92 B7 07] UVarInt
|
||||
// 1A [25] 2F 38 65 32 63 32 38 62 64 2D 35 38 61 31 2D 34 66 37 30 2D 38 39 61 31 2D 65 37 31 39 66 63 33 30 37 65 65 66
|
||||
// 20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01
|
||||
|
||||
|
||||
// 00 00 00 07 00 00 00
|
||||
// [4B]
|
||||
// 08
|
||||
// 01 12
|
||||
// 03 98
|
||||
// 01 02
|
||||
// 08 02
|
||||
//
|
||||
// 1A
|
||||
// [47]
|
||||
// 08 [A2 FF 8C F0 03]
|
||||
// 10 [A6 A7 F1 EA 02]
|
||||
// 1A [25] 2F 39 61 31 66 37 31 36 32 2D 38 37 30 38 2D 34 39 30 38 2D 38 31 63 30 2D 66 34 63 64 66 33 35 63 38 64 37 65
|
||||
// 20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01
|
||||
|
||||
// TODO: 2019/11/22 should be ProtoBuf
|
||||
|
||||
return buildSessionPacket(
|
||||
bot,
|
||||
sessionKey,
|
||||
version = TIMProtocol.version0x04,
|
||||
name = "FriendImagePacket.RequestImageLink"
|
||||
) {
|
||||
writeHex("00 00 00 07 00 00")
|
||||
|
||||
writeUShort(0x004Bu)
|
||||
|
||||
writeUByte(0x08u)
|
||||
writeTV(0x01_12u)
|
||||
writeTV(0x03_98u)
|
||||
writeTV(0x01_02u)
|
||||
writeTV(0x08_02u)
|
||||
|
||||
writeUByte(0x1Au)
|
||||
writeUByte(0x47u)
|
||||
writeTUVarint(0x08u, bot.toUInt())
|
||||
writeTUVarint(0x10u, bot.toUInt()) // 这里实际上应该是这张图片来自哪个 QQ 号. 但传 bot 也没事.
|
||||
writeTLV(0x1Au, imageId.value.toByteArray(Charsets.ISO_8859_1))
|
||||
writeHex("20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(
|
||||
id: PacketId,
|
||||
sequenceId: UShort,
|
||||
handler: BotNetworkHandler
|
||||
): FriendImageResponse {
|
||||
|
||||
// 上传图片, 成功获取ID
|
||||
//00 00 00 08 00 00
|
||||
// [01 0D]
|
||||
// 12 06
|
||||
// 98 01 01 A0 01 00
|
||||
// 08 01 //packet type 01=上传图片; 02=下载图片
|
||||
// 12 [86 02]
|
||||
// 08 00
|
||||
// 10 [9B A4 D4 9A 0A]
|
||||
// 18 00
|
||||
// 28 00
|
||||
// 38 F1 C0 A1 BF 05
|
||||
// 38 BB C8 E4 E2 0F
|
||||
// 38 FB AE FA 9D 0A
|
||||
// 38 E5 C6 8B CD 06
|
||||
// 40 BB 03 // ports
|
||||
// 40 90 3F
|
||||
// 40 50
|
||||
// 40 BB 03
|
||||
// 4A [80 01] 76 B2 58 23 B8 F6 B1 E6 AE D4 76 EC 3C 08 79 B1 DF 05 D5 C2 4A E0 CC F1 2F 26 4F D4 DC 44 5A 9A 16 A9 E4 22 EB 92 96 05 C3 C9 8F C5 5F 84 00 A3 4E 63 BE 76 F7 B9 7B 09 43 A6 14 EE C8 6D 6A 48 02 E3 9D 62 CD 42 3E 15 93 64 8F FC F5 88 50 74 6A 6A 03 C9 FE F0 96 EA 76 02 DC 4F 09 D0 F5 60 73 B2 62 8F 8B 11 06 BF 06 1B 18 00 FE B4 5E F3 12 72 F2 66 9C F5 01 97 1C 0A 5B 68 5B 85 ED 9C
|
||||
// 52 [25] 2F 37 38 62 36 34 64 63 32 2D 31 66 32 31 2D 34 33 62 38 2D 39 32 62 31 2D 61 30 35 30 35 30 34 30 35 66 65 32
|
||||
// 5A [25] 2F 37 38 62 36 34 64 63 32 2D 31 66 32 31 2D 34 33 62 38 2D 39 32 62 31 2D 61 30 35 30 35 30 34 30 35 66 65 32
|
||||
// 60 00 68 80 80 08
|
||||
// 20 01
|
||||
|
||||
// 上传图片, 图片过大
|
||||
//00 00 00 09 00 00
|
||||
// [00 1D]
|
||||
// 12 [07] 98 01 01 A0 01 C7 01
|
||||
// 08 01
|
||||
// 12 19 08 00 18 C7 01 22 12 66 69 6C 65 20 73 69 7A 65 20 6F 76 65 72 20 6D 61 78
|
||||
discardExact(3) // 00 00 00
|
||||
if (readUByte().toUInt() == 0x09u) {
|
||||
return FriendImageOverFileSizeMax
|
||||
}
|
||||
discardExact(2) //00 00
|
||||
|
||||
discardExact(2) //全长 (有 offset)
|
||||
|
||||
discardExact(1); discardExact(readUVarInt().toInt()) // 12 [06] 98 01 01 A0 01 00
|
||||
// TODO: 2019/11/22 should be ProtoBuf
|
||||
|
||||
check(readUByte().toUInt() == 0x08u)
|
||||
return when (val flag = readUByte().toUInt()) {
|
||||
0x01u -> {
|
||||
//00 00 00 08 00 00
|
||||
// [00 83]
|
||||
// 12 06
|
||||
// 98 01 01
|
||||
// A0 01 00 08 01 12 7D
|
||||
// 08 00
|
||||
// 10 9B A4 D4 9A 0A
|
||||
// 18 00
|
||||
// 28 01
|
||||
// 32 1B
|
||||
// 0A [10] 81 9B B9 33 52 BD CE 88 A9 BA 3B 1C A4 A8 8B EF
|
||||
// 10 00
|
||||
// 18 8E 4B
|
||||
// 20 40
|
||||
// 28 40
|
||||
// 52 25 2F 63 36 62 38 37 61 39 63 2D 37 30 64 36 2D 34 61 38 38 2D 61 39 33 36 2D 36 34 31 33 65 37 39 62 33 66 64 34
|
||||
// 5A 25 2F 63 36 62 38 37 61 39 63 2D 37 30 64 36 2D 34 61 38 38 2D 61 39 33 36 2D 36 34 31 33 65 37 39 62 33 66 64 34
|
||||
// 60 00
|
||||
// 68 80 80 08
|
||||
// 20 01
|
||||
|
||||
try {
|
||||
while (readUByte().toUInt() != 0x4Au) readUVarLong()
|
||||
val uKey = readBytes(readUVarInt().toInt())//128
|
||||
while (readUByte().toUInt() != 0x52u) readUVarLong()
|
||||
val imageId = ImageId0x06(readString(readUVarInt().toInt()))//37
|
||||
return FriendImageUKey(imageId, uKey)
|
||||
} catch (e: EOFException) {
|
||||
val toDiscard = readUByte().toInt() - 37
|
||||
|
||||
return if (toDiscard < 0) {
|
||||
FriendImageOverFileSizeMax
|
||||
} else {
|
||||
discardExact(toDiscard)
|
||||
val imageId = ImageId0x06(readString(37))
|
||||
FriendImageAlreadyExists(imageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
0x02u -> {
|
||||
//00 00 00 08 00 00
|
||||
// [02 2B]
|
||||
// 12 [06] 98 01 02 A0 01 00
|
||||
// 08 02
|
||||
// 1A [A6 04]
|
||||
// 0A [25] 2F 38 65 32 63 32 38 62 64 2D 35 38 61 31 2D 34 66 37 30 2D 38 39 61 31 2D 65 37 31 39 66 63 33 30 37 65 65 66
|
||||
// 18 00
|
||||
// 32 [7B] 68 74 74 70 3A 2F 2F 36 31 2E 31 35 31 2E 32 33 34 2E 35 34 3A 38 30 2F 6F 66 66 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 2F 38 65 32 63 32 38 62 64 2D 35 38 61 31 2D 34 66 37 30 2D 38 39 61 31 2D 65 37 31 39 66 63 33 30 37 65 65 66 2F 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33 32 7C 68 74 74 70 3A 2F 2F 31 30 31 2E 32 32 37 2E 31 33 31 2E 36 37 3A 38 30 2F 6F 66 66 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 2F 38 65 32 63 32 38 62 64 2D 35 38 61 31 2D 34 66 37 30 2D 38 39 61 31 2D 65 37 31 39 66 63 33 30 37 65 65 66 2F 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33 32 7D 68 74 74 70 3A 2F 2F 31 35 37 2E 32 35 35 2E 31 39 32 2E 31 30 35 3A 38 30 2F 6F 66 66 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 2F 38 65 32 63 32 38 62 64 2D 35 38 61 31 2D 34 66 37 30 2D 38 39 61 31 2D 65 37 31 39 66 63 33 30 37 65 65 66 2F 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33 32 7C 68 74 74 70 3A 2F 2F 31 32 30 2E 32 34 31 2E 31 39 30 2E 34 31 3A 38 30 2F 6F 66 66 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 2F 38 65 32 63 32 38 62 64 2D 35 38 61 31 2D 34 66 37 30 2D 38 39 61 31 2D 65 37 31 39 66 63 33 30 37 65 65 66 2F 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33
|
||||
// 3A 00 80 01 00
|
||||
|
||||
|
||||
//00 00 00 08 00 00
|
||||
// [02 29]
|
||||
// 12 [06] 98 01 02 A0 01 00
|
||||
// 08 02
|
||||
// 1A [A4 04]
|
||||
// 0A [25] 2F 62 61 65 30 63 64 66 66 2D 65 33 34 30 2D 34 38 39 34 2D 39 37 36 65 2D 30 66 62 35 38 61 61 31 36 35 66 64
|
||||
// 18 00
|
||||
// 32 [7A] 68 74 74 70 3A 2F 2F 31 30 31 2E 38 39 2E 33 39 2E 32 31 3A 38 30 2F 6F 66 66 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 2F 62 61 65 30 63 64 66 66 2D 65 33 34 30 2D 34 38 39 34 2D 39 37 36 65 2D 30 66 62 35 38 61 61 31 36 35 66 64 2F 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33
|
||||
// 32 7B 68 74 74 70 3A 2F 2F 36 31 2E 31 35 31 2E 31 38 33 2E 32 31 3A 38 30 2F 6F 66 66 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 2F 62 61 65 30 63 64 66 66 2D 65 33 34 30 2D 34 38 39 34 2D 39 37 36 65 2D 30 66 62 35 38 61 61 31 36 35 66 64 2F 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33 32 7D 68 74 74 70 3A 2F 2F 31 35 37 2E 32 35 35 2E 31 39 32 2E 31 30 35 3A 38 30 2F 6F 66 66 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 2F 62 61 65 30 63 64 66 66 2D 65 33 34 30 2D 34 38 39 34 2D 39 37 36 65 2D 30 66 62 35 38 61 61 31 36 35 66 64 2F 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33 32 7C 68 74 74 70 3A 2F 2F 31 32 30 2E 32 34 31 2E 31 39 30 2E 34 31 3A 38 30 2F 6F 66 66 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 2F 62 61 65 30 63 64 66 66 2D 65 33 34 30 2D 34 38 39 34 2D 39 37 36 65 2D 30 66 62 35 38 61 61 31 36 35 66 64 2F 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33 3A 00 80 01 00
|
||||
|
||||
discardExact(1)
|
||||
discardExact(2)// [A4 04] 后文长度
|
||||
check(readUByte().toUInt() == 0x0Au) { "Illegal identity. Required 0x0Au" }
|
||||
/* val imageId = */ImageId0x06(readString(readUByte().toInt()))
|
||||
|
||||
check(readUByte().toUInt() == 0x18u) { "Illegal identity. Required 0x18u" }
|
||||
check(readUShort().toUInt() == 0x0032u) { "Illegal identity. Required 0x0032u" }
|
||||
|
||||
val link = readUVarIntLVString()
|
||||
discard()
|
||||
FriendImageLink(link)
|
||||
}
|
||||
else -> error("Unknown FriendImageIdRequestPacket flag $flag")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
@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
|
||||
|
||||
|
@ -1,42 +0,0 @@
|
||||
@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
|
||||
}
|
@ -1,221 +0,0 @@
|
||||
@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()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,426 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.contact.GroupInternalId
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.contact.groupInternalId
|
||||
import net.mamoe.mirai.data.Packet
|
||||
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.timpc.internal.RawGroupInfo
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.timpc.utils.unsupportedFlag
|
||||
import net.mamoe.mirai.timpc.utils.unsupportedType
|
||||
import net.mamoe.mirai.utils.NoLog
|
||||
import net.mamoe.mirai.utils.PacketVersion
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import kotlin.collections.set
|
||||
|
||||
|
||||
internal object GroupNotFound : GroupPacket.InfoResponse {
|
||||
override fun toString(): String = "GroupPacket.InfoResponse.GroupNotFound"
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
internal object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketResponse>() {
|
||||
@PacketVersion(date = "2019.10.19", timVersion = "2.3.2 (21173)")
|
||||
fun Message(
|
||||
bot: Long,
|
||||
groupInternalId: GroupInternalId,
|
||||
sessionKey: SessionKey,
|
||||
message: MessageChain
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey, name = "GroupPacket.GroupMessage") {
|
||||
writeUByte(0x2Au)
|
||||
writeGroup(groupInternalId)
|
||||
|
||||
writeShortLVPacket {
|
||||
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")
|
||||
writeFully(TIMProtocol.messageConst1)
|
||||
writeZero(2)
|
||||
|
||||
writePacket(message.toPacket())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出群
|
||||
*/
|
||||
@PacketVersion(date = "2019.11.28", timVersion = "2.3.2 (21173)")
|
||||
fun QuitGroup(
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
group: GroupInternalId
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey, name = "GroupPacket.QuitGroup") {
|
||||
writeUByte(0x09u)
|
||||
writeGroup(group)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询群信息
|
||||
*/
|
||||
@PacketVersion(date = "2019.11.27", timVersion = "2.3.2 (21173)")
|
||||
fun QueryGroupInfo(
|
||||
bot: Long,
|
||||
groupInternalId: GroupInternalId,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey, name = "GroupPacket.QueryGroupInfo", headerSizeHint = 9) {
|
||||
writeUByte(0x72u)
|
||||
writeGroup(groupInternalId)
|
||||
writeZero(4)
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁言群成员
|
||||
*/
|
||||
@PacketVersion(date = "2019.12.2", timVersion = "2.3.2 (21173)")
|
||||
fun Mute(
|
||||
bot: Long,
|
||||
groupInternalId: GroupInternalId,
|
||||
sessionKey: SessionKey,
|
||||
target: Long,
|
||||
/**
|
||||
* 0 为取消
|
||||
*/
|
||||
timeSeconds: UInt
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey, name = "GroupPacket.Mute") {
|
||||
writeUByte(0x7Eu)
|
||||
writeGroup(groupInternalId)
|
||||
writeByte(0x20)
|
||||
writeByte(0x00)
|
||||
writeByte(0x01)
|
||||
writeQQ(target)
|
||||
writeUInt(timeSeconds)
|
||||
}
|
||||
|
||||
internal interface GroupPacketResponse : Packet
|
||||
|
||||
//@NoLog
|
||||
internal object MessageResponse : Packet, GroupPacketResponse {
|
||||
override fun toString(): String = "GroupPacket.MessageResponse"
|
||||
}
|
||||
|
||||
@NoLog
|
||||
internal object MuteResponse : Packet, GroupPacketResponse {
|
||||
override fun toString(): String = "GroupPacket.MuteResponse"
|
||||
}
|
||||
|
||||
internal interface InfoResponse : Packet, GroupPacketResponse
|
||||
|
||||
/**
|
||||
* 退出群的返回
|
||||
*/
|
||||
class QuitGroupResponse(private val _group: GroupInternalId?) : Packet, GroupPacketResponse {
|
||||
val group: GroupInternalId get() = _group ?: error("request failed")
|
||||
val isSuccess: Boolean get() = _group != null
|
||||
|
||||
override fun toString(): String = "GroupPacket.QuitResponse"
|
||||
}
|
||||
|
||||
@PacketVersion(date = "2019.11.27", timVersion = "2.3.2 (21173)")
|
||||
@UseExperimental(ExperimentalStdlibApi::class)
|
||||
override suspend fun ByteReadPacket.decode(
|
||||
id: PacketId,
|
||||
sequenceId: UShort,
|
||||
handler: BotNetworkHandler
|
||||
): GroupPacketResponse {
|
||||
return when (val packetType = readUByte().toUInt()) {
|
||||
0x2Au -> MessageResponse
|
||||
0x7Eu -> MuteResponse // 成功: 7E 00 22 96 29 7B;
|
||||
|
||||
0x09u -> {
|
||||
if (readByte().toInt() == 0) {
|
||||
QuitGroupResponse(readUInt().toLong().groupInternalId())
|
||||
} else {
|
||||
QuitGroupResponse(null)
|
||||
}
|
||||
}
|
||||
|
||||
0x72u -> {
|
||||
when (val flag = readByte().toInt()) {
|
||||
0x02 -> GroupNotFound
|
||||
0x00 -> {
|
||||
/*
|
||||
27 0B 60 E7
|
||||
27 0B 60 E7
|
||||
00 00 00 03 01 01 00 04 01
|
||||
40 23 00 40
|
||||
3E 03 3F A2 群主
|
||||
|
||||
01 00
|
||||
00 00 00 00
|
||||
00 00 00 27
|
||||
19 01 F4 01
|
||||
00 00 00 01 00 00
|
||||
00 2B
|
||||
|
||||
05 4D 69 72 61 69 群名
|
||||
00
|
||||
00
|
||||
00
|
||||
00
|
||||
00
|
||||
38 96 A2 13 CE 50 65 AD E4 2C FB 26 6A 4C D5 0D F0 B4 79 0B A1 8A B8 48 17 B1 7D BD A6 27 AF BD E8 EF E2 C5 40 AA A7 9C C0 1E 65 9F 54 6D 0F ED 9B 30 B1 03 97 F0 46 2A 46
|
||||
00
|
||||
0F 00 00 00 00 06 00 03 00 02 00 00 00 04 00 04
|
||||
00 00 00 01 00 05 00 04 5D F5 37 65 00 06 00 04 04 08 00 00 00 07 00 04 00 00 00 00 00 09 00 01 00
|
||||
76 E4 B8 DD 00
|
||||
38 B5 21 5D 00 00
|
||||
3B E7 BB BC 00 00
|
||||
3E 03 3F A2 00 00
|
||||
76 E4 B8 DD 00 00
|
||||
*/
|
||||
discardExact(4) // group internal id
|
||||
val group = readUInt().toLong() // group id
|
||||
|
||||
discardExact(13) //00 00 00 03 01 01 00 04 01 00 80 01 40
|
||||
val owner = readUInt().toLong()
|
||||
discardExact(22)
|
||||
val groupName = readUByteLVString()
|
||||
|
||||
/*
|
||||
来自群2的完整数据, 需要回答问题然后经管理员审核
|
||||
00
|
||||
00
|
||||
95 E7 AC AC 4D 2B 31 E6 AC A1 E5 85 A8 E7 BE A4 E4 B8 AD E5 B0 8F E5 92 B8 E9 B1 BC E8 81 9A E4 BC 97 E5 88 92 E6 B0 B4 EF BC 9B 0A E5 AE 9A E4 BA 8E E3 80 90 31 2E 31 31 EF BC 88 E5 91 A8 E5 85 AD EF BC 89 E4 B8 80 E6 A0 A1 E5 8C BA E3 80 91 E5 91 A8 E8 BE B9 E4 B8 BE E8 A1 8C EF BC 9B 0A E4 B8 AD E5 8D 88 31 EF BC 9A 30 30 E4 BA 8E E8 A5 BF E8 8B 91 E5 AE BE E9 A6 86 E9 9B 86 E5 90 88 EF BC 8C E5 90 9B E4 B8 B4 E5 9F 8E 2B E9 9F A9 E7 9B 9B 2F
|
||||
00
|
||||
00
|
||||
38 86 35 BF ED DD 19 4A 1B FD C8 8C 18 89 6C 78 3D A7 F3 A3 47 0D 53 C0 81 B8 D5 D0 42 21 12 24 D1 43 88 79 BA 6A 69 A8 48 0D 2D DF C8 C5 B7 EC 30 D8 4D 65 DE FB 43 A0 77
|
||||
00
|
||||
0F
|
||||
00 00 00 00
|
||||
|
||||
07
|
||||
00
|
||||
01
|
||||
00
|
||||
1C 42 58 32 31 E6 98 AF E5 93 AA E4 B8 AA E6 A0 A1 E5 8C BA E5 93 AA E6 A0 8B E6 A5 BC
|
||||
|
||||
00
|
||||
03 00 02 00 03 00
|
||||
04 00 04 00 00 00 06 00 05 00
|
||||
04 58 B5 48 78
|
||||
00 06 00 04 00 18 00 10 00 07 00 04 00 04 20 00 00 09 00 01 00
|
||||
|
||||
46 70 19 A0 01
|
||||
06 20 98 58 00 00 08 1F 88 5C 00 00 10 0F 94 C5 00 00 11 3C B8 8C 00 00 11 4D 47 6B 00 00 11 AA 9B 45 00 00 13 8B 67 2F 00 00 14 24 5B 7D 00 00 14 9C 62 B9 00 00 14 F4 28 2A 00 00 15 0A 6F 5E 00 00 15 A5 8D 0C 00 00 17 B5 89 32 00 00 19 4E 07 87 00 00 1A 92 53 3C 00 00 1A CA 57 D1 00 00 1B 58 72 29 00 00 21 F8 67 A1 00 01 23 53 B0 8E 00 00 23 B5 55 61 00 00 23 B8 27 65 00 00 23 E8 5F 65 00 00 24 C6 B4 9B 00 00 25 2D A1 41 00 00 25 8E E2 CF 00 00 26 8B CB 82 00 00 2B 2A 0A B5 00 00 2B 2C 15 29 00 00 2C 0B 4B F3 00 00 2C DD 05 DC 00 00 2D BB 44 D6 00 01 2E EA 62 3E 00 00 2F 51 2C 3F 00 00 30 20 D3 5B 00 00 30 D6 0C 2E 00 00 31 B9 3E 72 00 00 31 FD E1 E8 00 00 32 70 75 0C 00 00 33 17 C2 62 00 00 33 73 74 62 00 00 35 58 8C 77 00 00 36 43 7E 2B 00 00 36 A5 8D AC 00 01 36 CF 1A 56 00 00 37 E6 20 8A 00 00 38 3B 42 07 00 01 38 42 CB 8F 00 00 39 0C 01 C9 00 00 39 53 A1 5A 00 00 39 79 9E CF 00 00 3A 17 AF 4A 00 00 3A 24 E6 6F 00 00 3A 83 8E A4 00 00 3A 91 3D D8 00 00 3A BF 77 3A 00 00 3A D4 C6 93 00 00 3B DA B0 79 00 00 3B DA BC 23 00 00 3C C0 C9 23 00 00 3D 3A 9C 64 00 00 3E 03 3F A2 00 00 3E 7A 07 E4 00 00 3F C5 CD 13 00 00 40 A8 6D F7 00 00 41 2A A7 B1 00 00 43 33 F3 F0 00 00 43 A1 51 93 00 00 43 C4 D3 8D 00 00 44 00 F8 A6 00 00 44 05 64 4F 00 00 44 7A A8 1D 00 00 45 0D B4 0D 00 01 46 07 29 CD 00 00 46 70 19 A0 00 00
|
||||
*/
|
||||
|
||||
/*
|
||||
来自群1的从现在这个位置的数据, 直接加入群
|
||||
00
|
||||
0F
|
||||
00 00 00 00
|
||||
|
||||
06
|
||||
|
||||
00
|
||||
03 00 02 00 00 00
|
||||
04 00 04 00 00 00 01 00 05 00
|
||||
04 5D F5 37 65
|
||||
00 06 00 04 04 08 00 00 00 07 00 04 00 00 00 00 00 09 00 01 00
|
||||
|
||||
76 E4 B8 DD 00
|
||||
38 B5 21 5D 00 00
|
||||
3B E7 BB BC 00 00
|
||||
3E 03 3F A2 00 00
|
||||
76 E4 B8 DD 00 00
|
||||
*/
|
||||
|
||||
discardExact(readUByte()) // 00
|
||||
discardExact(readUByte()) // 00
|
||||
val announcement = readUByteLVString()
|
||||
discardExact(readUByte()) // 00
|
||||
discardExact(readUByte()) // 00
|
||||
discardExact(readUByte()) // 38 ... 未知
|
||||
|
||||
discardExact(2 + 4)
|
||||
|
||||
|
||||
// 验证类型,
|
||||
when (val verifyType = readByte().toInt()) {
|
||||
6 -> { // 允许任何人
|
||||
}
|
||||
|
||||
7 -> { // 需要回答问题?
|
||||
discardExact(3) // 00 01 00, 需要提交给管理员审核
|
||||
readUByteLVString() // 验证问题
|
||||
}
|
||||
|
||||
else -> {
|
||||
DebugLogger.error("Cannot parse GroupPacket.QueryGroupInfo. unknown verifyType=$verifyType. Still trying to parse...")
|
||||
discardExact(3)
|
||||
readUByteLVString() // 验证问题
|
||||
}
|
||||
}
|
||||
discardExact(43)
|
||||
|
||||
val stop = readUInt().toLong() // 标记读取群成员的结束
|
||||
discardExact(1) // 00
|
||||
val members = mutableMapOf<Long, MemberPermission>()
|
||||
do {
|
||||
val qq = readUInt().toLong()
|
||||
val status = readUShort().toInt() // 这个群成员的状态, 最后一 bit 为管理员权限. 这里面还包含其他状态
|
||||
if (qq == owner) {
|
||||
continue
|
||||
}
|
||||
|
||||
val permission = when (status.takeLowestOneBit()) {
|
||||
1 -> MemberPermission.ADMINISTRATOR
|
||||
else -> MemberPermission.MEMBER
|
||||
}
|
||||
members[qq] = permission
|
||||
} while (qq != stop && remaining != 0L)
|
||||
members[owner] = MemberPermission.OWNER
|
||||
return RawGroupInfo(group, owner, groupName, announcement, members)
|
||||
/*
|
||||
* 群 Mirai
|
||||
*
|
||||
* 00 00 00 03 01 41 00 04 01
|
||||
* 40 23 04 40
|
||||
* B1 89 BE 09 群主
|
||||
*
|
||||
* 02 00 00 00 00 00 00 00 00 00 21 00 C8 01
|
||||
* 00 00 00 01 00 00
|
||||
* 00 2D
|
||||
*
|
||||
* 06 4D 69 72 61 69 20 群名
|
||||
* 00
|
||||
* 00
|
||||
* 00
|
||||
* 00
|
||||
* 00
|
||||
* 38 87 5F D8 E8 D4 E9 79 73 8A A4 21 1C 3E 2C 43 D0 23 55 53 49 D3 1C DB F6 1F 84 59 77 66 DA 9C D7 26 0F E3 BD E1 F2 B9 29 D1 F6 97 1C 42 5E B0 AF 09 51 72 DA 03 37 AB 65
|
||||
* 00
|
||||
* 0A 00 00 00 00 06 00 03 00 02 00
|
||||
* 01 00 04 00 04 00 00 00 01 00 05 00 04 5D 90 A7 25 00 06 00 04 04 08 00 00 00 07 00 04 00 00 05 80 00 09 00 01 01
|
||||
* B1 89 BE 09 00
|
||||
* 3E 03 3F A2 00 01
|
||||
* 48 76 54 DC 00 00
|
||||
* 76 E4 B8 DD 00 00
|
||||
* 89 1A 5E AC 00 00
|
||||
* B1 89 BE 09 00 00
|
||||
*/
|
||||
|
||||
/*
|
||||
* 群 XkWhXi
|
||||
*
|
||||
* 00 00 00 03 01 41 00 04 01 40 21 04 40
|
||||
* 3E 03 3F A2 群主
|
||||
*
|
||||
* 02 00 00 00 01 00 00 00 00 27 1A 00 C8 01
|
||||
* 00 00 00 01 00 00
|
||||
* F3 C8
|
||||
*
|
||||
* 06 58 6B 57 68 58 69
|
||||
* 00
|
||||
* 00
|
||||
* 3B E6 AC A2 E8 BF 8E E5 BC 80 E8 BD A6 EF BC 8C E5 8E BB 74 6D E7 9A 84 E7 BD 91 E8 AD A6 0A E6 AC A2 E8 BF 8E E5 BC 80 E8 BD A6 EF BC 8C E5 8E BB 74 6D E7 9A 84 E7 BD 91 E8 AD A6
|
||||
* 00
|
||||
* 00
|
||||
* 38 EB 3B A5 90 AC E3 70 1F 42 51 B4 72 81 C8 F5 5A D8 80 69 B6 76 AD A4 AA CC 6A 17 4C 79 81 FF 82 04 BA 13 CE 28 DA 6C 3F 41 77 C0 77 40 B5 87 8E EE 29 20 65 FC 2D FF 63
|
||||
* 00
|
||||
* 0A 00 00 00 00 06 00 03 00 02 00
|
||||
* 01 00 04 00 04 00 00 00 05 00 05 00 04 57 94 6F 41 00 06 00 04 04 08 00 10 00 07 00 04 00 00 04 04 00 09 00 01 00
|
||||
* B1 89 BE 09 00 2D 5C 53 A6 00 01 2F 9B 1C F2 00 00 35 49 95 D1 00 01 3B FA 06 9F 00 00 3E 03 3F A2 00 00 42 C4 32 63 00 01 59 17 3E 05 00 01 6A 89 3E 3E 00 00 6D D7 4E CA 00 00 76 E4 B8 DD 00 00 7C BB 60 3C 00 01 7C BC D3 C1 00 01 87 73 86 9D 00 00 90 19 72 65 00 00 97 30 9A 6B 00 00 9C B1 E5 55 00 01 B1 89 BE 09 00 01
|
||||
*/
|
||||
|
||||
/*
|
||||
* 群 20秃顶28火葬30重生异世
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* 群 Big convene' (与上面两个来自不同 bot)
|
||||
*
|
||||
* 00 00 00 03 01 01 00 04 01 00 80 01 40
|
||||
* 6C 18 F5 DA 群主
|
||||
*
|
||||
* 02 00 00 27 1B 00 00 00 00 27 1B 01 F4 01
|
||||
* 00 00 00 01 00 00
|
||||
* 0F 1F
|
||||
*
|
||||
* 0C 42 69 67 20 63 6F 6E 76 65 6E 65 27 00 群名
|
||||
* 00 96 E6 AF 95 E4 B8 9A E4 BA 86 EF BC 8C E5 B8 8C E6 9C 9B E5 A4 A7 E5 AE B6 E8 83 BD E5 A4 9F E5 83 8F E4 BB A5 E5 89 8D E9 82 A3 E6 A0 B7 E5 BC 80 E5 BF 83 EF BC 8C E5 AD A6 E4 B9 A0 E8 BF 9B E6 AD A5 EF BC 8C E5 A4 A9 E5 A4 A9 E5 BF AB E4 B9 90 E3 80 82 E6 AD A4 E7 BE A4 E7 A6 81 E6 AD A2 E9 AA 82 E4 BA BA EF BC 8C E5 88 B7 E5 B1 8F E6 9A B4 E5 8A 9B EF BC 8C E8 BF 9D E8 A7 84 E8 80 85 E7 A6 81 E8 A8 80 EF BC 8C E4 B8 A5 E9 87 8D E8 80 85 E5 B0 B1
|
||||
* 76 E8 BF 9B E7 BE A4 E6 97 B6 EF BC 8C E8 AF B7 E4 BF AE E6 94 B9 E6 AD A3 E7 A1 AE E5 A7 93 E5 90 8D E3 80 82 E4 B8 8D E8 83 BD 54 E5 90 8C E5 AD A6 EF BC 8C E5 A4 AA E8 BF 87 E5 88 86 E7 9A 84 54 21 28 E4 BA 92 E8 B5 9E E7 BE A4 EF BC 8C E6 89 8B E6 9C BA E5 9C A8 E7 BA BF E8 81 8A E5 A4 A9 E8 80 85 E5 8F AF E4 BB A5 E4 BA 92 E8 B5 9E E5 AF B9 E6 96 B9
|
||||
* 00 38 D9 FD F5 21 A6 1F 8D 61 37 A1 7A 92 91 2A 2C 71 46 A9 B9 1C 45 EB 38 74 4A 74 EA 77 7D 14 DB 12 D0 B0 09 C2 AA 22 16 F1 D0 B9 97 21 F0 5A A0 06 59 A7 3B 2F 32 D2 B8 E3
|
||||
* 00 0F 00 00 00 00 06 00 03 00 02 01 01 00 04 00 04 00 00 00 15 00 05 00 04 52 7C C5 7C 00 06 00 04 00 00 00 20 00 07 00 04 00 00 00 00 00 09 00 01 00
|
||||
*
|
||||
* C5 15 BE BE 00 ???为啥这个只有一个呢
|
||||
* 1C ED 9F 9B 00 00
|
||||
* 26 D0 E1 3A 00 00
|
||||
* 2D 5C 53 A6 00 01 自己 管理员
|
||||
* 2D BD 28 D2 00 00
|
||||
* 2E 94 76 3E 00 00
|
||||
* 35 F3 BC F2 00 00
|
||||
* 37 D6 91 AB 00 00
|
||||
* 3A 60 1C 3E 00 80 10000000 群员, 好友
|
||||
* 3A 86 EA A3 00 48 01001000 群员 手机在线
|
||||
* 3D 7F E7 70 00 00
|
||||
* 3E 03 3F A2 00 09 00001001 好友, 特别关心, TIM PC 在线, 管理员
|
||||
* 41 47 0C DD 00 40 01000000 群员, 离线
|
||||
* 41 B6 32 A8 00 80
|
||||
* 44 C8 DA 23 00 00
|
||||
* 45 3E 1B 6A 00 80 10000000 群员 手机在线
|
||||
* 45 C6 59 E9 00 C0 群员
|
||||
* 4A BD C6 F9 00 00
|
||||
* 4C 67 45 E8 00 00
|
||||
* 4E AD C2 C2 00 80
|
||||
* 4F A0 F7 EC 00 80
|
||||
* 50 CB 11 E8 00 00
|
||||
* 58 22 21 90 00 00
|
||||
* 59 17 3E 05 00 01 管理员 好友
|
||||
* 5E 74 48 D9 00 00
|
||||
* 5E A2 B5 88 00 00
|
||||
* 66 A1 32 9B 00 40
|
||||
* 68 07 29 0A 00 00
|
||||
* 68 0F EF 4F 00 00
|
||||
* 69 8B 14 F3 00 80
|
||||
* 6A A5 27 4E 00 00
|
||||
* 6C 11 A0 89 00 81 10000001 管理员
|
||||
* 6C 18 F5 DA 00 08 群主
|
||||
* 6C 21 F8 E2 00 01 管理员
|
||||
* 71 F8 F5 18 00 00
|
||||
* 72 0B CC B6 00 00
|
||||
* 75 53 38 DF 00 00
|
||||
* 7A A1 8B 82 00 00
|
||||
* 7C 8C 1D 1B 00 00
|
||||
* 7C BC D3 C1 00 00
|
||||
* 84 2D B8 5F 00 00
|
||||
* 88 4C 33 76 00 00
|
||||
* 8C C8 0D 43 00 00
|
||||
* 90 B8 65 22 00 00
|
||||
* 91 54 89 E9 00 00
|
||||
* 9C E6 93 A5 00 01 管理员
|
||||
* 9D 59 6A 36 00 00
|
||||
* 9D 63 81 5C 00 00
|
||||
* 9E 31 AF AC 00 00
|
||||
* 9E 69 86 25 00 80
|
||||
* A1 FD CA 2D 00 00
|
||||
* A5 22 5C 48 00 00
|
||||
* A5 F2 9A B7 00 00
|
||||
* AF 25 74 9E 00 01
|
||||
* B1 50 24 00 00 00
|
||||
* B2 BD 81 A9 00 00
|
||||
* B5 0E B3 DD 00 00
|
||||
* B9 BF 0D BC 00 00
|
||||
* C5 15 BE BE 00 00
|
||||
*/
|
||||
}
|
||||
else -> unsupportedFlag("GroupPacketResponse typed 0x72", flag.toUHexString())
|
||||
}
|
||||
}
|
||||
|
||||
else -> unsupportedType("GroupPacketResponse", packetType.toUHexString())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
@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()
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
@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
|
@ -1,274 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import io.ktor.util.date.GMTDate
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.data.Gender
|
||||
import net.mamoe.mirai.data.Packet
|
||||
import net.mamoe.mirai.data.Profile
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.PacketVersion
|
||||
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
|
||||
inline class AvatarLink(val value: String) : Packet
|
||||
|
||||
inline class NicknameMap(val delegate: Map<UInt, String>) : Packet
|
||||
|
||||
/**
|
||||
* 批量查询昵称.
|
||||
*/
|
||||
internal object QueryNicknamePacket : SessionPacketFactory<NicknameMap>() {
|
||||
/**
|
||||
* 单个查询.
|
||||
*/
|
||||
@PacketVersion(date = "2019.12.7", timVersion = "2.3.2 (21173)")
|
||||
operator fun invoke(
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
qq: Long
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
|
||||
writeHex("03 00 00 00 00 00 00 00 00 00 00")
|
||||
writeByte(1)
|
||||
writeQQ(qq)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询.
|
||||
* 注意!! 服务器不一定全都返回... 需要重复查没返回的
|
||||
*/
|
||||
@PacketVersion(date = "2019.12.7", timVersion = "2.3.2 (21173)")
|
||||
operator fun invoke(
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
qq: Array<UInt>
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
|
||||
writeHex("03 00 00 00 00 00 00 00 00 00 00")
|
||||
writeUByte(qq.size.toUByte())
|
||||
qq.forEach {
|
||||
writeUInt(it)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
批量查询昵称
|
||||
|
||||
发出包ID = UnknownPacketId(01 26)
|
||||
sequence = 44 57
|
||||
fixVer2=02 00 00 00 01 2E 01 00 00 69 35
|
||||
解密body=03 00 00 00 00 00 00 00 00 00 00 [24]
|
||||
(02 45 16 DF) (6C 78 B1 E0) (11 73 69 76) (36 79 19 E1) (49 28 A4 F4) (81 66 8B BC) (2D 6B 19 EC) (28 3D 91 25) (00 54 E5 06) (37 E9 94 CF) (55 7A D6 86)
|
||||
(01 60 31 EC) (2F B1 5E EF) (05 B0 F4 6F) (0F 0D 35 E1) (00 66 7C C5) A6 81 A4 9D 31 05 12 1C A6 A0 EE EF 10 18 86 83 37 99 77 D7 50 BA 4A 8F 10 CE 72 4C 32 71 EE 30 79 63 C6 98 (3E C2 FA 6E) 02 27 13 93 01 2E E5 D7 37 E9 68 46 00 64 B2 1D 03 37 67 20 0A 9C 58 FB 05 94 75 87
|
||||
(0B 9F C6 B6)
|
||||
(18 BE 4B 0E)
|
||||
|
||||
接收包id=UnknownPacketId(01 26),
|
||||
sequence=44 57
|
||||
解析body=UnknownPacket(01 26)
|
||||
body= 03 00 00 00 00 00 00 00 00 00 00 12 04 14 37
|
||||
(02 45 16 DF) 00 36 FF 00
|
||||
[0F] E6 94 BE E7 9D 9B E3 81 AE E5 A4 A9 E7 A9 BA
|
||||
11 88 02 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 80 08 80 00 41
|
||||
(6C 78 B1 E0) 00 2D 1C 01
|
||||
[19] 61 2E E8 B4 A2 E7 A5 9E 7C 20 E6 8E A5 E5 90 84 E7 A7 8D E4 B8 9A E5 8A A1
|
||||
11 C0 02 40 07 C7 08 1C 00 4D 59 53 00 00 4B 4C 00 4B 55 4C 00 00 00 00 00 00 04 00 00 E2 10 34
|
||||
(11 73 69 76) 01 DD 19 00
|
||||
[0C] E5 BC 80 E5 BF 83 E5 B0 B1 E5 A5 BD
|
||||
11 08 82 46 07 CA 00 00 00 00 00 31 00 00 33 32 00 00 00 35 08 04 08 04 08 04 04 01 17 E3 10 32
|
||||
(36 79 19 E1) 00 00 1F 00
|
||||
[0A] 45 70 69 6D 65 74 68 65 75 73
|
||||
00 08 02 00 07 C4 02 08 00 41 42 57 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 10 49
|
||||
(49 28 A4 F4) 02 B5 17 01
|
||||
[21] E9 A1 B9 E7 9B AE 6B 61 6B 61 6F 74 61 6C 6B EF BC 88 E6 9C 89 E4 BA 8B E6 8A 96 E6 88 91 EF BC 89
|
||||
11 80 02 00 07 CC 06 0B 00 00 00 31 00 00 34 34 00 00 00 31 00 00 00 00 00 00 04 00 00 80 10 2E
|
||||
(81 66 8B BC) 02 64 77 00
|
||||
[06] E4 B8 87 E7 A0 81
|
||||
00 80 02 00 07 6C 0A 03 00 00 00 31 00 00 33 31 00 00 00 35 00 00 00 00 00 00 04 00 00 00 00 37
|
||||
(2D 6B 19 EC) 02 58 16 01
|
||||
[0F] E5 93 A5 E5 8F AA E6 98 AF E4 BC A0 E9 80 81
|
||||
11 00 02 00 07 CC 0C 0F 00 00 00 31 00 00 35 33 00 00 00 33 00 00 00 00 00 00 04 00 00 00 00 31
|
||||
28 3D 91 25 02 D9 FF FF 09 E5 B0 8F E8 A1 A8 E5 BC 9F 11 C8 02 46 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 04 08 04 08 04 04 20 06 E2 00 3B 00 54 E5 06 02 5B 78 00 13 61 E5 85 A8 E6 96 B0 E5 9F 9F E5 90 8D E6 89 B9 E5 8F 91 00 08 82 46 07 6B 08 16 00 00 00 31 00 00 34 33 00 00 00 31 00 00 00 00 00 00 04 00 08 02 00 4C 37 E9 94 CF 00 00 00 FF 24 E6 B3 89 E5 B7 9E E5 B8 82 E6 86 A8 E9 BC A0 E7 BD 91 E7 BB 9C E7 A7 91 E6 8A 80 E6 9C 89 E9 99 90 E5 85 AC 01 00 00 00 00 00 00 00 00 00 00 31 00 00 33 35 00 00 00 35 00 00 00 00 00 00 04 00 00 40 02 30 55 7A D6 86 02 49 1B 01 08 E5 AE A2 E6 9C 8D 56 36 11 00 02 00 07 C8 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 00 12 00 4A 01 60 31 EC 02 58 FF 00 22 E5 BC 80 E5 BD A9 E7 BD 91 2D E5 B0 8F E8 B1 86 28 31 32 E5 88 B0 32 32 E7 82 B9 E5 9C A8 E7 BA BF 29 00 40 00 00 00 00 05 1E 00 00 00 31 00 00 35 31 00 00 00 31 08 04 04 09 0C 04 04 00 00 02 00 38 2F B1 5E EF 00 00 00 FF 10 E4 B8 8A E6 96 B9 65 E6 8E A8 E8 BD AF E4 BB B6 01 80 00 00 00 00 00 00 00 00 00 31 00 00 34 34 00 00 00 33 00 00 00 00 00 00 04 00 00 00 00 39 05 B0 F4 6F 00 78 23 01 11 E2 99 A1 20 E2 9C BF E2 80 BF E2 9C BF 20 52 4F 4E 11 88 82 42 07 C0 09 0C 00 00 00 31 00 00 33 35 00 00 00 35 00 00 00 00 00 00 04 00 00 80 00 2E 0F 0D 35 E1 00 00 23 00 06 E5 9C B0 E8 A1 A3 00 40 02 46 07 C0 02 08 00 00 00 31 00 00 34 35 00 00 00 33 00 00 00 00 00 00 04 10 00 02 00 31 00 66 7C C5 00 00 0A 00 09 E8 B5 9A E5 B0 8F E5 AE A2 00 C8 02 42 07 D8 0C 0C 00 00 00 31 00 00 34 31 00 00 00 31 00 00 00 00 00 00 04 24 02 00 00 3A
|
||||
(A6 81 A4 9D) 02 25 29 00 12 E6 96 B0 E6 98 93 E9 80 9A E5 AE A2 E6 9C 8D E4 B8 89 11 80 02 01 07 BA 0A 10 00 00 00 31 00 00 33 35 00 00 00 35 00 00 00 00 00 00 04 00 00 00 00 3A 31 05 12 1C 01 86 1C 00 12 E5 BF A7 E4 BC A4 E8 BF 98 E6 98 AF E5 BF AB E4 B9 90 00 88 02 02 07 C7 04 13 00 00 00 31 00 00 33 35 00 00 00 35 00 00 00 00 00 00 04 00 08 20 00 00 04 5D EC AF 48
|
||||
|
||||
---------------------------
|
||||
|
||||
发出包ID = UnknownPacketId(01 26)
|
||||
sequence = 44 58
|
||||
fixVer2=02 00 00 00 01 2E 01 00 00 69 35
|
||||
解密body=03 00 00 00 00 00 00 00 00 00 00 12 A6 A0 EE EF 10 18 86 83 37 99 77 D7 50 BA 4A 8F 10 CE 72 4C 32 71 EE 30 79 63 C6 98
|
||||
(3E C2 FA 6E) 02 27 13 93 01 2E E5 D7 37 E9 68 46 (00 64 B2 1D) (03 37 67 20) 0A 9C 58 FB 05 94 75 87 (11 48 2B 1A) 0B 9F C6 B6 18 BE 4B 0E
|
||||
|
||||
接收包id=UnknownPacketId(01 26),
|
||||
sequence=44 58
|
||||
解析body=UnknownPacket(01 26)
|
||||
body=03 00 00 00 00 00 00 00 00 00 00 00 03 8E 3C A6 A0 EE EF 02 07 6C 01 14 E8 8B B9 E6 9E 9C 49 44 E4 B8 93 E4 B8 9A E8 A7 A3 E9 94 81 11 00 02 01 07 77 01 01 00 00 00 31 00 00 31 31 00 00 00 31 00 00 00 00 00 00 04 00 00 02 00 31 10 18 86 83 00 C3 22 00 09 35 35 38 E7 94 B5 E8 AE AF 00 08 02 40 07 C1 0C 01 00 00 00 31 00 00 33 35 00 00 00 35 08 04 08 04 08 04 04 00 04 00 10 35 37 99 77 D7 02 5B 2A 00 0D E8 BF 9C E6 8B 93 E7 94 B5 E8 AE AF 33 00 C0 00 02 07 B9 09 0E 00 00 00 31 00 00 33 35 00 00 00 35 00 00 00 00 00 00 04 00 00 40 00 2E 50 BA 4A 8F 02 1C 09 00 06 E6 96 B9 E5 80 8D 11 40 02 00 07 DA 01 01 00 00 00 31 00 00 34 34 00 00 00 33 00 00 00 00 00 00 04 00 00 82 00 2C 10 CE 72 4C 00 00 23 00 04 4C 6F 73 74 11 08 02 00 07 C0 02 12 00 00 00 31 00 00 31 31 00 00 00 38 08 04 00 00 00 00 04 00 08 72 00 30 32 71 EE 30 00 93 21 00 08 69 57 65 62 53 68 6F 70 00 00 02 00 07 C2 01 01 00 00 00 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 00 12 00 2B 79 63 C6 98 02 A6 12 00 03 5E 4F 5E 11 80 02 00 07 D1 08 12 00 00 00 31 00 00 33 37 00 00 00 32 00 00 00 00 00 00 04 00 00 60 00 3D 3E C2 FA 6E 02 2B 1D 01 15 E9 A3 8E E9 93 83 E8 8D 89 E6 95 99 E8 82 B2 E6 9C 8D E5 8A A1 00 80 00 00 07 C6 06 10 00 00 00 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 00 2E 02 27 13 93 02 0D FF FF 06 E7 83 82 E8 8F 9C 00 08 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 04 A2 00 33 01 2E E5 D7 00 D8 22 00 0B 4F 28 E2 88 A9 5F E2 88 A9 29 4F 11 48 82 46 07 C1 08 1A 00 00 00 31 00 00 33 35 00 00 00 35 08 04 08 04 08 04 04 00 0C 80 08 30 37 E9 68 46 00 00 00 FF 08 33 35 E4 BA 92 E8 81 94 01 00 00 00 00 00 00 00 00 00 00 31 00 00 33 35 00 00 00 32 00 00 00 00 00 00 04 00 00 40 02 37 00 64 B2 1D 00 00 FF FF 0F E5 98 89 E4 BC A6 E8 99 8E E8 99 8E E8 99 8E 01 48 06 42 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 01 40 8C 00 34 03 37 67 20 00 93 24 00 0C E8 93 9D E6 98 9F E7 A7 91 E6 8A 80 00 08 02 46 07 BF 0A 1A 00 00 00 31 00 00 33 35 00 00 00 35 08 04 08 04 08 04 04 00 00 00 10 2C 0A 9C 58 FB 01 53 00 00 04 4B E3 80 81 00 48 02 02 07 E3 00 00 00 00 00 31 00 00 00 00 00 00 00 00 0C 04 08 04 04 09 04 FD 03 02 00 2E 05 94 75 87 02 25 1F 00 06 54 4E 54 50 52 4F 00 40 02 00 07 C4 01 01 00 00 00 31 00 00 34 33 00 00 00 31 00 00 00 00 00 00 04 00 00 02 00 2E 11 48 2B 1A 02 0A 0B 00 06 E6 9D 8E E9 98 B3 11 08 02 02 07 D8 03 1C 00 00 00 31 00 00 34 34 00 00 00 33 00 00 00 00 00 00 04 00 00 6C 00 30 0B 9F C6 B6 00 AE 14 00 08 F0 9F 91 BC F0 9F 91 BF 11 00 02 42 07 CF 01 18 00 00 00 00 00 00 00 00 00 00 00 00 08 04 08 04 04 07 04 00 04 A0 00 34
|
||||
(18 BE 4B 0E) 00 00 36 00
|
||||
[0C] E5 B3 B0 E5 9B 9E E8 B7 AF E8 BD AC
|
||||
00 08 02 00 07 AD 02 03 00 00 00 31 00 00 31 35 00 00 32 32 00 00 00 00 00 00 04 00 00 00 00 00
|
||||
|
||||
04 5D EC AF 48
|
||||
*/
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): NicknameMap {
|
||||
//03 00 00 00 00 00 00 00 00 00 00 12 04 14 37
|
||||
val type = readUByte().toInt()
|
||||
if (type == 15) {
|
||||
discardExact(14)
|
||||
|
||||
val map = linkedMapOf<UInt, String>()
|
||||
while (remaining != 5L) { // 最后总是会剩余 04 5D EC AF 48
|
||||
val qq = readUInt()
|
||||
discardExact(4) // 4 个状态信息, 未知
|
||||
val nickname = readString(readUByte().toInt())
|
||||
discardExact(32) // 未知
|
||||
map[qq] = nickname
|
||||
}
|
||||
return NicknameMap(map)
|
||||
} else {
|
||||
error("Unsupported type $type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用户资料的头像
|
||||
/**
|
||||
* 请求获取头像
|
||||
*/ // ? 这个包的数据跟下面那个包一样
|
||||
internal object RequestProfileAvatarPacket : SessionPacketFactory<AvatarLink>() {
|
||||
//00 01 00 17 D4 54 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5
|
||||
operator fun invoke(
|
||||
bot: Long,
|
||||
qq: Long,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
|
||||
writeUShort(0x01u)
|
||||
writeQQ(qq)
|
||||
writeHex("00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5")
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): AvatarLink {
|
||||
println(" RequestProfileAvatarPacket body=${this.readBytes().toUHexString()}")
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求账号详细信息.
|
||||
*
|
||||
* @see Profile
|
||||
*/
|
||||
internal object RequestProfileDetailsPacket : SessionPacketFactory<RequestProfileDetailsResponse>() {
|
||||
//00 01 3E F8 FB E3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5
|
||||
//00 01 B1 89 BE 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5
|
||||
//00 01 87 73 86 9D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5
|
||||
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")
|
||||
operator fun invoke(
|
||||
bot: Long,
|
||||
qq: Long,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
|
||||
writeUShort(0x01u)
|
||||
writeQQ(qq)
|
||||
writeHex("00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5")
|
||||
}
|
||||
|
||||
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): RequestProfileDetailsResponse {
|
||||
discardExact(3)
|
||||
val qq = readUInt().toLong()
|
||||
discardExact(6)
|
||||
val map = readTLVMap(tagSize = 2, expectingEOF = true)
|
||||
//map.printTLVMap("Profile(qq=$qq) raw=")
|
||||
//map.mapValues { it.value.encodeToString() }.printTLVMap("Profile(qq=$qq) str=")
|
||||
val profile = Profile(
|
||||
qq = qq,
|
||||
nickname = map[0x4E22]?.encodeToString() ?: "",//error("Cannot determine nickname")
|
||||
englishName = map[0x4E54]?.encodeToString(),
|
||||
chineseName = map[0x4E2A]?.encodeToString(),
|
||||
qAge = map[0x6597]?.get(0)?.toInt(),
|
||||
zipCode = map[0x4E25]?.encodeToString(),
|
||||
phone = map[0x4E27]?.encodeToString(),
|
||||
gender = when (map[0x4E29]?.let { it[0] }?.toUInt()) {
|
||||
null -> Gender.SECRET //error("Cannot determine gender, entry 0x4E29u not found")
|
||||
0x02u -> Gender.FEMALE
|
||||
0x01u -> Gender.MALE
|
||||
else -> Gender.SECRET // 猜的
|
||||
//else -> error("Cannot determine gender, bad value of 0x4E29u: ${map[0x4729u]!![0].toHexString()}")
|
||||
},
|
||||
birthday = map[0x4E3F]?.let { GMTDate(it.toUInt().toLong()) },
|
||||
personalStatement = map[0x4E33]?.encodeToString(),
|
||||
homepage = map[0x4E2D]?.encodeToString(),
|
||||
company = map[0x5DC8]?.encodeToString(),
|
||||
school = map[0x4E35]?.encodeToString(),
|
||||
email = map[0x4E2B]?.encodeToString()
|
||||
)
|
||||
map.clear()
|
||||
|
||||
return RequestProfileDetailsResponse(qq, profile)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class RequestProfileDetailsResponse(
|
||||
val qq: Long,
|
||||
val profile: Profile
|
||||
) : Packet {
|
||||
//00 01 00 99 6B F8 D2 00 00 00 00 00 29
|
||||
// 4E 22 00 0F E4 B8 8B E9 9B A8 E6 97 B6 E6 B5 81 E6 B3 AA 4E 25 00 00 4E 26 00 0C E4 B8 AD E5 9B BD E6 B2 B3 E5 8C 97 4E 27 00 0B 30 33 31 39 39 39 39 39 39 39 39
|
||||
// 4E 29 [00 01] 01 4E 2A 00 00 4E 2B 00 17 6D 61 69 6C 2E 71 71 32 35 37 33 39 39 30 30 39 38 2E 40 2E 63 6F 6D 4E 2D 00 00 4E 2E 00 02 31 00 4E 2F 00 04 36 37 38 00 4E 30 00 00 4E 31 00 01 00 4E 33 00 00 4E 35 00 00 4E 36 00 01 00 4E 37 00 01 00 4E 38 00 01 00 4E 3F 00 04 07 C1 01 01 4E 40 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 4E 41 00 02 00 00 4E 42 00 02 00 00 4E 43 00 02 00 00 4E 45 00 01 22 4E 49 00 04 00 00 00 00 4E 4B 00 04 00 00 00 00 4E 4F 00 01 00 4E 54 00 00 4E 5B 00 00 52 0B 00 04 00 C0 00 01 52 0F 00 14 00 00 00 00 00 00 00 00 12 00 00 48 09 10 00 00 00 00 00 00 5D C2 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 5D C8 00 00 65 97 00 01 08 69 9D 00 04 00 00 00 00 69 A9 00 00 9D A5 00 02 00 01 A4 91 00 02 00 00 A4 93 00 02 00 00 A4 94 00 02 00 00 A4 9C 00 02 00 00 A4 B5 00 02 00 00
|
||||
|
||||
//00 01 00 87 73 86 9D 00 00 00 00 00 29 4E 22 00 15 E6 98 AF E6 9C 9D E8 8F 8C E4 B8 8D E7 9F A5 E6 99 A6 E6 9C 94 4E 25 00 00 4E 26 00 00 4E 27 00 00
|
||||
// 4E 29 [00 01] 01 4E 2A 00 00 4E 2B 00 00 4E 2D 00 00 4E 2E 00 02 31 00 4E 2F 00 04 37 32 30 00 4E 30 00 00 4E 31 00 01 01 4E 33 00 00 4E 35 00 00 4E 36 00 01 00 4E 37 00 01 04 4E 38 00 01 00 4E 3F 00 04 07 CF 00 00 4E 40 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 4E 41 00 02 00 00 4E 42 00 02 00 00 4E 43 00 02 00 00 4E 45 00 01 13 4E 49 00 04 00 00 00 00 4E 4B 00 04 00 00 00 00 4E 4F 00 01 00 4E 54 00 00 4E 5B 00 04 00 00 00 00 52 0B 00 04 13 80 02 00 52 0F 00 14 00 04 02 00 00 00 00 00 12 04 10 58 89 50 C0 00 22 00 00 00 5D C2 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 5D C8 00 00 65 97 00 01 08 69 9D 00 04 00 00 00 00 69 A9 00 00 9D A5 00 02 00 01 A4 91 00 02 00 00 A4 93 00 02 00 01 A4 94 00 02 00 00 A4 9C 00 02 00 00 A4 B5 00 02 00 00
|
||||
|
||||
//00 01 00 76 E4 B8 DD
|
||||
// 00 00 00 00 00 29
|
||||
|
||||
// 4E 22 [00 0E] 73 74 65 61 6D 63 68 69 6E 61 2E 66 75 6E //昵称
|
||||
// 4E 25 [00 06] 34 33 33 31 30 30 //邮编
|
||||
// 4E 26 [00 09] E4 B8 8D E7 9F A5 E9 81 93 //?
|
||||
// 4E 27 [00 0A] 31 33 38 2A 2A 2A 2A 2A 2A 2A // 手机号
|
||||
// 4E 29 [00 01] 02 性别, 女02, 男01
|
||||
// 4E 2A [00 00]
|
||||
// 4E 2B [00 00]
|
||||
// 4E 2D [00 23] 68 74 74 70 3A 2F 2F 77 77 77 2E 34 33 39 39 2E 63 6F 6D 2F 66 6C 61 73 68 2F 33 32 39 37 39 2E 68 74 6D //http://www.4399.com/flash/32979.htm //???
|
||||
// 4E 2E [00 02] 31 00
|
||||
// 4E 2F [00 04] 36 30 33 00
|
||||
// 4E 30 [00 00]
|
||||
// 4E 31 [00 01] 00
|
||||
// 4E 33 [00 00]
|
||||
// 4E 35 [00 00]
|
||||
// 4E 36 [00 01] 0A
|
||||
// 4E 37 [00 01] 06
|
||||
// 4E 38 [00 01] 00
|
||||
// 4E 3F [00 04] 07 DD 0B 13 生日 short byte byte
|
||||
// 4E 40 [00 0C] 00 41 42 57 0// 0 00 00 00 00 00 00 00
|
||||
// 4E 41 [00 02] 08 04
|
||||
// 4E 42 [00 02] 00 00
|
||||
// 4E 43 [00 02] 0C 04
|
||||
// 4E 45 [00 01] 05
|
||||
// 4E 49 [00 04] 00 00 00 00
|
||||
// 4E 4B [00 04] 00 00 00 00
|
||||
// 4E 4F [00 01] 06
|
||||
// 4E 54 [00 00]
|
||||
// 4E 5B [00 04] 00 00 00 00
|
||||
// 52 0B [00 04] 13 80 02 00
|
||||
// 52 0F [00 14] 01 00 00 00 00 00 00 00 52 00 40 48 89 50 80 02 00 00 03 00
|
||||
// 5D C2 [00 0C] 00 41 42 57 00 00 00 00 00 00 00 00
|
||||
// 5D C8 [00 00]
|
||||
// 65 97 [00 01] 07
|
||||
// 69 9D [00 04] 00 00 00 00
|
||||
// 69 A9 [00 00]
|
||||
// 9D A5 [00 02] 00 01
|
||||
// A4 91 [00 02] 00 00
|
||||
// A4 93 [00 02] 00 01
|
||||
// A4 94 [00 02] 00 00
|
||||
// A4 9C [00 02] 00 00
|
||||
// A4 B5 [00 02] 00 00
|
||||
|
||||
/*
|
||||
00 01 00 76 E4 B8 DD 00 00 00 00 00 29
|
||||
4E 22 00 0E 73 74 65 61 6D 63 68 69 6E 61 2E 66 75 6E 4E 25 00 06 34 33 33 31 30 30 4E 26 00 09 E4 B8 8D E7 9F A5 E9 81 93 4E 27 00 0A 31 33 38 2A 2A 2A 2A 2A 2A 2A 4E 29 00 01 01 4E 2A 00 00 4E 2B 00 00 4E 2D 00 23 68 74 74 70 3A 2F 2F 77 77 77 2E 34 33 39 39 2E 63 6F 6D 2F 66 6C 61 73 68 2F 33 32 39 37 39 2E 68 74 6D 4E 2E 00 02 31 00 4E 2F 00 04 36 30 33 00 4E 30 00 00 4E 31 00 01 00 4E 33 00 00 4E 35 00 00 4E 36 00 01 0A 4E 37 00 01 06 4E 38 00 01 00 4E 3F 00 04 07 DD 0B 13 4E 40 00 0C 00 41 42 57 00 00 00 00 00 00 00 00 4E 41 00 02 08 04 4E 42 00 02 00 00 4E 43 00 02 0C 04 4E 45 00 01 05 4E 49 00 04 00 00 00 00 4E 4B 00 04 00 00 00 00 4E 4F 00 01 06 4E 54 00 00 4E 5B 00 04 00 00 00 00 52 0B 00 04 13 80 02 00 52 0F 00 14 01 00 00 00 00 00 00 00 52 00 40 48 89 50 80 02 00 00 03 00 5D C2 00 0C 00 41 42 57 00 00 00 00 00 00 00 00 5D C8 00 00 65 97 00 01 07 69 9D 00 04 00 00 00 00 69 A9 00 00 9D A5 00 02 00 01 A4 91 00 02 00 00 A4 93 00 02 00 01 A4 94 00 02 00 00 A4 9C 00 02 00 00 A4 B5 00 02 00 00
|
||||
*/
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
@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())
|
||||
}
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
@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()
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
@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
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
@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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
@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())
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
@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
|
||||
)
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
@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
|
||||
|
||||
*/
|
@ -1,24 +0,0 @@
|
||||
@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())
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
@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
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
@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))*/
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
@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)
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
@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()
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
@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
|
||||
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
@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()}")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
@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)
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
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
|
@ -1,42 +0,0 @@
|
||||
@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())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "FunctionName")
|
||||
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.*
|
||||
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.OutgoingPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketFactory
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketId
|
||||
import net.mamoe.mirai.timpc.network.packet.buildOutgoingPacket
|
||||
import net.mamoe.mirai.utils.cryptor.DecrypterByteArray
|
||||
import net.mamoe.mirai.utils.cryptor.DecrypterType
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
|
||||
internal object CaptchaKey : DecrypterByteArray,
|
||||
DecrypterType<CaptchaKey> {
|
||||
override val value: ByteArray = TIMProtocol.key00BA
|
||||
}
|
||||
|
||||
internal object CaptchaPacket : PacketFactory<CaptchaPacket.CaptchaResponse, CaptchaKey>(CaptchaKey) {
|
||||
/**
|
||||
* 请求验证码传输
|
||||
*/
|
||||
fun RequestTransmission(
|
||||
bot: Long,
|
||||
token0825: ByteArray,
|
||||
captchaSequence: Int,
|
||||
token00BA: ByteArray
|
||||
): OutgoingPacket = buildOutgoingPacket(name = "CaptchaPacket.RequestTransmission") {
|
||||
writeQQ(bot)
|
||||
writeFully(TIMProtocol.fixVer)
|
||||
writeFully(TIMProtocol.key00BA)
|
||||
encryptAndWrite(TIMProtocol.key00BA) {
|
||||
writeHex("00 02 00 00 08 04 01 E0")
|
||||
writeFully(TIMProtocol.constantData2)
|
||||
writeHex("00 00 38")
|
||||
writeFully(token0825)
|
||||
writeHex("01 03 00 19")
|
||||
writeFully(TIMProtocol.publicKey)
|
||||
writeHex("13 00 05 00 00 00 00")
|
||||
writeUByte(captchaSequence.toUByte())
|
||||
writeHex("00 28")
|
||||
writeFully(token00BA)
|
||||
writeHex("00 10")
|
||||
writeFully(TIMProtocol.key00BAFix)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新验证码
|
||||
*/
|
||||
fun Refresh(
|
||||
bot: Long,
|
||||
token0825: ByteArray
|
||||
): OutgoingPacket = buildOutgoingPacket(name = "CaptchaPacket.Refresh") {
|
||||
writeQQ(bot)
|
||||
writeFully(TIMProtocol.fixVer)
|
||||
writeFully(TIMProtocol.key00BA)
|
||||
encryptAndWrite(TIMProtocol.key00BA) {
|
||||
writeHex("00 02 00 00 08 04 01 E0")
|
||||
writeFully(TIMProtocol.constantData2)
|
||||
writeHex("00 00 38")
|
||||
writeFully(token0825)
|
||||
writeHex("01 03 00 19")
|
||||
writeFully(TIMProtocol.publicKey)
|
||||
writeHex("13 00 05 00 00 00 00 00 00 00 00 10")
|
||||
writeFully(TIMProtocol.key00BAFix)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交验证码
|
||||
*/
|
||||
fun Submit(
|
||||
bot: Long,
|
||||
token0825: ByteArray,
|
||||
captcha: String,
|
||||
captchaToken: IoBuffer
|
||||
): OutgoingPacket = buildOutgoingPacket(name = "CaptchaPacket.Submit") {
|
||||
require(captcha.length == 4) { "captcha.length must == 4" }
|
||||
writeQQ(bot)
|
||||
writeFully(TIMProtocol.fixVer)
|
||||
writeFully(TIMProtocol.key00BA)
|
||||
encryptAndWrite(TIMProtocol.key00BA) {
|
||||
|
||||
//00 02 00 00 08 04 01 E0
|
||||
// 00 00 04 56 00 00 00 01 00 00 15 E3 01
|
||||
// 00 38 58 CE A0 12 81 31 5C 5E 36 23 5B E4 0E 05 A6 47 BF 7C 1A 7A 35 37 59 90 17 50 66 0C 07 03 77 E4 48 DB 28 0A CF C3 A9 B7 C0 95 D3 9D 00 AA A5 EB FB D6 85 8D 10 61 5A D0
|
||||
// 01 03
|
||||
// 00 19 02 CA 53 7E F0 7B 32 82 EC 9F DE CF 51 8B A4 93 26 76 EC 42 1C 02 00 74 58
|
||||
// 14 00 05 00 00 00 00 00
|
||||
// 04
|
||||
// 6C 73 64 61
|
||||
//
|
||||
// 00 40 CE 99 84 E8 F1 59 31 B0 3F 6C 4D 44 09 E4 82 77 96 67 03 A7 3A EA 8F 36 B9 20 79 7E C9 0F 75 3C 2A C3 E1 E5 C6 00 B3 5E 91 5B 47 63 EF AF 30 C0 48 2F 58 23 96 CF 65 2F 4C 75 95 A6 CA 5A 2C 5C
|
||||
//
|
||||
// 00 10 E1 50 C9 F4 F6 F4 2F D1 7F E9 8C AB B6 1C 38 7B
|
||||
|
||||
writeHex("00 02 00 00 08 04 01 E0")
|
||||
writeFully(TIMProtocol.constantData2) //00 00 04 53 00 00 00 01 00 00 15 85
|
||||
writeHex("01 00 38")
|
||||
writeFully(token0825)
|
||||
writeHex("01 03")
|
||||
|
||||
writeShort(25)
|
||||
writeFully(TIMProtocol.publicKey)//25
|
||||
|
||||
writeHex("14 00 05 00 00 00 00 00 04")
|
||||
writeStringUtf8(captcha.toUpperCase())
|
||||
writeHex("00 40")
|
||||
writeFully(captchaToken)
|
||||
|
||||
writeShort(16)
|
||||
writeFully(TIMProtocol.key00BAFix)//16
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CaptchaResponse : Packet {
|
||||
lateinit var token00BA: ByteArray//56 bytes
|
||||
|
||||
class Correct : CaptchaResponse() {
|
||||
override fun toString(): String = "CaptchaResponse.Correct"
|
||||
}
|
||||
|
||||
class Transmission : CaptchaResponse() {
|
||||
lateinit var captchaSectionN: IoBuffer
|
||||
lateinit var captchaToken: IoBuffer//0x40=64bytes
|
||||
var transmissionCompleted: Boolean = false//验证码是否已经传输完成
|
||||
override fun toString(): String = "CaptchaResponse.Transmission"
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): CaptchaResponse =
|
||||
when (val flag = readByte().toUInt()) {
|
||||
0x14u -> {//00 05 00 00 00 00 00 00 38
|
||||
CaptchaResponse.Correct().apply {
|
||||
discardExact(9)
|
||||
token00BA = readBytes(56)
|
||||
}
|
||||
}
|
||||
0x13u -> {
|
||||
CaptchaResponse.Transmission().apply {
|
||||
with(debugPrintThis("验证码包")) {
|
||||
/*
|
||||
* 00 05 01 00 00 01 23
|
||||
* 00 40 A0 E9 2F 12 1D 2E B1 15 26 89 EB C3 F0 9A 0C 03 00 03 A0 F2 74 57 58 57 E9 7A 2B C7 52 5D BC D8 7B D5 A4 7C AD 33 85 85 39 88 D2 CE AD 68 36 2E F0 AE 19 E8 25 3A F7 3A AD BE 19 A9 E7 C4 B5 4C
|
||||
* 02 BC 28 01 38 40 C6 4E 85 A6 32 24 0C 3C B3 19 46 5D AD 56 AC 3D 3A E1 ED AD 8C 60 05 47 37 10 DC AD E5 72 F9 F9 18 B8 0C 13 10 D2 4E C9 3C 02 BE 57 E4 02 E0 6C 6C 6E E9 3C 57 28 66 BD 0C D3 FF CC 5A 47 B4 F1 7C 87 85 24 B0 60 44 20 1C 1E AD 95 7B CB 45 AF 43 95 10 0F 1D 0B 33 CB 09 7E BE F8 35 B0 D4 5C AB 9E 5A BE 34 E8 B9 2E 65 C7 DA F5 E1 EB 71 43 31 A1 2E 40 4D 84 22 EF 8F CD 05 13 33 E5 CF E3 AA 09 C7 71 15 30 A4 83 A7 36 84 90 4D 4C A7 67 66 4B A5 D7 C5 FB 5E D7 26 ED 9C 92 AD 7C 8F 09 36 A3 60 84 16 07 45 B4 6E EA CD 05 EC C7 0B BA A2 BE 71 24 E6 49 C1 FC 05 3E 26 C9 E6 F7 EA B3 25 8D BA 1F 15 3D DC BC FD CE A6 79 FF 8B 28 B6 12 78 F3 8F EB A6 A9 B6 A5 5F 65 58 CC CD FC F6 BC A6 46 21 68 70 64 82 C7 8F 79 1A C0 B3 48 B6 CD C8 7C 7E 90 61 43 F7 A6 D7 B1 39 F1 72 C7 78 7E 37 49 50 6A B6 9F 5B 8D A9 C0 B0 BB F0 EF 9D CD 6E F7 E7 5A 3C BA E1 02 2E A0 2D 00 04 07 25 B3 B2 34 FA CD 6E C3 A4 ED 87 88 59 D8 63 0C 1D 27 D1 04 4D B5 5B 6E 43 07 17 79 FA EB C2 A3 11 77 72 9D C5 55 90 80 EF 01 47 1A 10 02 E7 02 F6 8A 76 E9 E5 C1 A0 F6 E4 B4 65 36 7F 41 36 37 E8 CE 99 7F 49 66 2A 61 7D A8 D2 57 D9 18 E9 FA 85 CB 3A 1E 7A DE 8C 07 F5 2A CA 33 25 D4 E0 86 08 75 50 B6 1C EE 99 BA 56 F8 4F E9 EF CD E6 27 EE 81 D9 CC 5E 7F 4A 33 54 CB F9 A5 92 DE 76 0B F4 57 29 65 77 BC BD 3D CC E5 1C C4 2E 2E 02 0E 41 A0 09 29 ED DB F2 53 6B 19 6A ED EC FA D5 0B 76 E6 87 CC 99 9E 80 75 28 A6 92 6D 63 DB BF D7 09 B1 DA DD EC CB D6 7F 5E 60 14 83 C7 B8 19 85 97 37 BA 64 0C AA B7 E9 D5 E0 C2 0F 7A 86 DA 56 96 D1 07 FD DA F0 F1 83 9E 8B 49 F3 DF 3C 2F FD 35 33 55 D2 D4 FA D0 3B 52 BE CD 22 60 22 9E 4C 03 EA 1A 3A 23 46 29 C0 A2 12 51 BC 81 EF E6 FF E8 E9 19 8D 66 F4 F4 A5 FE CD 33 8F 77 67 DC 38 F9 E4 1F D4 63 0D CF 24 AA F5 E1 89 7D F3 79 3D B6 47 02 E9 F8 C9 D0 5A DF 84 00 08 B6 E2 95 3F 3D B3 4E 83 CE EC 91 52 ED 61 63 74 7B 6E CC CC EE A3 5D 3F 7B 91 2E EA F7 3C 0C 3A 4C BC 08 86 A0 6A 63 D0 2D 30 EF 28 BC B3 85 57 85 C1 39 D8 AC FC ED 64 C7 C4 A9 EA F2 5A C5 7F 96 9B 1B CF 97 1E 16 8B EB E4 D7 23 7B 7B D9 E4 09 C9 32 BD 35 B6 AF FE 92 C5 78 BF E1 1A D8 A1 0A 09 5E DE 22 8A F7 7A 9F 4E A2 FD 7E
|
||||
* 01 //第几个包
|
||||
* 01 //是否还有更多
|
||||
* 00 28 39 24 31 73 77 6E 55 E7 99 4D 9E 56 AF 6D 38 77 10 60 3B 68 45 41 35 70 1D B4 FE 7E CE 78 65 5A D7 C8 95 AF F2 6B 6D C8
|
||||
* 00 10 CC A9 FA 63 A8 34 C7 3C E6 F7 2E 15 B7 EF 3E 07
|
||||
*/
|
||||
discardExact(7)
|
||||
captchaToken = readIoBuffer(readUShort().toInt()) // 0x40=64,
|
||||
|
||||
/*
|
||||
*00 05 01 00 00 01 23
|
||||
* 00 40 0B 84 40 B1 59 9C FE B8 EC E4 E8 36 2B 4B 03 C7 9F 5D FA A3 7B 43 BD 50 19 55 EA 4C A8 DE 49 FF 5F 45 89 7F 2E B2 6D C9 D6 B7 08 3B 60 31 74 4C FA DA 5F 5F A6 80 ED A1 19 48 F9 C9 4A 6A AD F6
|
||||
* 00 48 39 46 4F 92 0F 70 C5 55 81 3E 0B E7 96 18 4F 31 93 FE 1B D6 A9 50 97 97 E0 83 76 03 6C 50 80 2B 65 13 63 44 A3 9E 0B D9 C0 10 60 70 5B A1 53 2C FD 3B 84 DF A6 E6 F6 1F 71 B5 A6 54 00 00 00 00 49 45 4E 44 AE 42 60 82
|
||||
* 04 //第几个包
|
||||
* 00 //是否还有更多
|
||||
* 00 28 3C 40 BD A5 8B F9 63 97 7E 62 34 E3 F9 49 49 9E 21 01 3C 64 21 AE 8D 87 21 9F 44 4A 0C 6F 85 32 B4 13 4C 59 66 E7 EE 17
|
||||
* 00 10 AF 66 92 E2 B4 39 6B 9A BA 29 EF AA 8D 98 79 55
|
||||
*/
|
||||
captchaSectionN = readIoBuffer(readUShort().toInt()) // <=700
|
||||
|
||||
discardExact(1) // 第几个包
|
||||
transmissionCompleted = readByte().toInt() == 0
|
||||
|
||||
token00BA = readBytes(readUShort().toInt())
|
||||
|
||||
println(token00BA.toUHexString())
|
||||
// 剩余
|
||||
// 00 10 AF 66 92 E2 B4 39 6B 9A BA 29 EF AA 8D 98 79 55
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
发出包ID = CaptchaPacket(00 BA)
|
||||
sequence = 71 6B
|
||||
fixVer2=03 00 01 00 01 2E 01 00 00 69 35 00 00 00 00
|
||||
密文=65 F7 F3 14 E3 94 10 1F DD 95 84 A3 F5 9F AD 94 8D 4F 6A 70 F8 4A DE 43 AF 75 D1 3F 3A 3F F2 E0 A8 16 1A 46 13 CD B0 51 45 00 29 52 57 75 6D 4A 4C D9 B7 98 8C B0 96 EC 57 4E 67 FB 8D C5 F1 BF 72 38 40 42 19 54 C2 28 F4 72 C8 AE 24 EB 66 B5 D0 45 0B 72 44 81 E2 F6 2B EE C3 85 93 BA CB B7 72 F4 1A 30 F9 5B 3D B0 79 3E F4 0B F2 1A A7 49 60 3B 37 02 60 0C 5D D5 76 76 47 4F B5 B3 F5 CA 58 6C FC D2 41 3E 24 D1 FB 0A 18 53 D8 E5 A5 85 A8 BC 51 54 3B 66 5B 21 C6 7B AF C9 62 F0 AA 9C CF 2E 84 0F CC 15 5B 35 93 49 5C E4 28 49 A7 8A D3 30 A9 6E 36 4E 7A 49 28 69 4D C3 25 39 6E 45 6E 40 F2 86 1E F4 4F 00 A6 9D E6 9B 84 19 69 C1 31 6A 17 BA F0 0D 8A 22 09 86 24 92 F7 22 C3 47 7F F2 BF 94 8A 8A B5 29
|
||||
解密body=
|
||||
00 02 00 00 08 04 01 E0 00 00 04 56 00 00 00 01 00 00 15 E3 01 00 38 58 CE A0 12 81 31 5C 5E 36 23 5B E4 0E 05 A6 47 BF 7C 1A 7A 35 37 59 90 17 50 66 0C 07 03 77 E4 48 DB 28 0A CF C3 A9 B7 C0 95 D3 9D 00 AA A5 EB FB D6 85 8D 10 61 5A D0 01 03 00 19 02 CA 53 7E F0 7B 32 82 EC 9F DE CF 51 8B A4 93 26 76 EC 42 1C 02 00 74 58 14 00 05 00 00 00 00 00 04 6C 73 64 61 00 40 CE 99 84 E8 F1 59 31 B0 3F 6C 4D 44 09 E4 82 77 96 67 03 A7 3A EA 8F 36 B9 20 79 7E C9 0F 75 3C 2A C3 E1 E5 C6 00 B3 5E 91 5B 47 63 EF AF 30 C0 48 2F 58 23 96 CF 65 2F 4C 75 95 A6 CA 5A 2C 5C 00 10 E1 50 C9 F4 F6 F4 2F D1 7F E9 8C AB B6 1C 38 7B
|
||||
--------------
|
||||
接收包id=CaptchaPacket(00 BA),
|
||||
sequence=71 6B
|
||||
密文body=92 74 E1 41 8D DE AA 26 5B 3E 8F 91 E0 DC 41 DD 39 EF 1E 2F FD 0E 19 41 B2 AD 8F A1 8D 5D C7 D4 D5 36 2B 51 E5 05 91 9F EE 89 82 B0 C6 B7 EC BE 40 8A 7E 06 FC 82 FA 39 AC 54 94 27 26 8D 71 46 1A F2 BB 23 A8 7E 15 69 8A C7 00 D1 52 06 2C 8B
|
||||
解密body=解密失败
|
||||
|
||||
*/
|
||||
|
||||
else -> error("Unable to analyze RequestCaptchaTransmissionPacket, unknown id: $flag")
|
||||
}
|
||||
}
|
||||
/*
|
||||
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()
|
||||
ServerCaptchaTransmissionResponsePacket(data.toReadPacket(), data.size, "00 BA 31 01".hexToBytes()).let {
|
||||
it.dataDecode()
|
||||
println(it.toString())
|
||||
}
|
||||
}*/
|
@ -1,42 +0,0 @@
|
||||
@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
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.writeFully
|
||||
import net.mamoe.mirai.event.Subscribable
|
||||
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.writeHex
|
||||
import net.mamoe.mirai.utils.io.writeQQ
|
||||
|
||||
@NoLog
|
||||
internal object HeartbeatPacket : SessionPacketFactory<HeartbeatPacketResponse>() {
|
||||
operator fun invoke(
|
||||
bot: Long,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildOutgoingPacket {
|
||||
writeQQ(bot)
|
||||
writeFully(TIMProtocol.fixVer)
|
||||
encryptAndWrite(sessionKey) {
|
||||
writeHex("00 01 00 01")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): HeartbeatPacketResponse =
|
||||
HeartbeatPacketResponse
|
||||
}
|
||||
|
||||
@NoLog
|
||||
internal object HeartbeatPacketResponse : Packet, Subscribable
|
@ -1,376 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.data.Gender
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.data.LoginResult
|
||||
import net.mamoe.mirai.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketFactory
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketId
|
||||
import net.mamoe.mirai.timpc.network.packet.buildOutgoingPacket
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.cryptor.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
|
||||
internal object ShareKey : DecrypterByteArray,
|
||||
DecrypterType<ShareKey> {
|
||||
override val value: ByteArray = TIMProtocol.shareKey
|
||||
}
|
||||
|
||||
internal inline class PrivateKey(override val value: ByteArray) : DecrypterByteArray {
|
||||
companion object Type : DecrypterType<PrivateKey>
|
||||
}
|
||||
|
||||
internal inline class SubmitPasswordResponseDecrypter(private val privateKey: PrivateKey) : Decrypter {
|
||||
override fun decrypt(input: ByteReadPacket, offset: Int, length: Int): ByteReadPacket {
|
||||
var decrypted = ShareKey.decrypt(input, offset, length)
|
||||
(decrypted.remaining).let {
|
||||
if (it.toInt() % 8 == 0 && it >= 16) {
|
||||
decrypted = try {
|
||||
privateKey.decrypt(decrypted)
|
||||
} catch (e: Exception) {
|
||||
// 某些情况不需要这次解密
|
||||
decrypted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decrypted
|
||||
}
|
||||
|
||||
companion object Type : DecrypterType<SubmitPasswordResponseDecrypter>
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交密码
|
||||
*/
|
||||
internal object SubmitPasswordPacket : PacketFactory<SubmitPasswordPacket.LoginResponse, SubmitPasswordResponseDecrypter>(SubmitPasswordResponseDecrypter) {
|
||||
operator fun invoke(
|
||||
bot: Long,
|
||||
passwordMd5: ByteArray,
|
||||
loginTime: Int,
|
||||
loginIP: String,
|
||||
privateKey: PrivateKey,
|
||||
token0825: ByteArray,
|
||||
token00BA: ByteArray? = null,
|
||||
randomDeviceName: Boolean = false,
|
||||
tlv0006: IoBuffer? = null
|
||||
): OutgoingPacket = buildOutgoingPacket {
|
||||
writeQQ(bot)
|
||||
writeFully(TIMProtocol.passwordSubmissionTLV1)
|
||||
writeShort(25); writeFully(TIMProtocol.publicKey)//=25
|
||||
writeZero(2)
|
||||
writeShort(16); writeFully(TIMProtocol.key0836)//=16
|
||||
|
||||
// shareKey 极大可能为 publicKey, key0836 计算得到
|
||||
encryptAndWrite(TIMProtocol.shareKey) {
|
||||
writePart1(bot, passwordMd5, loginTime, loginIP, privateKey, token0825, randomDeviceName, tlv0006)
|
||||
if (token00BA != null) {
|
||||
writeHex("01 10")
|
||||
writeHex("00 3C")
|
||||
writeHex("00 01")
|
||||
writeHex("00 38"); writeFully(token00BA)
|
||||
}
|
||||
writePart2()
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LoginResponse : Packet {
|
||||
class KeyExchange(
|
||||
val tlv0006: IoBuffer,//120bytes
|
||||
val tokenUnknown: ByteArray?,
|
||||
val privateKeyUpdate: PrivateKey//16bytes
|
||||
) : LoginResponse() {
|
||||
override fun toString(): String = "LoginResponse.KeyExchange"
|
||||
}
|
||||
|
||||
class CaptchaInit(
|
||||
val captchaPart1: IoBuffer,
|
||||
val token00BA: ByteArray
|
||||
) : LoginResponse() {
|
||||
override fun toString(): String = "LoginResponse.CaptchaInit"
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
class Success(
|
||||
val sessionResponseDecryptionKey: SessionResponseDecryptionKey,
|
||||
|
||||
val token38: IoBuffer,//56
|
||||
val token88: IoBuffer,//136
|
||||
val encryptionKey: IoBuffer,//16
|
||||
|
||||
val nickname: String,
|
||||
val age: Short,
|
||||
val gender: Gender
|
||||
) : LoginResponse() {
|
||||
override fun toString(): String = "LoginResponse.Success"
|
||||
}
|
||||
|
||||
data class Failed(val result: LoginResult) : LoginResponse()
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): LoginResponse {
|
||||
val size = remaining.toInt()
|
||||
return when {
|
||||
size == 229 || size == 271 || size == 207 || size == 165 /* TODO CHECK 165 */ -> {
|
||||
discardExact(5)//01 00 1E 00 10
|
||||
val privateKeyUpdate = PrivateKey(readBytes(0x10))
|
||||
discardExact(4)//00 06 00 78
|
||||
val tlv0006 = readIoBuffer(0x78)
|
||||
|
||||
return try {
|
||||
discardExact(8)//01 10 00 3C 00 01 00 38
|
||||
LoginResponse.KeyExchange(tlv0006, readBytes(56), privateKeyUpdate)
|
||||
} catch (e: EOFException) {
|
||||
//什么都不做. 因为有的包就是没有这个数据.
|
||||
LoginResponse.KeyExchange(tlv0006, null, privateKeyUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
size == 844 || size == 871 || size == 852 -> {
|
||||
/*
|
||||
FB 01 04 03 3B 00 01 00 BA 02 03 34 13
|
||||
00 05 01 00 00 01 23
|
||||
00 40 AA F6 23 CF 12 15 32 BE 21 5C 8D 43 7B BA 10 BD D8 8B 4B 23 54 7F C5 1C C2 34 51 84 B0 9E 86 8C 30 26 97 B3 26 A8 23 C2 15 72 26 C7 52 88 BA 56 C8 A3 C5 3E C4 DC B3 5A 96 DE 8F A8 72 AB 9D 00 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 FD F7 ED D8 FF E9 EE F6 E7 74 9A 81 F8 F1 E7 EF FE EE 19 84 53 FF F2 EC F6 FC EF FF FE F3 E7 FF EF BE E7 CE 35 6D 4D 4A 92 6D 1A 73 48 54 77 5F EA EB DE E1 F5 E2 88 B2 97 B1 C7 B3 D3 EE D8 8D C5 A6 9E D8 B8 92 93 00 F8 00 00 09 FE 49 44 41 54 58 C3 B4 58 0B 97 AB BC 0D B4 2D 8C DF F8 81 CD FF FF A9 1D 99 6C 12 48 D2 9E EF B4 CD BD BB 09 2C C1 62 34 1A 8D 2C C4 7F F9 32 FC 7A FB 88 CF D6 5A 5D B5 B6 CB 82 73 38 BF F0 07 61 F0 B6 18 F1 BF 7F BD 45 70 1E 60 29 44 80 28 9C 58 F8 60 31 8E 5F 88 85 7F CF 60 FE 0F 31 F0 9B FD 3B 70 7C 50 0A 3F 3A 1F CC 25 97 E7 4B B8 5F B7 39 7F 96 7F F8 5B 3C 1F DF 3E 83 40 14 62 3E 35 7F 30 C8 09 80 C0 D9 F3 D4 C2 DF 30 E2 FA F3 1E C1 3F 7E FE F9 03 C8 ED F3 14 3E 62 C1 45 B9 13 84 A5 28 8D 10 9C B3 8A 5F C2 3D 50 B9 07 F1 F8 70 BE 99 E7 DF 7F FD BE 5E 83 55 17 AB FE A0 50 08 07 A9 5F B0 A8 98 FF B5 D6 33 FF 4C 0E D0 F3 EF 9B E6 13 86 DB C3 19 F1 BC E8 F3 B7 B8 9D 79 A0 F0 80 1D F1 20 DF AE 54 ED 6B F7 5C 19 96 09 A9 A5 94 DE FE 64 A3 B9 FD FB CF D8 5F AE 51 1C C1 5B 39 5A 65 C5 4E 39 C7 98 5A F0 7C 2C 5C C1 89 48 7B F9 B6 E0 CF 95 CC 83 74 E2 0E C9 3D 70 DC 5F B8 59 7D 78 03 E7 8A D6 4A ED 39 87 75 DD 56 0A B6 58 05 2A D0 9E 68 34 A6 C5 F2 E5 A1 C4 25 1A F3 B1 92 B8 A5 E1 1E 3D 6E EA 18 02 67 16 53 CA E2 90 73 D5 72 4A DB 8A 17 75 07 50 16 4B 14 8F 41 60 89 B3 E2 C2 A4 AF 95 F9 FA BB F9 75 D5 05 05 16 20 66 C2 AC 79 2C 62 AB D5 8D 41 C0 6B 8B A4 95 C5 C2 14 37 79 EC 8C AB 7B DD E5 4A C7 0B 05 C5 FD C0 FC 7D C5 DC 03 E1 8F 9C 86 99 02 DC 1D 49 51 4A EB 41 72 3D 63 C8 01 35 61 1C 6D EB 31 76 14 AA 5B BE B3 CA FC 40 E3 72 95 B9 9D 78 86 8A 1A 04 E9 3A CA AE 70 2D 2C 4E F9 01 14 90 88 0D AF E6 2B 28 DB B6 35 A5 9D E5 52 5D 51 B8 05 F4 F6 B8 D7 1A FC 08 E4 FD CB 78 30 94 7F 6D 84 20 0A 1E D8 38 EB 47 6C A9 8D 96 10 42 92 1A C8 0C A0 90 0E BE DA DE F3 F9 3B D7 5F 84 CA 5C C1 79 1C 2C 82 9F 5B E6 46 B9 39 00 61 0C 87 40 BB B4 75 27 E4 00 01 00 28 F4 99 45 7E B9 5C 79 76 6C EE E1 E3 F9 E9 CA C8 07 1A D1 93 40 BC 90 6A C3 05 E9 D6 B5 FF BD AE E9 33 01 0D 29 9D 6B 03 01 15 00 10 5F 09 4A F0 F5 43 58 F1 ED 1C C2 09 AB CB 1A FA
|
||||
*/
|
||||
|
||||
/*
|
||||
FB 01 04 03 3B 00 01 00 BA 02 03 34 13
|
||||
05 01 00 00 01 23
|
||||
|
||||
[00 40] E7 C7 8D 04 D4 37 E7 37 4E BD 68 6B CF DA EA FB 8B FD BB 95 90 FF 36 61 43 64 78 00 0D 07 EB F5 00 AC 1A 21 A9 5D 1F D1 3A 04 89 D0 18 49 CF D1 6B B6 F2 A2 A4 6B A2 3C 2C 8C 5E 7F 1A 94 37 D4 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 3C 50 4C 54 45 FF F7 ED 8D 70 44 D8 BD 96 9C 73 37 FD FA F7 FB F2 E4 FB EE DA FB F8 EC FD F5 F2 FF FA DF FF F5 CB FD E9 CB EA D4 B5 AB 93 6F 9C 80 59 80 60 2C F8 E2 BF 7F 66 42 C2 A6 7E B9 94 5F D7 79 28 98 00 00 0A D2 49 44 41 54 58 C3 8C 98 8D 82 AB A8 12 84 11 05 E4 47 44 F3 FE EF BA 5F B5 49 26 66 E6 DE 5D 33 67 E2 99 18 68 AA AB AB 0B 9C BB 5F C9 AE 75 59 97 45 6F 8B 6E F4 4B 2F 3E 08 D1 FD CB B5 DE 2E F7 5F AE 74 7F A5 7C 05 B1 24 4D AD 9B 85 91 74 BB A6 10 F8 F4 D7 37 6E 2F 42 70 F7 00 D2 BF BE 5E 71 BC 7E 3F 51 E0 62 C9 0A 23 84 F0 FA 6B FA 0C FC CF DF 16 C3 15 C4 F3 C1 F0 1A F7 FF FE B3 48 5E BF 9E 73 B1 70 46 62 84 C8 95 73 8E 35 3F 43 F9 79 D4 FD 75 AF 2F AD AF 9C 06 97 3F 9F FA FB FD B5 80 F7 FF C9 C4 CF DF 43 4E 9A BD 56 8B 22 06 71 E4 E7 9B 1F F3 3E 87 B8 62 48 17 6A D9 91 B7 90 93 FB 78 FA FD A0 FB FC F3 2B 1F 6F 44 5E 01 04 2D 7F 34 7F 9E DB E9 5B 25 88 10 60 C8 FA 43 21 E7 BE 62 49 EF 94 89 B9 C9 22 B8 CF F1 BF 50 70 BF FE 1F 9C 06 C8 D5 6F C7 64 57 DF 7C CD 49 30 7C 51 E9 FD F6 0A 4C 40 5D 20 08 86 94 3E 10 FA 8B 07 2E DD 91 78 DE 28 E9 21 E6 E8 FB 7E 6C 9B F7 FE DC 88 82 18 E0 E7 9A BE BE FD 99 47 BD AD E9 8A C1 C1 83 9C 7E 62 48 7F 54 C7 7B D9 77 62 2E 41 20 90 80 E6 FB B4 F9 12 03 29 18 7E EB C7 06 0E F9 4E 82 F4 89 A9 BE 1B AE 4A FE 11 89 DB F4 9F 85 E3 FE 22 EA 33 A0 B0 B8 48 04 CC D9 7D 29 4C 9B 44 8A E2 B7 69 1B F0 E1 0B 44 F7 83 34 91 8B 00 56 C9 D1 AA 51 82 F2 09 D8 5F B1 BB 5B 4C AF 34 C4 11 5B 1B DB B1 B5 52 0A 39 A5 18 A8 0A DF 0F CF 6D FA CC DE 57 10 FA B7 10 8A AE 48 0C 92 D8 F5 F7 A4 1F F0 A7 7B 1A AF 0F 48 44 D4 F7 FD 24 10 4A 69 44 A3 92 84 1A 53 6F 39 7F 32 EB 96 07 C7 8A B9 62 4A AA 61 2A 98 C5 90 88 F5 9B AF BF 2B DA DD 0B 86 2F 2E 71 01 D0 EA 59 73 3B 7B 9F 8E 7E 52 93 31 57 70 29 39 DD 34 F1 67 7C 42 07 7F A4 80 32 A2 82 7A AB 11 31 5B 17 77 A3 FD 17 27 BF 63 7A CA 42 72 8D 7A 08 D2 A4 76 6E F3 3C EF 3B B4 04 85 EC 8F 3E 4A FA 35 CA 00 01 00 28 83 4D 03 6C A3 41 E3 77 42 EC BD 53 16 A0 60 C8 32 CB 8C 39 2B 61 87 E3 50 79 E1 BD 5A 5D AF C0 A1 B7 27 83 FE CE CD 5B 01 15 00 10 B0 50 A7 C9 03 2F EC 2F 05 22 EC 27 32 93 FF 74
|
||||
*/
|
||||
|
||||
/*
|
||||
FB 01 04 03 3B 00 01 00 BA 02 03 34 13
|
||||
00 05 01 00 00 01 23
|
||||
[00 40] 67 42 E8 E5 08 2D D2 87 83 DB A6 D3 56 51 6F 43 A5 DB 67 CD 31 24 DE 2D AF 5A D2 13 F6 5D 7B D1 26 55 61 DB 95 80 C6 B1 74 66 DB C2 8C EC 71 0E DA 74 D0 6D 80 BB 88 B5 12 6A 30 24 DB 65 95 1C
|
||||
[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 3C 50 4C 54 45 FF F2 EC 55 96 73 F2 EF E4 79 AD 8F EC FD EC DC FF EB 23 70 4A FC FE F3 FD F7 ED F5 FA EC 30 84 5B AC E1 C3 12 78 49 CC E4 D0 99 C3 A9 12 87 53 E2 EF DE 49 7D 5F 3A 6F 51 BF F9 DA 26 D7 2D 0B 00 00 0B 58 49 44 41 54 58 C3 8C 98 8B 82 AC AA 0E 44 45 40 44 84 46 FD FF 7F BD AB E2 A3 9D D9 E7 EC 73 ED 99 7E 22 14 49 A5 92 30 CC AF 6B B8 AE 39 85 9E C6 79 98 E7 31 DB 47 FB 32 E7 FC 1A F3 EB E2 97 C4 08 EE D4 E0 39 C4 A0 DB E7 F3 E9 EF D7 60 8F FB 0D 57 D2 73 4E 29 0B C2 90 C2 9C C6 91 E9 13 83 5E 10 E6 5F 60 00 AB 41 F3 1C 42 18 46 20 84 CC ED FC 0F 86 E2 AF 7F 0F D4 E1 B1 03 33 64 4D C1 87 31 85 C0 BE 78 EA FA 35 7F 77 35 BC 6E 98 CF DF F4 48 73 C8 4C 30 84 70 CD 3A CC FF 17 88 97 1D EC 0F 13 84 64 D6 98 53 BC AE 30 0F BF 0C 9B ED 63 96 6D 6C F1 64 96 10 04 46 8E 32 82 59 E1 3F 11 7C 5F 6E 00 5A 7A 1E D9 85 AC 91 62 F1 CE 3B E7 5B B4 99 AF 89 CF C7 F5 74 5F 27 88 31 5F 86 31 F2 CC B7 8D 6E 9B FD D3 F3 30 BC B6 AF 5D C9 03 A2 A3 FD 34 7A 37 4D DB 3E AD CB F1 86 F0 45 C0 D2 E1 04 60 5F 25 BB 66 B1 67 30 42 9C 10 86 BF 81 F8 E9 02 99 37 40 44 D8 18 C4 C0 A1 EF D3 E2 4B F1 9F 4F 0B 27 84 6B F9 EF B3 AD 6F 2C D0 C2 63 EF A3 BE 91 25 C2 13 16 7F C7 F0 F5 C2 69 E2 1C 7A 6D AD 86 30 12 8A 29 6D 8B 3B 62 28 EB EA 44 F4 1C 6E 0E DC E3 BF 1F 65 BB D4 9B F7 BA D9 BC 19 60 50 7E B3 67 78 56 FB 15 08 F9 5C 1F 47 E6 34 A6 CA 14 B5 6D 25 E4 91 38 EC 7E 5A 4A 0A 71 59 97 13 42 1A 1F 07 26 D3 8D 71 B8 A2 93 7B C7 06 91 53 DD 62 24 A2 47 63 32 D3 88 5E 63 FA 97 BD 9B 12 E4 13 D2 29 2E 39 77 1C 98 FA E6 C5 47 AC 50 96 A9 85 1C DD BA 54 8D 65 C6 D1 08 7B 5E B8 FD 8E 1C 01 1A D9 7F 08 7D 8F 11 A4 A9 BA 69 F7 85 D0 32 5D 19 7F EE FD E7 4B 1E 6E 4D CA 29 57 D8 34 A6 E4 F6 68 1B 66 FF 93 47 1C CA 34 79 AC C5 00 91 CD E4 EB 21 BB 40 49 0A D2 5C 2B 91 D4 EB 04 04 6E F5 FB B4 EF DB 56 81 A0 9B E6 F9 A7 02 BD 22 22 DF 02 23 7D 1D 5B E8 62 63 DB 0B D3 A0 33 C1 4F AE A7 1C B7 69 8B A3 06 58
|
||||
00
|
||||
01
|
||||
[00 28] AD 53 81 65 DB 7D 7B 1E F4 AC 69 28 90 35 23 F3 0F DF AF 48 66 D9 06 13 0F AE 57 3C 5D AF CB 96 6C 5C CD 95 3F 2F 50 C9
|
||||
01
|
||||
15
|
||||
[00 10] A8 5D 2B 4F 33 AF 5D 99 B1 EF 92 DA C6 E5 A9 FB
|
||||
*/
|
||||
discardExact(20) // FB 01 04 03 3B 00 01 00 BA 02 03 34 13 00 05 01 00 00 01 23
|
||||
discardExact(readUShort()) // size=00 40, 64
|
||||
val captchaPart1 = readIoBuffer(readUShort().toInt()) // size=02 BC, 700
|
||||
|
||||
discardExact(2)//00 01
|
||||
|
||||
val token00BA = readBytes(readUShort().toInt()) // size=00 28, 40
|
||||
|
||||
/*
|
||||
剩余
|
||||
01
|
||||
15
|
||||
[00 10] A8 5D 2B 4F 33 AF 5D 99 B1 EF 92 DA C6 E5 A9 FB
|
||||
*/
|
||||
return LoginResponse.CaptchaInit(captchaPart1, token00BA)
|
||||
}
|
||||
size > 650 -> {
|
||||
|
||||
/*
|
||||
00 01 09 00 70 00 01 C4 20 CB 84 35 17 3F 43 FC 06 63 D9 49 5B 3C AC 00 38 12 9E 18 DC 47 41 FC EF 0F EA FC AD 22 88 82 17 C0 52 84 63 9B 0C 1E E9 28 AE 78 CC 0A D3 FE BE 46 4A 59 CE 64 07 81 A6 9E AC E6 31 4C 23 A9 3E C2 20 84 54 05 92 8E E9 00 20 B5 9E 51 9C C4 FD 2F E1 00 8B F7 2B CE 1B C8 DA F0 7D 62 DC 5A CA FE AF 8C 54 92 A8 58 9E F5 91 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 01 10 00 3C 00 01 00 38 1A F8 64 61 13 97 89 C1 64 E9 B9 97 A1 2F CE D6 91 5B D2 3A 60 D2 B7 F2 38 35 57 0C 24 51 18 FC 02 EA C6 E9 E8 B9 CB B3 35 97 8F 6E A1 CE 53 22 9E B5 2C 31 36 C6 3C C1 01 07 01 D3 00 01 00 16 00 00 00 01 00 00 00 64 00 00 0D FD 00 09 3A 80 00 00 00 3C 00 02 48 60 3F 44 54 39 70 44 24 62 2A 53 6E 71 72 34 00 88 00 04 5D E7 BE 55 AB 53 02 17 00 00 00 00 00 78 A9 44 3A 18 15 0F 3F 52 57 0B 6C C8 34 6B B6 B1 A6 B0 B5 9D 74 4D BD 52 88 DD E4 A1 F2 EC 3E 49 3B 05 B4 F5 46 2B 8A 2D 7D AE E6 91 66 DD A3 78 5C AF 7D 5A 65 AA AD 6C CD 65 55 49 4E 07 FE 3A AD 76 75 21 DC AF 92 48 AA 48 22 29 B4 D3 6A A5 D1 D5 EB 62 A8 17 6C E3 FA CB D6 BB BE CE 7F F4 4E 18 B4 BF 76 3D 9B AF CB A4 89 1A CC E8 B5 07 54 E2 6A 59 CE 0F 20 74 4B 60 6D 5A 49 24 5B 27 46 38 77 66 59 2B 46 7D 00 78 00 01 5D E7 BE 55 00 70 B9 A4 D6 DB CF AF C3 CA 04 98 22 60 B1 B5 9C 55 06 F1 B6 D8 CF 63 20 1E 81 90 DA 29 44 79 F0 13 65 3B 2B 83 B8 D7 93 D7 DF 05 71 19 5B 25 68 EA DD 9B 01 E0 F0 5F 7A 79 CF C6 35 A7 AC 14 D7 AF 1A 5D AF 72 D2 25 57 36 E0 DE 9B 0D A8 B1 62 78 3D 9F DE D6 0C 37 7F B7 AC 94 40 A7 0D A9 A2 71 AB E0 C2 EE 10 CA 67 59 C8 57 F4 36 2C 77 79 98 00 83 01 3D 77 5E 58 59 47 5F 3E 77 4B 45 4A 2A 2A 5A 6A 00 70 00 01 5D E7 BE 55 00 68 7A 22 3E 6F 09 F1 37 5F 95 62 FF 06 BB 6D C4 77 92 4C 16 23 65 8A FF 38 F2 7A A6 91 10 AB B6 3B 14 30 C6 AC 58 59 7B E8 3F B2 97 EA 63 99 B9 6E DC F5 2A D4 24 0B 38 6F 67 75 D5 BF FE 74 0B A0 E5 8A 64 10 41 EF 86 24 07 81 75 2E B3 BE EE A4 AD B1 91 37 BE 6B 80 43 AF D9 0F 73 1F B4 7B 82 CF 07 12 C6 41 39 B9 E8 53 70 42 51 5F 52 28 64 29 4E 4B 2D 77 32 29 52 01 08 00 23 00 01 00 1F 00 17 02 5B 0E 73 74 65 61 6D 63 68 69 6E 61 2E 66 75 6E 01 13 80 02 00 06 00 04 00 00 00 01 01 15 00 10 04 19 C6 27 44 A7 B7 34 EF 1C 45 67 78 1F CD 18
|
||||
*/
|
||||
|
||||
discardExact(7)//00 01 09 00 70 00 01
|
||||
//FB 01 04 03 33
|
||||
val encryptionKey = readIoBuffer(16)//C6 72 C7 73 70 01 46 A2 11 88 AC E4 92 7B BF 90
|
||||
|
||||
discardExact(2)//00 38
|
||||
val 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
|
||||
|
||||
val flagFront = readUByte().toUInt()
|
||||
val flagBack = readUByte().toUInt()
|
||||
discardExact(
|
||||
when (flagFront) {
|
||||
0x00u -> when (flagBack) {
|
||||
0x33u -> 28
|
||||
else -> null
|
||||
}
|
||||
0x01u -> when (flagBack) {
|
||||
0x07u -> 0
|
||||
0x10u -> 64
|
||||
else -> null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
?: error("Unknown length flag: 0x" + flagFront.toUByte().toUHexString() + flagBack.toUByte().toUHexString() + ", remaining packet = ${readBytes().toUHexString()}")
|
||||
)
|
||||
|
||||
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
|
||||
val sessionResponseDecryptionKey = SessionResponseDecryptionKey(readIoBuffer(16))
|
||||
discardExact(2)
|
||||
val 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()
|
||||
val 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
|
||||
val age = readShort()//00 05
|
||||
|
||||
discardExact(4)//00 04 00 00
|
||||
|
||||
discardExact(2)//00 01
|
||||
val gender = if (readBoolean()) Gender.FEMALE else Gender.MALE
|
||||
|
||||
return LoginResponse.Success(sessionResponseDecryptionKey, token38, token88, encryptionKey, nickname, age, gender)
|
||||
}
|
||||
|
||||
else -> LoginResponse.Failed(when (size) {
|
||||
135 -> {//包数据错误. 目前怀疑是 tlv0006
|
||||
this.readRemainingBytes().cutTail(1).decryptBy(TIMProtocol.shareKey).read {
|
||||
discardExact(51)
|
||||
MiraiLogger.error("Internal error: " + readUShortLVString())//抱歉,请重新输入密码。
|
||||
}
|
||||
|
||||
LoginResult.INTERNAL_ERROR
|
||||
}
|
||||
|
||||
246 -> LoginResult.PROTECTED
|
||||
240, 319, 320, 351 -> LoginResult.WRONG_PASSWORD
|
||||
//135 -> LoginState.RETYPE_PASSWORD
|
||||
63 -> LoginResult.BLOCKED
|
||||
263 -> LoginResult.UNKNOWN_QQ_NUMBER
|
||||
279, 495, 551, 487 -> LoginResult.DEVICE_LOCK
|
||||
343, 359 -> LoginResult.TAKEN_BACK
|
||||
|
||||
// 246: 33 05 08 00 22 01 00 00 03 E8 00 1B 02 00 00 00 01 00 00 15 85 08 36 00 00 00 33 00 00 00 00 76 E7 50 ED 00 00 01 26 01 00 00 B7 00 01 08 36 00 00 01 26 00 AD E8 AF A5 E5 8F B7 E7 A0 81 E9 95 BF E6 9C 9F E6 9C AA E7 99 BB E5 BD 95 EF BC 8C E4 B8 BA E4 BA 86 E4 BF 9D E9 9A 9C E5 B8 90 E5 8F B7 E5 AE 89 E5 85 A8 EF BC 8C E5 B7 B2 E8 A2 AB E7 B3 BB E7 BB 9F E8 AE BE E7 BD AE E6 88 90 E4 BF 9D E6 8A A4 E7 8A B6 E6 80 81 EF BC 8C E8 AF B7 E7 94 A8 E6 89 8B E6 9C BA 51 51 E6 9C 80 E6 96 B0 E7 89 88 E6 9C AC E7 99 BB E5 BD 95 EF BC 8C E7 99 BB E5 BD 95 E6 88 90 E5 8A 9F E5 90 8E E5 8D B3 E5 8F AF E8 87 AA E5 8A A8 E8 A7 A3 E9 99 A4 E4 BF 9D E6 8A A4 E7 8A B6 E6 80 81 E3 80 82 01 15 00 10 26 F9 4C F4 F0 CA 6C 53 98 77 54 2B BD CD 40 66
|
||||
|
||||
// 165: 01 00 1E 00 10 72 36 7B 6B 6D 78 3A 4B 63 7B 47 5B 68 3E 21 59 00 06 00 78 34 F6 F9 49 AA 13 F5 F5 01 36 13 E1 4C F7 0F 25 C1 2C 10 75 CA 69 E9 12 B3 6D F4 A7 59 60 FF 01 03 73 28 47 A3 2A B8 46 C3 92 24 D5 8A AE 8B C2 45 0C 31 27 B5 17 9E 22 13 59 AF B4 CC F6 E3 3A 91 60 13 21 11 3C 25 D9 50 F4 23 C6 06 1D F4 15 41 BA 5D 7B 66 26 96 EB 0E 04 14 8E 5B D4 33 6E B8 5D E7 10 3A 0E EF 96 B1 D4 22 E4 74 48 A7 1D 3A 46 7D E6 EF 1F 6B 69 01 15 00 10 6F 99 48 5E 98 AE D3 4B F8 35 63 1D 70 EE 6D 82
|
||||
|
||||
else -> {
|
||||
MiraiLogger.error("login response packet size = $size, data=${this.readRemainingBytes().toUHexString()}")
|
||||
LoginResult.UNKNOWN
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inline class SessionResponseDecryptionKey(private val delegate: IoBuffer) : Decrypter {
|
||||
override fun decrypt(input: ByteReadPacket, offset: Int, length: Int): ByteReadPacket = input.decryptBy(delegate, offset, length)
|
||||
|
||||
override fun toString(): String = "SessionResponseDecryptionKey"
|
||||
|
||||
companion object Type : DecrypterType<SessionResponseDecryptionKey>
|
||||
}
|
||||
|
||||
private fun BytePacketBuilder.writePart1(
|
||||
qq: Long,
|
||||
password: ByteArray,
|
||||
loginTime: Int,
|
||||
loginIP: String,
|
||||
privateKey: PrivateKey,
|
||||
token0825: ByteArray,
|
||||
randomDeviceName: Boolean,
|
||||
tlv0006: IoBuffer? = null
|
||||
) {
|
||||
//this.writeInt(System.currentTimeMillis().toInt())
|
||||
this.writeHex("01 12")//tag
|
||||
this.writeHex("00 38")//length
|
||||
this.writeFully(token0825)//length
|
||||
this.writeHex("03 0F")//tag
|
||||
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.writeFully(tlv0006)
|
||||
} else {
|
||||
this.writeTLV0006(qq, password, loginTime, loginIP, privateKey)
|
||||
}
|
||||
//fix
|
||||
this.writeFully(TIMProtocol.passwordSubmissionTLV2)
|
||||
this.writeHex("00 1A")//tag
|
||||
this.writeHex("00 40")//length
|
||||
this.writeFully(TIMProtocol.passwordSubmissionTLV2.encryptBy(privateKey.value))
|
||||
this.writeFully(TIMProtocol.constantData1)
|
||||
this.writeFully(TIMProtocol.constantData2)
|
||||
this.writeQQ(qq)
|
||||
this.writeZero(4)
|
||||
|
||||
this.writeHex("01 03")//tag
|
||||
this.writeHex("00 14")//length
|
||||
|
||||
this.writeHex("00 01")//tag
|
||||
this.writeHex("00 10")//length
|
||||
this.writeHex("60 C9 5D A7 45 70 04 7F 21 7D 84 50 5C 66 A5 C6")//key
|
||||
}
|
||||
|
||||
private fun BytePacketBuilder.writeTLV0006(qq: Long, passwordMd5: ByteArray, loginTime: Int, loginIP: String, privateKey: PrivateKey) {
|
||||
val secondMD5 = md5(passwordMd5 + byteArrayOf(0, 0, 0, 0) + qq.toUInt().toByteArray())
|
||||
|
||||
this.encryptAndWrite(secondMD5) {
|
||||
writeRandom(4)
|
||||
writeHex("00 02")
|
||||
writeQQ(qq)
|
||||
writeFully(TIMProtocol.constantData2)
|
||||
writeHex("00 00 01")
|
||||
|
||||
writeFully(passwordMd5)
|
||||
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.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BytePacketBuilder.writePart2() {
|
||||
|
||||
this.writeHex("03 12")//tag
|
||||
this.writeHex("00 05")//length
|
||||
this.writeHex("01 00 00 00 01")//value
|
||||
|
||||
this.writeHex("05 08")//tag
|
||||
this.writeHex("00 05")//length
|
||||
this.writeHex("01 00 00 00 00")//value
|
||||
|
||||
this.writeHex("03 13")//tag
|
||||
this.writeHex("00 19")//length
|
||||
this.writeHex("01")//value
|
||||
|
||||
this.writeHex("01 02")//tag
|
||||
this.writeHex("00 10")//length
|
||||
this.writeHex("04 EA 78 D1 A4 FF CD CC 7C B8 D4 12 7D BB 03 AA")//key
|
||||
this.writeZero(3)
|
||||
this.writeByte(0)//maybe 00, 0F, 1F
|
||||
|
||||
this.writeHex("01 02")//tag
|
||||
this.writeHex("00 62")//length
|
||||
this.writeHex("00 01")//word?
|
||||
this.writeHex("04 EB B7 C1 86 F9 08 96 ED 56 84 AB 50 85 2E 48")//key
|
||||
this.writeHex("00 38")//length
|
||||
//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")
|
||||
this.writeCRC32()
|
||||
}
|
||||
|
||||
internal fun BytePacketBuilder.writeCRC32() = writeCRC32(getRandomByteArray(16))
|
||||
|
||||
internal fun BytePacketBuilder.writeCRC32(key: ByteArray) {
|
||||
writeFully(key)//key
|
||||
writeInt(crc32(key))
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE", "FunctionName")
|
||||
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.writeFully
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.data.Packet
|
||||
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.*
|
||||
|
||||
internal inline class SKey(
|
||||
val value: String
|
||||
) : Packet
|
||||
|
||||
/**
|
||||
* 请求 `SKey`
|
||||
* SKey 用于 http api
|
||||
*/
|
||||
internal object RequestSKeyPacket : SessionPacketFactory<SKey>() {
|
||||
operator fun invoke(
|
||||
bot: Long,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildOutgoingPacket {
|
||||
writeQQ(bot)
|
||||
writeFully(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")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): SKey {
|
||||
//11 00 97 D7 0F 1C FD 50 7A 41 DD 4D 66 93 EF 8C 85 D1 84 3D 66 95 9D E5 B4 96 A5 E3 92 37 28 D8 80 DA EF 8C 85 D1 84 3D 66 95 9D E5 B4 96 A5 E3 92 37 28 D8 80 DA
|
||||
|
||||
discardExact(4)
|
||||
return SKey(readString(10)).also {
|
||||
DebugLogger.warning("SKey 包后面${readRemainingBytes().toUHexString()}")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun BotNetworkHandler.handlePacket(packet: SKey) {
|
||||
// _sKey = packet.value
|
||||
// _cookies = "uin=o$qqAccount;skey=$sKey;"
|
||||
|
||||
// TODO: 2019/11/27 SKEY 实现
|
||||
/*
|
||||
if (sKeyRefresherJob?.isActive != true) {
|
||||
sKeyRefresherJob = NetworkScope.launch {
|
||||
while (isOpen) {
|
||||
delay(1800000)
|
||||
try {
|
||||
requestSKey()
|
||||
} catch (e: Throwable) {
|
||||
bot.logger.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketFactory
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketId
|
||||
import net.mamoe.mirai.timpc.network.packet.SessionKey
|
||||
import net.mamoe.mirai.timpc.network.packet.buildOutgoingPacket
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.utils.localIpAddress
|
||||
|
||||
internal object RequestSessionPacket : PacketFactory<RequestSessionPacket.SessionKeyResponse, SessionResponseDecryptionKey>(SessionResponseDecryptionKey) {
|
||||
operator fun invoke(
|
||||
bot: Long,
|
||||
serverIp: String,
|
||||
token38: IoBuffer,
|
||||
token88: IoBuffer,
|
||||
encryptionKey: IoBuffer
|
||||
) = buildOutgoingPacket {
|
||||
writeQQ(bot)
|
||||
writeHex("02 00 00 00 01 2E 01 00 00 68 52 00 30 00 3A")
|
||||
writeHex("00 38")
|
||||
writeFully(token38)
|
||||
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")
|
||||
writeFully(TIMProtocol.constantData1)
|
||||
writeFully(TIMProtocol.constantData2)
|
||||
writeQQ(bot)
|
||||
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? or that may cause errors?
|
||||
}
|
||||
}
|
||||
|
||||
internal class SessionKeyResponse(
|
||||
val sessionKey: SessionKey,
|
||||
val tlv0105: ByteReadPacket? = null
|
||||
) : Packet {
|
||||
override fun toString(): String = "SessionKeyResponse"
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): SessionKeyResponse {
|
||||
when (remaining) {
|
||||
407L -> {
|
||||
discardExact(25)//todo test
|
||||
return SessionKeyResponse(SessionKey(readBytes(16)))
|
||||
}
|
||||
|
||||
439L -> {
|
||||
discardExact(63)
|
||||
return SessionKeyResponse(SessionKey(readBytes(16)))
|
||||
}
|
||||
|
||||
502L,//?
|
||||
512L,
|
||||
527L -> {
|
||||
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
|
||||
val sessionKey = SessionKey(readBytes(16))
|
||||
val tlv0105 = buildPacket {
|
||||
writeHex("01 05 00 88 00 01 01 02 00 40 02 01 03 3C 01 03 00 00")
|
||||
discardExact(remaining - 122 - 1)
|
||||
writeFully(readIoBuffer(56))
|
||||
writeHex("00 40 02 02 03 3C 01 03 00 00")
|
||||
discardExact(11)
|
||||
writeFully(readIoBuffer(56))
|
||||
} //todo 这个 tlv0105似乎可以保存起来然后下次登录时使用.
|
||||
return SessionKeyResponse(sessionKey, 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(remaining.toString())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readBytes
|
||||
import kotlinx.io.core.writeFully
|
||||
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.OutgoingPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketFactory
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketId
|
||||
import net.mamoe.mirai.timpc.network.packet.buildOutgoingPacket
|
||||
import net.mamoe.mirai.utils.cryptor.DecrypterByteArray
|
||||
import net.mamoe.mirai.utils.cryptor.DecrypterType
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
|
||||
internal object TouchKey : DecrypterByteArray,
|
||||
DecrypterType<TouchKey> {
|
||||
override val value: ByteArray = TIMProtocol.touchKey
|
||||
}
|
||||
|
||||
/**
|
||||
* 与服务器建立连接的第一个包
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
internal object TouchPacket : PacketFactory<TouchPacket.TouchResponse, TouchKey>(TouchKey) {
|
||||
operator fun invoke(
|
||||
bot: Long,
|
||||
serverIp: String,
|
||||
isRedirect: Boolean
|
||||
): OutgoingPacket = buildOutgoingPacket {
|
||||
writeQQ(bot)
|
||||
writeFully(TIMProtocol.fixVer)
|
||||
writeFully(TIMProtocol.touchKey)
|
||||
|
||||
encryptAndWrite(TIMProtocol.touchKey) {
|
||||
writeFully(TIMProtocol.constantData1)
|
||||
writeFully(TIMProtocol.constantData2)
|
||||
writeQQ(bot)
|
||||
writeHex(if (isRedirect) "00 01 00 00 03 09 00 0C 00 01" else "00 00 00 00 03 09 00 08 00 01")
|
||||
writeIP(serverIp)
|
||||
writeHex(
|
||||
if (isRedirect) "01 6F A1 58 22 01 00 36 00 12 00 02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 14 00 1D 01 03 00 19"
|
||||
else "00 02 00 36 00 12 00 02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 14 00 1D 01 02 00 19"
|
||||
)
|
||||
writeFully(TIMProtocol.publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TouchResponse : Packet {
|
||||
class OK(
|
||||
var loginTime: Int,
|
||||
val loginIP: String,
|
||||
val token0825: ByteArray // 56
|
||||
) : TouchResponse() {
|
||||
override fun toString(): String = "TouchResponse.OK"
|
||||
}
|
||||
|
||||
class Redirection(
|
||||
val serverIP: String
|
||||
) : TouchResponse() {
|
||||
override fun toString(): String = "TouchResponse.Redirection"
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): TouchResponse {
|
||||
when (val flag = readByte().toUByte().toInt()) {
|
||||
0xFE -> {
|
||||
discardExact(94)
|
||||
return TouchResponse.Redirection(readIP())
|
||||
}
|
||||
0x00 -> {
|
||||
discardExact(4)
|
||||
val token0825 = readBytes(56)
|
||||
discardExact(6)
|
||||
|
||||
val loginTime = readInt()
|
||||
val loginIP = readIP()
|
||||
return TouchResponse.OK(loginTime, loginIP, token0825)
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException(flag.toByte().toUHexString())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package net.mamoe.mirai.timpc.utils
|
||||
|
||||
/**
|
||||
* 表示这里是不可到达的位置.
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
internal inline fun assertUnreachable(): Nothing = error("This clause should not be reached")
|
@ -1,15 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package net.mamoe.mirai.timpc.utils
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
|
||||
internal fun ByteReadPacket.unsupportedFlag(name: String, flag: String): Nothing =
|
||||
error("Unsupported flag of $name. flag=$flag, remaining=${readBytes().toUHexString()}")
|
||||
|
||||
internal fun ByteReadPacket.unsupportedType(name: String, type: String): Nothing =
|
||||
error("Unsupported type of $name. type=$type, remaining=${readBytes().toUHexString()}")
|
@ -1,8 +0,0 @@
|
||||
package net.mamoe.mirai.timpc.utils
|
||||
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.writeFully
|
||||
import kotlinx.serialization.SerializationStrategy
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
|
||||
fun <T> BytePacketBuilder.writeProto(serializer: SerializationStrategy<T>, obj: T) = writeFully(ProtoBuf.dump(serializer, obj))
|
@ -1,28 +0,0 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.mamoe.mirai.timpc
|
||||
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.io.InputStream
|
||||
import kotlinx.io.core.use
|
||||
import kotlinx.io.streams.inputStream
|
||||
import net.mamoe.mirai.BotAccount
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.utils.BotConfiguration
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.toExternalImage
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
internal actual class TIMPCBot actual constructor(
|
||||
account: BotAccount,
|
||||
configuration: BotConfiguration
|
||||
) : TIMPCBotBase(account, configuration) {
|
||||
suspend inline fun Image.downloadAsStream(): InputStream = download().inputStream()
|
||||
suspend inline fun Image.downloadAsBufferedImage(): BufferedImage = withContext(IO) { downloadAsStream().use { ImageIO.read(it) } }
|
||||
suspend inline fun Image.downloadAsExternalImage(): ExternalImage = download().use { it.toExternalImage() }
|
||||
|
||||
suspend inline fun Image.downloadTo(file: File) = file.outputStream().use { downloadTo(it) }
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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()
|
@ -1,64 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.timpc.TIMPC
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 筛选掉无法登录(冻结/设备锁/UNKNOWN)的 qq
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
|
||||
const val qqList = "" +
|
||||
"3383596103----13978930542\n" +
|
||||
"3342679146----aaaa9899\n" +
|
||||
"1491095272----abc123\n" +
|
||||
"3361065539----aaaa9899\n" +
|
||||
"1077612696----asd123456789\n" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"\n"
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
suspend fun main() {
|
||||
val goodBotList = Collections.synchronizedList(mutableListOf<Bot>())
|
||||
|
||||
withContext(GlobalScope.coroutineContext) {
|
||||
qqList.split("\n")
|
||||
.filterNot { it.isEmpty() }
|
||||
.map { it.split("----") }
|
||||
.map { Pair(it[0].toLong(), it[1]) }
|
||||
.forEach { (qq, password) ->
|
||||
runBlocking {
|
||||
val bot = TIMPC.Bot(
|
||||
qq,
|
||||
if (password.endsWith(".")) password.substring(0, password.length - 1) else password
|
||||
)
|
||||
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
bot.login()
|
||||
}
|
||||
goodBotList.add(bot)
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println("Filtering finished")
|
||||
println(goodBotList.joinToString("\n") { it.uin.toString() + " " + it.account.passwordMd5 })
|
||||
}
|
@ -1,393 +0,0 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "MemberVisibilityCanBePrivate", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
import PacketDebugger.dataReceived
|
||||
import PacketDebugger.dataSent
|
||||
import PacketDebugger.qq
|
||||
import PacketDebugger.sessionKey
|
||||
import io.ktor.util.date.GMTDate
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readBytes
|
||||
import kotlinx.io.core.readUShort
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.internal.ArrayListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.timpc.TIMPC
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.timpc.network.packet.event.IgnoredEventPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.login.CaptchaKey
|
||||
import net.mamoe.mirai.timpc.network.packet.login.HeartbeatPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.login.ShareKey
|
||||
import net.mamoe.mirai.timpc.network.packet.login.TouchKey
|
||||
import net.mamoe.mirai.utils.cryptor.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import org.pcap4j.core.BpfProgram.BpfCompileMode
|
||||
import org.pcap4j.core.PacketListener
|
||||
import org.pcap4j.core.PcapNetworkInterface
|
||||
import org.pcap4j.core.PcapNetworkInterface.PromiscuousMode
|
||||
import org.pcap4j.core.Pcaps
|
||||
import java.io.File
|
||||
import java.nio.charset.Charset
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* 避免 print 重叠. 单线程处理足够调试
|
||||
*/
|
||||
val DISPATCHER = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
|
||||
|
||||
private fun listenDevice(localIp: String, device: PcapNetworkInterface) {
|
||||
val sender = device.openLive(65536, PromiscuousMode.PROMISCUOUS, 10)
|
||||
thread {
|
||||
sender.setFilter("src $localIp && udp port 8000", BpfCompileMode.OPTIMIZE)
|
||||
try {
|
||||
sender.loop(Int.MAX_VALUE, PacketListener {
|
||||
runBlocking {
|
||||
withContext(DISPATCHER) {
|
||||
try {
|
||||
dataSent(it.rawData.drop(42).toByteArray())
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
val receiver = device.openLive(65536, PromiscuousMode.PROMISCUOUS, 10)
|
||||
thread {
|
||||
receiver.setFilter("dst $localIp && udp port 8000", BpfCompileMode.OPTIMIZE)
|
||||
try {
|
||||
receiver.loop(Int.MAX_VALUE, PacketListener {
|
||||
runBlocking {
|
||||
withContext(DISPATCHER) {
|
||||
try {
|
||||
dataReceived(it.rawData.drop(42).toByteArray())
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun File.toRecorder(): Recorder =
|
||||
Recorder(Json.plain.fromJson(ArrayListSerializer(Recorder.Record.serializer()), Json.plain.parseJson(this.readText())).toMutableList())
|
||||
|
||||
internal class Recorder(
|
||||
val list: MutableList<Record> = mutableListOf()
|
||||
) : CoroutineScope {
|
||||
@Serializable
|
||||
data class Record(
|
||||
val isSend: Boolean,
|
||||
@Suppress("ArrayInDataClass")
|
||||
val data: ByteArray
|
||||
)
|
||||
|
||||
fun recordSend(data: ByteArray) {
|
||||
launch { list.add(Record(true, data)) }
|
||||
}
|
||||
|
||||
fun recordReceive(data: ByteArray) {
|
||||
launch { list.add(Record(false, data)) }
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Transient
|
||||
@Transient
|
||||
override val coroutineContext: CoroutineContext = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
internal fun Recorder.writeTo(file: File) {
|
||||
file.writeText(Json.plain.toJson(ArrayListSerializer(Recorder.Record.serializer()), this.list).toString(), Charset.defaultCharset())
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
suspend fun main() {
|
||||
|
||||
fun startPacketListening() {
|
||||
val localIp = Pcaps.findAllDevs()[0].addresses[0].address.hostAddress
|
||||
println("localIp = $localIp")
|
||||
Pcaps.findAllDevs().forEach {
|
||||
listenDevice(localIp, it)
|
||||
}
|
||||
println("Using sessionKey = ${sessionKey.value.toUHexString()}")
|
||||
println("Filter QQ = $qq")
|
||||
PacketDebugger.recorder?.let { println("Recorder is enabled") }
|
||||
Runtime.getRuntime().addShutdownHook(thread(false) {
|
||||
PacketDebugger.recorder?.writeTo(File(GMTDate().toString() + ".record"))?.also { println("${PacketDebugger.recorder.list.size} records saved.") }
|
||||
})
|
||||
println("Ready perfectly")
|
||||
}
|
||||
|
||||
suspend fun decryptRecordedPackets(filename: String?) {
|
||||
(if (filename == null) File(".").listFiles()?.maxBy { it.lastModified() }!!
|
||||
else File(filename)).toRecorder().also {
|
||||
println("total count = " + it.list.size)
|
||||
println()
|
||||
}.list.forEach {
|
||||
if (it.isSend) {
|
||||
try {
|
||||
dataSent(it.data)
|
||||
} catch (e: Exception) {
|
||||
//e.printStackTrace()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
dataReceived(it.data)
|
||||
} catch (e: Exception) {
|
||||
// e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//decryptRecordedPackets(null)
|
||||
startPacketListening()
|
||||
}
|
||||
|
||||
/**
|
||||
* 抓包分析器.
|
||||
* 设置好 [sessionKey], 和 [qq] 后运行即可开始抓包和自动解密
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
internal object PacketDebugger {
|
||||
|
||||
/**
|
||||
* 会话密匙, 用于解密数据.
|
||||
* 在一次登录中会话密匙不会改变.
|
||||
*
|
||||
* 从 TIM 内存中读取, windows 方法:
|
||||
* 1. x32dbg 附加 TIM
|
||||
* 2. `符号` 中找到 common.dll
|
||||
* 3. 搜索函数 `oi_symmetry_encrypt2` (TEA 加密函数)
|
||||
* 4. 双击跳转
|
||||
* 5. 设置断点
|
||||
* 6. 在 TIM 发送一条消息触发断点
|
||||
* 7. 运行完 `mov eax,dword ptr ss:[ebp+10]`
|
||||
* 8. 查看内存, `eax` 到 `eax+10` 的 16 字节就是 `sessionKey`
|
||||
*/
|
||||
val sessionKey: SessionKey get() = SessionKey("84 CF F9 57 77 E8 92 56 32 DD 61 A5 E0 AA CB 4C".hexToBytes())
|
||||
// TODO: 2019/12/7 无法访问 internal 是 kotlin bug, KT-34849
|
||||
|
||||
/**
|
||||
* null 则不筛选
|
||||
*/
|
||||
val qq: Long? = null
|
||||
/**
|
||||
* 打开后则记录每一个包到文件.
|
||||
*/
|
||||
val recorder: Recorder? = Recorder()
|
||||
|
||||
val IgnoredPacketIdList: List<PacketId> = listOf(
|
||||
// KnownPacketId.get<FriendOnlineStatusChangedPacket>(),
|
||||
// KnownPacketId.get<ChangeOnlineStatusPacket>(),
|
||||
// KnownPacketId.get<HeartbeatPacket>()
|
||||
)
|
||||
|
||||
suspend fun dataReceived(data: ByteArray) {
|
||||
recorder?.recordReceive(data)
|
||||
//println("raw = " + data.toUHexString())
|
||||
data.read {
|
||||
discardExact(3)
|
||||
val id = matchPacketId(readUShort())
|
||||
val sequenceId = readUShort()
|
||||
val packetQQ = readQQ()
|
||||
|
||||
if (id == HeartbeatPacket.id || (qq != null && packetQQ != qq))
|
||||
return@read
|
||||
|
||||
if (IgnoredPacketIdList.contains(id)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
discardExact(3)//0x00 0x00 0x00. 但更可能是应该 discard 8
|
||||
// val remaining = this.readRemainingBytes().cutTail(1)
|
||||
val encryptedBody = this@read.readRemainingBytes().cutTail(1)
|
||||
try {
|
||||
lateinit var decodedBody: ByteArray
|
||||
val packet = use {
|
||||
with(id.factory) {
|
||||
provideDecrypter(id.factory)
|
||||
.decrypt(ByteReadPacket(encryptedBody))
|
||||
.let {
|
||||
decodedBody = it.readBytes()
|
||||
ByteReadPacket(decodedBody)
|
||||
}
|
||||
.runCatching {
|
||||
decode(id, sequenceId, DebugNetworkHandler)
|
||||
}.getOrElse { /*it.printStackTrace();*/ null }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (packet !is IgnoredEventPacket) {
|
||||
println("--------------")
|
||||
println("接收包id=$id, \nsequence=${sequenceId.toUHexString()}")
|
||||
if (packet !is UnknownPacket) {
|
||||
println(" 解密body=${decodedBody.toUHexString()}")
|
||||
}
|
||||
println(" 解析body=$packet")
|
||||
}
|
||||
|
||||
//handlePacket(id, sequenceId, packet, id.factory)
|
||||
} catch (e: Exception) {
|
||||
println("--------------")
|
||||
println("接收包id=$id, \nsequence=${sequenceId.toUHexString()}")
|
||||
println(" 密文body=" + encryptedBody.toUHexString())
|
||||
println(" 解密body=解密失败")
|
||||
} finally {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供解密密匙. 无需修改
|
||||
*/
|
||||
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
|
||||
internal fun <D : Decrypter> provideDecrypter(factory: PacketFactory<*, D>): D =
|
||||
when (factory.decrypterType) {
|
||||
TouchKey -> TouchKey
|
||||
CaptchaKey -> CaptchaKey
|
||||
ShareKey -> ShareKey
|
||||
|
||||
NoDecrypter -> NoDecrypter
|
||||
|
||||
SessionKey -> sessionKey
|
||||
|
||||
else -> error("No decrypter is found for ${factory.decrypterType}")
|
||||
} as? D ?: error("Internal error: could not cast decrypter which is found for factory to class Decrypter")
|
||||
|
||||
|
||||
/*
|
||||
/**
|
||||
* 处理一个包
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun <TPacket : Packet> handlePacket(
|
||||
id: PacketId,
|
||||
sequenceId: UShort,
|
||||
packet: TPacket,
|
||||
factory: PacketFactory<TPacket, *>
|
||||
) {
|
||||
return
|
||||
}*/
|
||||
|
||||
fun dataSent(rawPacket: ByteArray) = rawPacket.cutTail(1).read {
|
||||
recorder?.recordSend(rawPacket)
|
||||
|
||||
// 02 38 03
|
||||
// 03 52 78 9F
|
||||
// 3E 03 3F A2 04 00 00 00 01 2E 01 00 00 69 35 00 00 00 00 00 00 00 00
|
||||
|
||||
// 02 38 03
|
||||
// 01 BD 63 D6
|
||||
// 3E 03 3F A2 02 00 00 00 01 2E 01 00 00 69 35
|
||||
|
||||
discardExact(3)//head
|
||||
val id = matchPacketId(readUShort())
|
||||
val sequence = readUShort().toUHexString()
|
||||
if (IgnoredPacketIdList.contains(id)) {
|
||||
return
|
||||
}
|
||||
val packetQQ = readQQ()
|
||||
if (qq != null && packetQQ != qq) {
|
||||
return@read
|
||||
}
|
||||
println("---------------------------")
|
||||
println("发出包ID = $id")
|
||||
println("sequence = $sequence")
|
||||
|
||||
println(
|
||||
" fixVer2=" + when (val flag = readByte().toInt()) {
|
||||
2 -> byteArrayOf(2) + readBytes(TIMProtocol.fixVer2.size - 1)
|
||||
3 -> byteArrayOf(3) + readBytes(TIMProtocol.fixVer2.size - 1 + 4)//4 个0
|
||||
4 -> byteArrayOf(4) + readBytes(TIMProtocol.fixVer2.size - 1 + 8)//8 个 0
|
||||
0 -> byteArrayOf(0) + readBytes(2)
|
||||
else -> error("unknown fixVer2 flag=$flag. Remaining =${readBytes().toUHexString()}")
|
||||
}.toUHexString()
|
||||
)
|
||||
|
||||
//39 27 DC E2 04 00 00 00 00 00 00 00 1E 0E 89 00 00 01 05 0F 05 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 3E 03 3F A2 00 00 00 00 00 00 00 00 00 00 00
|
||||
|
||||
val encryptedBody = readRemainingBytes()
|
||||
try {
|
||||
println(" 解密body=${encryptedBody.decryptBy(sessionKey.value).toUHexString()}")
|
||||
} catch (e: DecryptionFailedException) {
|
||||
println(" 密文=" + encryptedBody.toUHexString())
|
||||
println(" 解密body=解密失败")
|
||||
}
|
||||
|
||||
encryptedBody.read {
|
||||
|
||||
/*
|
||||
when (idHex.substring(0, 5)) {
|
||||
"00 CD" -> {
|
||||
println("好友消息")
|
||||
|
||||
val raw = readRemainingBytes()
|
||||
//println("解密前数据: " + raw.toUHexString())
|
||||
val messageData = raw.decryptBy(sessionKey.value)
|
||||
//println("解密结果: " + messageData.toUHexString())
|
||||
println("尝试解消息")
|
||||
|
||||
try {
|
||||
messageData.read {
|
||||
discardExact(
|
||||
4 + 4 + 12 + 2 + 4 + 4 + 16 + 2 + 2 + 4 + 2 + 16 + 4 + 4 + 7 + 15 + 2
|
||||
+ 1
|
||||
)
|
||||
val chain = readMessageChain()
|
||||
println(chain)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("失败")
|
||||
}
|
||||
}*/
|
||||
|
||||
/*
|
||||
"03 88" -> {
|
||||
println("0388上传图片-获取图片ID")
|
||||
discardExact(8)
|
||||
|
||||
//val body = readRemainingBytes().decryptBy(sessionKey)
|
||||
//println(body.toUHexString())
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal object DebugNetworkHandler : BotNetworkHandler(), CoroutineScope {
|
||||
override val supervisor: CompletableJob = SupervisorJob()
|
||||
|
||||
override val bot: Bot = TIMPC.Bot(qq ?: 0L, "")
|
||||
|
||||
override suspend fun login() {}
|
||||
|
||||
override suspend fun awaitDisconnection() {
|
||||
}
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = GlobalScope.coroutineContext
|
||||
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package packetdebugger
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readUShort
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketId
|
||||
|
||||
import net.mamoe.mirai.timpc.network.packet.matchPacketId
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
@UseExperimental(ExperimentalUnsignedTypes::class)
|
||||
class PacketDecoderScope(
|
||||
val packet: ByteReadPacket
|
||||
) {
|
||||
@PublishedApi
|
||||
internal var _id: UShort = 0u
|
||||
@PublishedApi
|
||||
internal var _sequence: UShort = 0u
|
||||
|
||||
internal val id: PacketId get() = matchPacketId(_id)
|
||||
val sequence: UShort get() = _sequence
|
||||
}
|
||||
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
@UseExperimental(ExperimentalContracts::class)
|
||||
inline fun ByteReadPacket.decodeOutgoingPacket(block: PacketDecoderScope.() -> Unit) {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
this.use {
|
||||
PacketDecoderScope(it).apply {
|
||||
discardExact(1) // head
|
||||
discardExact(2) // ver
|
||||
_id = readUShort()
|
||||
_sequence = readUShort()
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="net.mamoe.mirai.timpc">
|
||||
</manifest>
|
@ -8,7 +8,6 @@ import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.use
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.data.AddFriendResult
|
||||
import net.mamoe.mirai.data.ImageLink
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.utils.GroupNotFoundException
|
||||
@ -104,11 +103,9 @@ abstract class Bot : CoroutineScope {
|
||||
|
||||
// region actions
|
||||
|
||||
abstract suspend fun Image.getLink(): ImageLink
|
||||
abstract suspend fun Image.downloadAsByteArray(): ByteArray
|
||||
|
||||
suspend fun Image.downloadAsByteArray(): ByteArray = getLink().downloadAsByteArray()
|
||||
|
||||
suspend fun Image.download(): ByteReadPacket = getLink().download()
|
||||
abstract suspend fun Image.download(): ByteReadPacket
|
||||
|
||||
/**
|
||||
* 添加一个好友
|
||||
|
@ -36,7 +36,7 @@ interface Contact : CoroutineScope {
|
||||
*/
|
||||
suspend fun sendMessage(message: MessageChain)
|
||||
|
||||
suspend fun uploadImage(image: ExternalImage): ImageId
|
||||
suspend fun uploadImage(image: ExternalImage): Image
|
||||
}
|
||||
|
||||
suspend inline fun Contact.sendMessage(message: Message) = sendMessage(message.toChain())
|
||||
|
@ -37,7 +37,7 @@ class ContactList<C : Contact>(@MiraiInternalAPI val delegate: LockFreeLinkedLis
|
||||
|
||||
operator fun <C : Contact> LockFreeLinkedList<C>.get(id: Long): C {
|
||||
forEach { if (it.id == id) return it }
|
||||
throw NoSuchElementException()
|
||||
throw NoSuchElementException("No such contact with id $id")
|
||||
}
|
||||
|
||||
fun <C : Contact> LockFreeLinkedList<C>.getOrNull(id: Long): C? {
|
||||
|
@ -6,7 +6,6 @@ import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.data.EventPacket
|
||||
import net.mamoe.mirai.data.ImageLink
|
||||
import net.mamoe.mirai.event.events.BotEvent
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
@ -65,17 +64,14 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) :
|
||||
|
||||
suspend inline fun ExternalImage.upload(): Image = this.upload(subject)
|
||||
suspend inline fun Image.send() = this.sendTo(subject)
|
||||
suspend inline fun ImageId.send() = this.sendTo(subject)
|
||||
suspend inline fun Message.send() = this.sendTo(subject)
|
||||
suspend inline fun String.send() = this.toMessage().sendTo(subject)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Image download
|
||||
suspend inline fun Image.getLink(): ImageLink = with(bot) { getLink() }
|
||||
|
||||
suspend inline fun Image.downloadAsByteArray(): ByteArray = getLink().downloadAsByteArray()
|
||||
suspend inline fun Image.download(): ByteReadPacket = getLink().download()
|
||||
suspend inline fun Image.downloadAsByteArray(): ByteArray = bot.run { downloadAsByteArray() }
|
||||
suspend inline fun Image.download(): ByteReadPacket = bot.run { download() }
|
||||
// endregion
|
||||
|
||||
fun At.qq(): QQ = bot.getQQ(this.target)
|
||||
|
@ -2,76 +2,44 @@
|
||||
|
||||
package net.mamoe.mirai.message.data
|
||||
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.contact.sendMessage
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
sealed class Image : Message {
|
||||
abstract val resourceId: String
|
||||
|
||||
abstract override fun toString(): String
|
||||
|
||||
fun Image(id: String) = Image(ImageId(id))
|
||||
final companion object Key : Message.Key<Image>
|
||||
|
||||
/**
|
||||
* 图片消息. 在发送时将会区分群图片和好友图片发送.
|
||||
* 由接收消息时构建, 可直接发送
|
||||
*
|
||||
* @param id 这个图片的 [ImageId]
|
||||
*/ // TODO: 2020/1/31 去掉 Image. 将 Image 改为 interface/class
|
||||
inline class Image(inline val id: ImageId) : Message {
|
||||
override fun toString(): String = "[${id.value}]"
|
||||
abstract override fun eq(other: Message): Boolean
|
||||
}
|
||||
|
||||
companion object Key : Message.Key<Image>
|
||||
abstract class NotOnlineImage : Image() {
|
||||
abstract override val resourceId: String
|
||||
abstract val md5: ByteArray
|
||||
abstract val filepath: String
|
||||
abstract val fileLength: Int
|
||||
abstract val height: Int
|
||||
abstract val width: Int
|
||||
open val bizType: Int get() = 0
|
||||
open val imageType: Int get() = 1000
|
||||
open val downloadPath: String get() = resourceId
|
||||
|
||||
override fun toString(): String {
|
||||
return "[$resourceId]"
|
||||
}
|
||||
|
||||
override fun eq(other: Message): Boolean {
|
||||
return other is Image && other.id == this.id
|
||||
return other.toString() == this.toString()
|
||||
}
|
||||
}
|
||||
|
||||
inline val Image.idValue: String get() = id.value
|
||||
|
||||
inline class ImageId0x06(override inline val value: String) : ImageId {
|
||||
override fun toString(): String = "ImageId($value)"
|
||||
}
|
||||
|
||||
/**
|
||||
* 一般是群的图片的 id.
|
||||
*/
|
||||
class ImageId0x03 constructor(override inline val value: String, inline val uniqueId: UInt, inline val height: Int, inline val width: Int) :
|
||||
ImageId {
|
||||
override fun toString(): String = "ImageId(value=$value, uniqueId=${uniqueId}, height=$height, width=$width)"
|
||||
|
||||
val md5: ByteArray
|
||||
get() = this.value
|
||||
.substringAfter("{").substringBefore("}")
|
||||
.replace("-", "")
|
||||
.chunked(2)
|
||||
.map { (it[0] + it[1].toString()).toUByte(16).toByte() }
|
||||
.toByteArray().also { check(it.size == 16) }
|
||||
}
|
||||
|
||||
@Suppress("FunctionName", "NOTHING_TO_INLINE")
|
||||
inline fun ImageId(value: String): ImageId = ImageId0x06(value)
|
||||
|
||||
@Suppress("FunctionName", "NOTHING_TO_INLINE")
|
||||
inline fun ImageId(value: String, uniqueId: UInt, height: Int, width: Int): ImageId =
|
||||
ImageId0x03(value, uniqueId, height, width)
|
||||
|
||||
|
||||
/**
|
||||
* 图片的标识符. 由图片的数据产生.
|
||||
* 对于群, [value] 类似于 `{F61593B5-5B98-1798-3F47-2A91D32ED2FC}.jpg`, 由图片文件 MD5 直接产生.
|
||||
* 对于好友, [value] 类似于 `/01ee6426-5ff1-4cf0-8278-e8634d2909ef`, 由服务器返回.
|
||||
*
|
||||
* @see ExternalImage.groupImageId 群图片的 [ImageId] 获取
|
||||
* @see FriendImagePacket 好友图片的 [ImageId] 获取
|
||||
*/
|
||||
interface ImageId {
|
||||
val value: String
|
||||
}
|
||||
|
||||
fun ImageId.checkLength() = check(value.length == 37 || value.length == 42) { "Illegal ImageId length" }
|
||||
fun ImageId.requireLength() = require(value.length == 37 || value.length == 42) { "Illegal ImageId length" }
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun ImageId.image(): Image =
|
||||
Image(this)
|
||||
|
||||
suspend inline fun ImageId.sendTo(contact: Contact) = contact.sendMessage(this.image())
|
||||
open class NotOnlineImageFromFile(
|
||||
override val resourceId: String,
|
||||
override val md5: ByteArray,
|
||||
override val filepath: String,
|
||||
override val fileLength: Int,
|
||||
override val height: Int,
|
||||
override val width: Int,
|
||||
override val bizType: Int = 0,
|
||||
override val imageType: Int = 1000,
|
||||
override val downloadPath: String = resourceId
|
||||
) : NotOnlineImage()
|
@ -45,19 +45,6 @@ class ExternalImage(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于发送消息的 [ImageId]
|
||||
*/
|
||||
@Suppress("EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
val groupImageId: ImageId by lazy {
|
||||
ImageId0x03(
|
||||
"{${md5[0..3]}-${md5[4..5]}-${md5[6..7]}-${md5[8..9]}-${md5[10..15]}}.$format",
|
||||
0u,
|
||||
height,
|
||||
width
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String = "[ExternalImage(${width}x$height $format)]"
|
||||
}
|
||||
|
||||
@ -77,8 +64,8 @@ suspend fun ExternalImage.sendTo(contact: Contact) = when (contact) {
|
||||
* @see contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人
|
||||
*/
|
||||
suspend fun ExternalImage.upload(contact: Contact): Image = when (contact) {
|
||||
is Group -> contact.uploadImage(this).image()
|
||||
is QQ -> contact.uploadImage(this).image()
|
||||
is Group -> contact.uploadImage(this)
|
||||
is QQ -> contact.uploadImage(this)
|
||||
else -> assertUnreachable()
|
||||
}
|
||||
|
||||
|
@ -43,8 +43,6 @@ fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
|
||||
dependencies {
|
||||
|
||||
runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main")) // IDE bug
|
||||
runtimeOnly(files("../mirai-core-timpc/build/classes/kotlin/jvm/main")) // IDE bug
|
||||
implementation(project(":mirai-core-timpc"))
|
||||
runtimeOnly(files("../mirai-core-qqandroid/build/classes/kotlin/jvm/main")) // IDE bug
|
||||
implementation(project(":mirai-core-qqandroid"))
|
||||
// runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main")) // classpath is not added correctly by IDE
|
||||
|
@ -4,8 +4,8 @@ apply plugin: "java"
|
||||
dependencies {
|
||||
implementation files("../../mirai-core/build/classes/kotlin/jvm/main") // IDE bug
|
||||
|
||||
implementation files("../../mirai-core-timpc/build/classes/kotlin/jvm/main") // IDE bug
|
||||
implementation project(":mirai-core-timpc")
|
||||
implementation files("../../mirai-core-qqandroid/build/classes/kotlin/jvm/main") // IDE bug
|
||||
implementation project(":mirai-core-qqandroid")
|
||||
|
||||
api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion
|
||||
api group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: coroutinesVersion
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
package demo.subscribe
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.BotAccount
|
||||
import net.mamoe.mirai.alsoLogin
|
||||
@ -13,13 +11,11 @@ import net.mamoe.mirai.event.*
|
||||
import net.mamoe.mirai.message.FriendMessage
|
||||
import net.mamoe.mirai.message.GroupMessage
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.ImageId
|
||||
import net.mamoe.mirai.message.data.PlainText
|
||||
import net.mamoe.mirai.message.data.firstOrNull
|
||||
import net.mamoe.mirai.message.sendAsImageTo
|
||||
import net.mamoe.mirai.timpc.Bot
|
||||
import net.mamoe.mirai.timpc.TIMPC
|
||||
import net.mamoe.mirai.utils.suspendToExternalImage
|
||||
import net.mamoe.mirai.qqandroid.Bot
|
||||
import net.mamoe.mirai.qqandroid.QQAndroid
|
||||
import java.io.File
|
||||
|
||||
private fun readTestAccount(): BotAccount? {
|
||||
@ -39,7 +35,7 @@ private fun readTestAccount(): BotAccount? {
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
suspend fun main() {
|
||||
val bot = TIMPC.Bot( // JVM 下也可以不写 `TIMPC.` 引用顶层函数
|
||||
val bot = QQAndroid.Bot( // JVM 下也可以不写 `TIMPC.` 引用顶层函数
|
||||
1994701121,
|
||||
"123456"
|
||||
) {
|
||||
@ -99,7 +95,7 @@ fun Bot.messageDSL() {
|
||||
// 等同于 reply("你在一个群里")
|
||||
}
|
||||
|
||||
reply("图片, ID= ${message[Image].id}")//获取第一个 Image 类型的消息
|
||||
reply("图片, ID= ${message[Image]}")//获取第一个 Image 类型的消息
|
||||
reply(message)
|
||||
}
|
||||
|
||||
@ -222,29 +218,6 @@ suspend fun directlySubscribe(bot: Bot) {
|
||||
"复读" in message -> sender.sendMessage(message)
|
||||
|
||||
"发群消息" in message -> 580266363.group().sendMessage(message.toString().substringAfter("发群消息"))
|
||||
|
||||
"上传群图片" in message -> withTimeoutOrNull(5000) {
|
||||
val filename = message.toString().substringAfter("上传群图片")
|
||||
val image = File(
|
||||
"C:\\Users\\Him18\\Desktop\\$filename"
|
||||
).suspendToExternalImage()
|
||||
920503456.group().uploadImage(image)
|
||||
reply(image.groupImageId.value)
|
||||
delay(100)
|
||||
920503456.group().sendMessage(Image(image.groupImageId))
|
||||
}
|
||||
|
||||
"发群图片" in message -> {
|
||||
920503456.group().sendMessage(Image(ImageId(message.toString().substringAfter("发群图片"))))
|
||||
}
|
||||
|
||||
"发好友图片" in message -> {
|
||||
reply(Image(ImageId(message.toString().substringAfter("发好友图片"))))
|
||||
}
|
||||
|
||||
message eq "发图片群2" -> 580266363.group().sendMessage(Image(ImageId("{7AA4B3AA-8C3C-0F45-2D9B-7F302A0ACEAA}.jpg")))
|
||||
|
||||
message eq "发图片2" -> reply(PlainText("test") + Image(ImageId("{7AA4B3AA-8C3C-0F45-2D9B-7F302A0ACEAA}.jpg")))
|
||||
}
|
||||
}
|
||||
}
|
@ -40,7 +40,7 @@ kotlin {
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib"
|
||||
implementation project(':mirai-core-timpc')
|
||||
implementation project(':mirai-core-qqandroid')
|
||||
|
||||
implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion
|
||||
implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: coroutinesVersion
|
||||
|
@ -3,6 +3,7 @@
|
||||
package net.mamoe.mirai.demo
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Binder
|
||||
@ -14,8 +15,7 @@ import kotlinx.io.core.IoBuffer
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.event.subscribeMessages
|
||||
import net.mamoe.mirai.timpc.Bot
|
||||
import net.mamoe.mirai.timpc.TIMPC
|
||||
import net.mamoe.mirai.qqandroid.QQAndroid
|
||||
import net.mamoe.mirai.utils.LoginFailedException
|
||||
import net.mamoe.mirai.utils.LoginSolver
|
||||
import java.lang.ref.WeakReference
|
||||
@ -42,9 +42,9 @@ class MiraiService : Service() {
|
||||
|
||||
}
|
||||
|
||||
private fun login(qq: Long, pwd: String) {
|
||||
private fun login(context: Context, qq: Long, pwd: String) {
|
||||
GlobalScope.launch {
|
||||
mBot = TIMPC.Bot(qq, pwd) {
|
||||
mBot = QQAndroid.Bot(context, qq, pwd) {
|
||||
loginSolver = object : LoginSolver() {
|
||||
override suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String? {
|
||||
val bytes = data.readBytes()
|
||||
@ -94,7 +94,7 @@ class MiraiService : Service() {
|
||||
inner class MiraiBinder : Binder() {
|
||||
|
||||
fun startLogin(qq: Long, pwd: String) {
|
||||
login(qq, pwd)
|
||||
login(applicationContext, qq, pwd)
|
||||
}
|
||||
|
||||
fun setCaptcha(captcha: String) {
|
||||
|
@ -3,8 +3,8 @@ apply plugin: "java"
|
||||
apply plugin: "application"
|
||||
|
||||
dependencies {
|
||||
implementation files("../../mirai-core-timpc/build/classes/kotlin/jvm/main") // IDE bug
|
||||
implementation project(":mirai-core-timpc")
|
||||
implementation files("../../mirai-core-qqandroid/build/classes/kotlin/jvm/main") // IDE bug
|
||||
implementation project(":mirai-core-qqandroid")
|
||||
|
||||
implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion
|
||||
implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: coroutinesVersion
|
||||
|
@ -134,7 +134,7 @@ suspend fun main() {
|
||||
|
||||
try {
|
||||
image.downloadTo(newTestTempFile(suffix = ".png").also { reply("Temp file: ${it.absolutePath}") })
|
||||
reply(image.id.value + " downloaded")
|
||||
reply(image.resourceId + " downloaded")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
reply(e.message ?: e::class.java.simpleName)
|
||||
|
@ -3,7 +3,6 @@ package net.mamoe.mirai.japt;
|
||||
import kotlinx.io.core.ByteReadPacket;
|
||||
import net.mamoe.mirai.BotAccount;
|
||||
import net.mamoe.mirai.data.AddFriendResult;
|
||||
import net.mamoe.mirai.data.ImageLink;
|
||||
import net.mamoe.mirai.message.data.Image;
|
||||
import net.mamoe.mirai.network.BotNetworkHandler;
|
||||
import net.mamoe.mirai.utils.GroupNotFoundException;
|
||||
@ -92,9 +91,6 @@ public interface BlockingBot {
|
||||
|
||||
// region actions
|
||||
|
||||
@NotNull
|
||||
ImageLink getLink(@NotNull Image image);
|
||||
|
||||
byte[] downloadAsByteArray(@NotNull Image image);
|
||||
|
||||
@NotNull
|
||||
|
@ -6,7 +6,6 @@ import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.BotAccount
|
||||
import net.mamoe.mirai.contact.GroupInternalId
|
||||
import net.mamoe.mirai.data.AddFriendResult
|
||||
import net.mamoe.mirai.data.ImageLink
|
||||
import net.mamoe.mirai.japt.BlockingBot
|
||||
import net.mamoe.mirai.japt.BlockingGroup
|
||||
import net.mamoe.mirai.japt.BlockingQQ
|
||||
@ -33,7 +32,6 @@ internal class BlockingBotImpl(private val bot: Bot) : BlockingBot {
|
||||
override fun getGroupByInternalId(internalId: Long): BlockingGroup = runBlocking { bot.getGroup(GroupInternalId(internalId)).blocking() }
|
||||
override fun getNetwork(): BotNetworkHandler = bot.network
|
||||
override fun login() = runBlocking { bot.login() }
|
||||
override fun getLink(image: Image): ImageLink = bot.run { runBlocking { image.getLink() } }
|
||||
override fun downloadAsByteArray(image: Image): ByteArray = bot.run { runBlocking { image.downloadAsByteArray() } }
|
||||
override fun download(image: Image): ByteReadPacket = bot.run { runBlocking { image.download() } }
|
||||
override fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult = runBlocking { bot.addFriend(id, message, remark) }
|
||||
|
@ -33,7 +33,7 @@ if (!keyProps.getProperty("sdk.dir", "").isEmpty()) {
|
||||
}
|
||||
|
||||
include(':mirai-core')
|
||||
include(':mirai-core-timpc')
|
||||
//include(':mirai-core-timpc')
|
||||
include(':mirai-core-qqandroid')
|
||||
|
||||
include(':mirai-japt')
|
||||
|
Loading…
Reference in New Issue
Block a user