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:
jiahua.liu 2020-02-03 00:17:22 +08:00
commit 6b698ef4d4
89 changed files with 200 additions and 6619 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="net.mamoe.mirai.timpc">
</manifest>

View File

@ -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
/**
* 添加一个好友

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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