From 3b35dbcac5b8e4bde96dce02a79e795e025e839e Mon Sep 17 00:00:00 2001 From: Him188 Date: Wed, 23 Dec 2020 20:57:39 +0800 Subject: [PATCH] Support OtherClient online status: - Add ClientKind - Add OtherClientOnlineEvent,OtherClientOfflineEvent - Add Bot.otherClients - Add OtherClientList --- mirai-core-api/src/commonMain/kotlin/Bot.kt | 6 ++ .../commonMain/kotlin/contact/OtherClient.kt | 55 +++++++++++++++---- .../kotlin/contact/OtherClientList.kt | 23 ++++++++ .../kotlin/event/events/otherClient.kt | 35 ++++++++++++ .../src/commonMain/kotlin/AbstractBot.kt | 5 ++ .../src/commonMain/kotlin/QQAndroidBot.kt | 7 +++ .../kotlin/contact/OtherClientImpl.kt | 38 +++++++++++++ .../network/protocol/data/jce/InstanceInfo.kt | 27 +++++++++ .../data/jce/SvcReqMSFLoginNotifyData.kt | 47 ++++++++++++++++ .../network/protocol/packet/PacketFactory.kt | 3 +- .../network/protocol/packet/login/StatSvc.kt | 46 ++++++++++++++-- 11 files changed, 276 insertions(+), 16 deletions(-) create mode 100644 mirai-core-api/src/commonMain/kotlin/contact/OtherClientList.kt create mode 100644 mirai-core-api/src/commonMain/kotlin/event/events/otherClient.kt create mode 100644 mirai-core/src/commonMain/kotlin/contact/OtherClientImpl.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/protocol/data/jce/InstanceInfo.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcReqMSFLoginNotifyData.kt diff --git a/mirai-core-api/src/commonMain/kotlin/Bot.kt b/mirai-core-api/src/commonMain/kotlin/Bot.kt index d26690240..846d0d066 100644 --- a/mirai-core-api/src/commonMain/kotlin/Bot.kt +++ b/mirai-core-api/src/commonMain/kotlin/Bot.kt @@ -76,6 +76,12 @@ public interface Bot : CoroutineScope, ContactOrBot, UserOrBot { // region contacts + /** + * 其他设备列表 + */ + public val otherClients: OtherClientList + + /** * [User.id] 与 [Bot.id] 相同的 [Friend] 实例 */ diff --git a/mirai-core-api/src/commonMain/kotlin/contact/OtherClient.kt b/mirai-core-api/src/commonMain/kotlin/contact/OtherClient.kt index 9a04f5804..f4b0a10ed 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/OtherClient.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/OtherClient.kt @@ -7,6 +7,8 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:Suppress("unused") + package net.mamoe.mirai.contact import net.mamoe.mirai.Bot @@ -17,6 +19,11 @@ import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PHONE * 其他设备. 如当 [Bot] 以 [ANDROID_PHONE] 登录时, 还可以有其他设备以 [ANDROID_PAD], iOS, PC 或其他设备登录. */ public interface OtherClient : Contact { + /** + * 设备类型 + */ + public val kind: ClientKind + /** * 此设备属于的 [Bot] */ @@ -28,14 +35,40 @@ public interface OtherClient : Contact { public override val id: Long get() = bot.id } -/* -public enum class ClientKind { - ANDROID_PHONE, - ANDROID_PAD, - ANDROID_WATCH, - IOS_PHONE, - IOS_PAD, - MAC_OS, - WINDOWS_QQ, - WINDOWS_TIM -}*/ \ No newline at end of file +/** + * 设备类型 + */ +public enum class ClientKind( + public val id: Int, +) { + ANDROID_PAD(68104), + AOL_CHAOJIHUIYUAN(73730), + AOL_HUIYUAN(73474), + AOL_SQQ(69378), + CAR(65806), + HRTX_IPHONE(66566), + HRTX_PC(66561), + MC_3G(65795), + MISRO_MSG(69634), + MOBILE_ANDROID(65799), + MOBILE_ANDROID_NEW(72450), + MOBILE_HD(65805), + MOBILE_HD_NEW(71426), + MOBILE_IPAD(68361), + MOBILE_IPAD_NEW(72194), + MOBILE_IPHONE(67586), + MOBILE_OTHER(65794), + MOBILE_PC(65793), + MOBILE_WINPHONE_NEW(72706), + QQ_FORELDER(70922), + QQ_SERVICE(71170), + TV_QQ(69130), + WIN8(69899), + WINPHONE(65804), + + UNKNOWN(-1); + + public companion object { + public operator fun get(id: Int): ClientKind? = values().find { it.id == id } + } +} diff --git a/mirai-core-api/src/commonMain/kotlin/contact/OtherClientList.kt b/mirai-core-api/src/commonMain/kotlin/contact/OtherClientList.kt new file mode 100644 index 000000000..63d1dc95f --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/contact/OtherClientList.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.contact + +import net.mamoe.mirai.utils.MiraiInternalApi +import java.util.concurrent.ConcurrentLinkedQueue + +public class OtherClientList internal constructor( + @MiraiInternalApi @JvmField + public val delegate: MutableCollection = ConcurrentLinkedQueue() +) : Collection by delegate { + public operator fun get(kind: ClientKind): OtherClient? = this.find { it.kind == kind } + + public fun getOrFail(kind: ClientKind): OtherClient = + get(kind) ?: throw NoSuchElementException("OtherClient with kind=$kind not found.") +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/event/events/otherClient.kt b/mirai-core-api/src/commonMain/kotlin/event/events/otherClient.kt new file mode 100644 index 000000000..d905c3dac --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/event/events/otherClient.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.event.events + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.contact.OtherClient +import net.mamoe.mirai.event.AbstractEvent +import net.mamoe.mirai.internal.network.Packet + +public interface OtherClientEvent : BotEvent, Packet { + public val client: OtherClient + override val bot: Bot + get() = client.bot +} + +/** + * 其他设备上线 + */ +public data class OtherClientOnlineEvent( + override val client: OtherClient +) : OtherClientEvent, AbstractEvent() + +/** + * 其他设备离线 + */ +public data class OtherClientOfflineEvent( + override val client: OtherClient +) : OtherClientEvent, AbstractEvent() \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/AbstractBot.kt b/mirai-core/src/commonMain/kotlin/AbstractBot.kt index f5c5bacf1..c3916de69 100644 --- a/mirai-core/src/commonMain/kotlin/AbstractBot.kt +++ b/mirai-core/src/commonMain/kotlin/AbstractBot.kt @@ -18,7 +18,9 @@ package net.mamoe.mirai.internal import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex import net.mamoe.mirai.Bot +import net.mamoe.mirai.contact.OtherClientList import net.mamoe.mirai.event.Listener import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.events.BotOfflineEvent @@ -72,6 +74,9 @@ internal abstract class AbstractBot constructor( override val isOnline: Boolean get() = _network.areYouOk() + val otherClientsLock = Mutex() // lock sync + override val otherClients: OtherClientList = OtherClientList() + /** * Close server connection, resend login packet, BUT DOESN'T [BotNetworkHandler.init] */ diff --git a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt index 5e244267a..6d8d2383f 100644 --- a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt +++ b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt @@ -18,6 +18,7 @@ import net.mamoe.mirai.LowLevelApi import net.mamoe.mirai.Mirai import net.mamoe.mirai.contact.* import net.mamoe.mirai.data.* +import net.mamoe.mirai.internal.contact.OtherClientImpl import net.mamoe.mirai.internal.contact.checkIsGroupImpl import net.mamoe.mirai.internal.message.* import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler @@ -38,6 +39,12 @@ internal fun Bot.asQQAndroidBot(): QQAndroidBot { return this as QQAndroidBot } +internal fun QQAndroidBot.createOtherClient( + kind: ClientKind +): OtherClientImpl { + return OtherClientImpl(this, coroutineContext, kind) +} + @Suppress("INVISIBLE_MEMBER", "BooleanLiteralArgument", "OverridingDeprecatedMember") internal class QQAndroidBot constructor( account: BotAccount, diff --git a/mirai-core/src/commonMain/kotlin/contact/OtherClientImpl.kt b/mirai-core/src/commonMain/kotlin/contact/OtherClientImpl.kt new file mode 100644 index 000000000..1408623cb --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/contact/OtherClientImpl.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.internal.contact + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.contact.ClientKind +import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.contact.OtherClient +import net.mamoe.mirai.message.MessageReceipt +import net.mamoe.mirai.message.data.Image +import net.mamoe.mirai.message.data.Message +import net.mamoe.mirai.utils.ExternalImage +import kotlin.coroutines.CoroutineContext + +internal class OtherClientImpl( + bot: Bot, + coroutineContext: CoroutineContext, + override val kind: ClientKind +) : OtherClient, AbstractContact(bot, coroutineContext) { + override suspend fun sendMessage(message: Message): MessageReceipt { + TODO("Not yet implemented") + } + + override suspend fun uploadImage(image: ExternalImage): Image { + TODO("Not yet implemented") + } + + override fun toString(): String { + return "OtherClient(bot=${bot.id},kind=$kind)" + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/InstanceInfo.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/InstanceInfo.kt new file mode 100644 index 000000000..d432c9499 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/InstanceInfo.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package network.protocol.data.jce + +import kotlinx.serialization.Serializable +import net.mamoe.mirai.contact.ClientKind +import net.mamoe.mirai.internal.utils.io.JceStruct +import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId + +@Serializable +internal data class InstanceInfo( + @JvmField @TarsId(0) val iAppId: Int? = null, + @JvmField @TarsId(1) val tablet: Byte? = null, + @JvmField @TarsId(2) val iPlatform: Long? = null, + /** + * @see ClientKind + */ + @JvmField @TarsId(3) val iProductType: Long? = null, + @JvmField @TarsId(4) val iClientType: Long? = null +) : JceStruct diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcReqMSFLoginNotifyData.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcReqMSFLoginNotifyData.kt new file mode 100644 index 000000000..c9c47ddb0 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcReqMSFLoginNotifyData.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package network.protocol.data.jce + +import kotlinx.serialization.Serializable +import net.mamoe.mirai.internal.utils.io.JceStruct +import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId + +// ANDROID PHONE QQ + +// 2020-12-23 20:16:57 D/soutv: PK = +//SvcReqMSFLoginNotifyData(iAppId=537066423, +//status=2, +//tablet=0, +//iPlatform=109, +//title=下线通知, +//info=你的帐号在手机上退出了, +//iProductType=0, +//iClientType=65799, +//vecInstanceList=[]) + +// ANDROID PHONE QQ +// 2020-12-23 20:21:02 D/soutv: PK = SvcReqMSFLoginNotifyData( +//iAppId=537066423, +//status=1, +//tablet=0, +//iPlatform=109, title=上线通知, info=你的帐号在手机上登录了, iProductType=0, iClientType=65799, vecInstanceList=[InstanceInfo(iAppId=537066423, tablet=0, iPlatform=109, iProductType=0, iClientType=65799)]) + +@Serializable +internal data class SvcReqMSFLoginNotifyData( + @JvmField @TarsId(0) val iAppId: Long, + @JvmField @TarsId(1) val status: Byte, // 上线=1, 下线=2 + @JvmField @TarsId(2) val tablet: Byte? = null, + @JvmField @TarsId(3) val iPlatform: Long? = null, + @JvmField @TarsId(4) val title: String? = "", + @JvmField @TarsId(5) val info: String? = "", + @JvmField @TarsId(6) val iProductType: Long? = null, + @JvmField @TarsId(7) val iClientType: Long? = null, + @JvmField @TarsId(8) val vecInstanceList: List? = null +) : JceStruct diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt index 8353ee2b3..68f20cf61 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt @@ -161,7 +161,8 @@ internal object KnownPacketFactories { OnlinePushPbPushTransMsg, MessageSvcPushNotify, ConfigPushSvc.PushReq, - StatSvc.ReqMSFOffline + StatSvc.ReqMSFOffline, + StatSvc.SvcReqMSFLoginNotify ) // SvcReqMSFLoginNotify 自己的其他设备上限 // MessageSvcPushReaded 电脑阅读了别人的消息, 告知手机 diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt index bd2a3329c..083b8a88c 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt @@ -9,10 +9,17 @@ package net.mamoe.mirai.internal.network.protocol.packet.login +import kotlinx.coroutines.cancel +import kotlinx.coroutines.sync.withLock import kotlinx.io.core.ByteReadPacket import kotlinx.serialization.protobuf.ProtoBuf +import net.mamoe.mirai.contact.ClientKind import net.mamoe.mirai.event.events.BotOfflineEvent +import net.mamoe.mirai.event.events.OtherClientOfflineEvent +import net.mamoe.mirai.event.events.OtherClientOnlineEvent import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.createOtherClient +import net.mamoe.mirai.internal.message.contextualBugReportException import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.QQAndroidClient import net.mamoe.mirai.internal.network.guid @@ -23,11 +30,9 @@ import net.mamoe.mirai.internal.network.protocol.data.jce.SvcReqRegister import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x769 import net.mamoe.mirai.internal.network.protocol.data.proto.StatSvcGetOnline import net.mamoe.mirai.internal.network.protocol.packet.* -import net.mamoe.mirai.internal.utils.MiraiPlatformUtils -import net.mamoe.mirai.internal.utils.NetworkType -import net.mamoe.mirai.internal.utils.encodeToString +import net.mamoe.mirai.internal.utils.* import net.mamoe.mirai.internal.utils.io.serialization.* -import net.mamoe.mirai.internal.utils.toReadPacket +import java.util.concurrent.CancellationException @Suppress("EnumEntryName", "unused") internal enum class RegPushReason { @@ -220,4 +225,37 @@ internal class StatSvc { } } } + + internal object SvcReqMSFLoginNotify : + IncomingPacketFactory("StatSvc.SvcReqMSFLoginNotify", "StatSvc.SvcReqMSFLoginNotify") { + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet? = + bot.otherClientsLock.withLock { + val notify = readUniPacket(network.protocol.data.jce.SvcReqMSFLoginNotifyData.serializer()) + + val kind = notify.iClientType?.toInt()?.let(ClientKind::get) ?: return null + + when (notify.status.toInt()) { + 1 -> { + if (bot.otherClients.any { it.kind == kind }) return null + val client = bot.createOtherClient(kind) + bot.otherClients.delegate.add(client) + OtherClientOnlineEvent(client) + } + + 2 -> { + val client = bot.otherClients.find { it.kind == kind } ?: return null + client.cancel(CancellationException("Offline")) + bot.otherClients.delegate.remove(client) + OtherClientOfflineEvent(client) + } + + else -> throw contextualBugReportException( + "decode SvcReqMSFLoginNotify (OtherClient status change)", + notify._miraiContentToString(), + additional = "unknown notify.status=${notify.status}" + ) + } + } + } }