Commit 6b698ef4 authored by jiahua.liu's avatar jiahua.liu

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