Support OtherClient list sync after login, support deviceName

This commit is contained in:
Him188 2020-12-24 23:37:52 +08:00
parent ad8ffa6cd4
commit 3ce6f092a1
18 changed files with 329 additions and 74 deletions

View File

@ -14,9 +14,7 @@
package net.mamoe.mirai package net.mamoe.mirai
import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent
import net.mamoe.mirai.event.events.MemberJoinRequestEvent import net.mamoe.mirai.event.events.MemberJoinRequestEvent
import net.mamoe.mirai.event.events.NewFriendRequestEvent import net.mamoe.mirai.event.events.NewFriendRequestEvent
@ -179,6 +177,14 @@ public interface IMirai : LowLevelApiAccessor {
message: String = "" message: String = ""
) )
/**
* 获取在线的 [OtherClient] 列表
*/
@JvmBlockingBridge
public suspend fun getOnlineOtherClientsList(
bot: Bot,
): List<OtherClientInfo>
/** /**
* 忽略加群验证需管理员权限 * 忽略加群验证需管理员权限
* *

View File

@ -12,21 +12,21 @@
package net.mamoe.mirai.contact package net.mamoe.mirai.contact
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.events.OtherClientOnlineEvent
import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PAD import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PAD
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PHONE import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PHONE
import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.MiraiInternalApi
/** /**
* 其他设备. 如当 [Bot] [ANDROID_PHONE] 登录时, 还可以有其他设备以 [ANDROID_PAD], iOS, PC 或其他设备登录. * 其他设备. 如当 [Bot] [ANDROID_PHONE] 登录时, 还可以有其他设备以 [ANDROID_PAD], iOS, PC 或其他设备登录.
*/ */
public interface OtherClient : Contact { public interface OtherClient : Contact {
/** public val info: OtherClientInfo
* 设备类型
*/
public val kind: ClientKind
/** /**
* 此设备属于的 [Bot] * 此设备属于的 [Bot]
@ -47,11 +47,72 @@ public interface OtherClient : Contact {
} }
} }
@MiraiInternalApi
public inline val OtherClient.appId: Int
get() = info.appId
public inline val OtherClient.platform: Platform get() = info.platform
public inline val OtherClient.deviceName: String get() = info.deviceName
public inline val OtherClient.deviceKind: String get() = info.deviceKind
@MiraiExperimentalApi
public data class OtherClientInfo @MiraiInternalApi constructor(
/**
* 仅运行时识别. 随着客户端更新此 ID 可能有变化.
*
* 不可能有 [appId] 相同的两个客户端t在线.
*/
public val appId: Int,
/**
* 登录平台
*/
public val platform: Platform,
/**
* 示例
* - Mi 10 Pro
* - 电脑
* - xxx iPad
* - mirai
*/
public val deviceName: String,
/**
* 示例
* - Mi 10 Pro
* - DESKTOP-ABCDEFG
* - iPad
* - mirai
*/
public val deviceKind: String,
)
/** /**
* 设备类型 * @see OtherClientInfo.platform
*/
public enum class Platform(
@MiraiInternalApi public val terminalId: Int,
@MiraiInternalApi public val platformId: Int,
) {
IOS(3, 1),
MOBILE(2, 2), // android
WINDOWS(1, 3),
UNKNOWN(0, 0)
;
public companion object {
@MiraiInternalApi
public fun getByTerminalId(terminalId: Int): Platform? = values().find { it.terminalId == terminalId }
}
}
/**
* 详细设备类型. 在登录时查询到的设备列表中无此信息. 只在 [OtherClientOnlineEvent] 才有.
*/ */
public enum class ClientKind( public enum class ClientKind(
public val id: Int, @MiraiInternalApi public val id: Int,
) { ) {
ANDROID_PAD(68104), ANDROID_PAD(68104),
AOL_CHAOJIHUIYUAN(73730), AOL_CHAOJIHUIYUAN(73730),
@ -76,9 +137,7 @@ public enum class ClientKind(
QQ_SERVICE(71170), QQ_SERVICE(71170),
TV_QQ(69130), TV_QQ(69130),
WIN8(69899), WIN8(69899),
WINPHONE(65804), WINPHONE(65804);
UNKNOWN(-1);
public companion object { public companion object {
public operator fun get(id: Int): ClientKind? = values().find { it.id == id } public operator fun get(id: Int): ClientKind? = values().find { it.id == id }

View File

@ -9,6 +9,7 @@
package net.mamoe.mirai.contact package net.mamoe.mirai.contact
import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.MiraiInternalApi import net.mamoe.mirai.utils.MiraiInternalApi
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -16,8 +17,9 @@ public class OtherClientList internal constructor(
@MiraiInternalApi @JvmField @MiraiInternalApi @JvmField
public val delegate: MutableCollection<OtherClient> = ConcurrentLinkedQueue() public val delegate: MutableCollection<OtherClient> = ConcurrentLinkedQueue()
) : Collection<OtherClient> by delegate { ) : Collection<OtherClient> by delegate {
public operator fun get(kind: ClientKind): OtherClient? = this.find { it.kind == kind } @MiraiExperimentalApi
public operator fun get(appId: Int): OtherClient? = this.find { it.appId == appId }
public fun getOrFail(kind: ClientKind): OtherClient = public fun getOrFail(appId: Int): OtherClient =
get(kind) ?: throw NoSuchElementException("OtherClient with kind=$kind not found.") get(appId) ?: throw NoSuchElementException("OtherClient with appId=$appId not found.")
} }

View File

@ -502,7 +502,7 @@ public class OtherClientMessageEvent constructor(
public override val senderName: String get() = sender.nick public override val senderName: String get() = sender.nick
public override val source: OnlineMessageSource.Incoming.FromFriend get() = message.source as OnlineMessageSource.Incoming.FromFriend public override val source: OnlineMessageSource.Incoming.FromFriend get() = message.source as OnlineMessageSource.Incoming.FromFriend
public override fun toString(): String = "OtherClientMessageEvent(client=${client.kind}, message=$message)" public override fun toString(): String = "OtherClientMessageEvent(client=${client.platform}, message=$message)"
} }
/** /**

View File

@ -10,9 +10,11 @@
package net.mamoe.mirai.event.events package net.mamoe.mirai.event.events
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.ClientKind
import net.mamoe.mirai.contact.OtherClient import net.mamoe.mirai.contact.OtherClient
import net.mamoe.mirai.event.AbstractEvent import net.mamoe.mirai.event.AbstractEvent
import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.utils.MiraiInternalApi
public interface OtherClientEvent : BotEvent, Packet { public interface OtherClientEvent : BotEvent, Packet {
public val client: OtherClient public val client: OtherClient
@ -23,13 +25,17 @@ public interface OtherClientEvent : BotEvent, Packet {
/** /**
* 其他设备上线 * 其他设备上线
*/ */
public data class OtherClientOnlineEvent( public data class OtherClientOnlineEvent @MiraiInternalApi constructor(
override val client: OtherClient override val client: OtherClient,
/**
* 详细设备类型通常非 `null`.
*/
val kind: ClientKind?
) : OtherClientEvent, AbstractEvent() ) : OtherClientEvent, AbstractEvent()
/** /**
* 其他设备离线 * 其他设备离线
*/ */
public data class OtherClientOfflineEvent( public data class OtherClientOfflineEvent(
override val client: OtherClient override val client: OtherClient,
) : OtherClientEvent, AbstractEvent() ) : OtherClientEvent, AbstractEvent()

View File

@ -28,6 +28,7 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.LongMsg
import net.mamoe.mirai.internal.network.protocol.packet.chat.* import net.mamoe.mirai.internal.network.protocol.packet.chat.*
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.internal.utils.MiraiPlatformUtils import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
import net.mamoe.mirai.internal.utils.encodeToString import net.mamoe.mirai.internal.utils.encodeToString
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
@ -156,6 +157,15 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
group.checkBotPermission(MemberPermission.ADMINISTRATOR) group.checkBotPermission(MemberPermission.ADMINISTRATOR)
} }
override suspend fun getOnlineOtherClientsList(bot: Bot): List<OtherClientInfo> {
bot.asQQAndroidBot()
val response = bot.network.run {
StatSvc.GetDevLoginInfo(bot.client).sendAndExpect<StatSvc.GetDevLoginInfo.Response>()
}
return response.deviceList.map { it.toOtherClientInfo() }
}
override suspend fun ignoreMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean) { override suspend fun ignoreMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean) {
checkGroupPermission(event.bot, event.groupId) { event::class.simpleName ?: "<anonymous class>" } checkGroupPermission(event.bot, event.groupId) { event::class.simpleName ?: "<anonymous class>" }
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")

View File

@ -23,7 +23,6 @@ import net.mamoe.mirai.internal.contact.checkIsGroupImpl
import net.mamoe.mirai.internal.message.* import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.internal.network.QQAndroidClient import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.data.jce.InstanceInfo
import net.mamoe.mirai.internal.network.protocol.packet.chat.* import net.mamoe.mirai.internal.network.protocol.packet.chat.*
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.network.LoginFailedException import net.mamoe.mirai.network.LoginFailedException
@ -41,10 +40,9 @@ internal fun Bot.asQQAndroidBot(): QQAndroidBot {
} }
internal fun QQAndroidBot.createOtherClient( internal fun QQAndroidBot.createOtherClient(
kind: ClientKind, info: OtherClientInfo,
instanceInfo: InstanceInfo,
): OtherClientImpl { ): OtherClientImpl {
return OtherClientImpl(this, coroutineContext, kind, instanceInfo) return OtherClientImpl(this, coroutineContext, info)
} }
@Suppress("INVISIBLE_MEMBER", "BooleanLiteralArgument", "OverridingDeprecatedMember") @Suppress("INVISIBLE_MEMBER", "BooleanLiteralArgument", "OverridingDeprecatedMember")

View File

@ -10,23 +10,18 @@
package net.mamoe.mirai.internal.contact package net.mamoe.mirai.internal.contact
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.ClientKind
import net.mamoe.mirai.contact.OtherClient import net.mamoe.mirai.contact.OtherClient
import net.mamoe.mirai.internal.network.protocol.data.jce.InstanceInfo import net.mamoe.mirai.contact.OtherClientInfo
import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.cast
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
internal val OtherClient.instanceInfo: InstanceInfo get() = this.cast<OtherClientImpl>().instanceInfo
internal class OtherClientImpl( internal class OtherClientImpl(
bot: Bot, bot: Bot,
coroutineContext: CoroutineContext, coroutineContext: CoroutineContext,
override val kind: ClientKind, override val info: OtherClientInfo,
val instanceInfo: InstanceInfo
) : OtherClient, AbstractContact(bot, coroutineContext) { ) : OtherClient, AbstractContact(bot, coroutineContext) {
override suspend fun sendMessage(message: Message): MessageReceipt<OtherClient> { override suspend fun sendMessage(message: Message): MessageReceipt<OtherClient> {
throw UnsupportedOperationException("OtherClientImpl.sendMessage is not yet supported.") throw UnsupportedOperationException("OtherClientImpl.sendMessage is not yet supported.")
@ -37,7 +32,7 @@ internal class OtherClientImpl(
} }
override fun toString(): String { override fun toString(): String {
return "OtherClient(bot=${bot.id},kind=$kind)" return "OtherClient(bot=${bot.id},deviceName=${info.deviceName},platform=${info.platform})"
} }
} }

View File

@ -107,7 +107,7 @@ internal fun net.mamoe.mirai.event.events.MessageEvent.logMessageReceived() {
"${sender.nick}(${sender.id}) -> $message".replaceMagicCodes() "${sender.nick}(${sender.id}) -> $message".replaceMagicCodes()
} }
is net.mamoe.mirai.event.events.OtherClientMessageEvent -> bot.logger.verbose { is net.mamoe.mirai.event.events.OtherClientMessageEvent -> bot.logger.verbose {
"${client.kind} -> $message".replaceMagicCodes() "${client.platform} -> $message".replaceMagicCodes()
} }
is GroupMessageSyncEvent -> bot.logger.verbose { is GroupMessageSyncEvent -> bot.logger.verbose {
renderGroupMessage(group, senderName, sender, message) renderGroupMessage(group, senderName, sender, message)

View File

@ -20,6 +20,8 @@ import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.buildPacket import kotlinx.io.core.buildPacket
import kotlinx.io.core.readBytes import kotlinx.io.core.readBytes
import net.mamoe.mirai.Mirai import net.mamoe.mirai.Mirai
import net.mamoe.mirai.contact.deviceName
import net.mamoe.mirai.contact.platform
import net.mamoe.mirai.event.* import net.mamoe.mirai.event.*
import net.mamoe.mirai.event.events.BotOfflineEvent import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.BotOnlineEvent import net.mamoe.mirai.event.events.BotOnlineEvent
@ -27,6 +29,7 @@ import net.mamoe.mirai.event.events.BotReloginEvent
import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.contact.* import net.mamoe.mirai.internal.contact.*
import net.mamoe.mirai.internal.createOtherClient
import net.mamoe.mirai.internal.network.protocol.data.jce.StTroopNum import net.mamoe.mirai.internal.network.protocol.data.jce.StTroopNum
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgSvc import net.mamoe.mirai.internal.network.protocol.data.proto.MsgSvc
import net.mamoe.mirai.internal.network.protocol.packet.* import net.mamoe.mirai.internal.network.protocol.packet.*
@ -247,11 +250,24 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
// println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}") // println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
registerClientOnline() registerClientOnline()
startHeartbeatJobOrKill() startHeartbeatJobOrKill()
bot.otherClientsLock.withLock {
updateOtherClientsList()
}
} }
private suspend fun registerClientOnline(timeoutMillis: Long = 3000) { private suspend fun registerClientOnline() {
StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>(timeoutMillis) StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>()
}
private suspend fun updateOtherClientsList() {
val list = Mirai.getOnlineOtherClientsList(bot)
bot.otherClients.delegate.clear()
bot.otherClients.delegate.addAll(list.map { bot.createOtherClient(it) })
bot.logger.info { "Online OtherClients: " + bot.otherClients.joinToString { "${it.deviceName}(${it.platform.name})" } }
} }
// caches // caches

View File

@ -6,18 +6,13 @@
* *
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
package net.mamoe.mirai.internal.network.protocol.data.jce package net.mamoe.mirai.internal.network.protocol.data.jce
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.mamoe.mirai.internal.utils.io.JceStruct import net.mamoe.mirai.internal.utils.io.JceStruct
import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId
@Suppress("ClassName", "SpellCheckingInspection")
@Serializable @Serializable
internal class shareData( internal class DeviceItemDes(
@JvmField @TarsId(0) val pkgname: String = "", @JvmField @TarsId(0) val vecItemDes: ByteArray
@JvmField @TarsId(1) val msgtail: String = "",
@JvmField @TarsId(2) val picurl: String = "",
@JvmField @TarsId(3) val url: String = ""
) : JceStruct ) : JceStruct

View File

@ -0,0 +1,91 @@
/*
* 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.network.protocol.data.jce
import kotlinx.serialization.Serializable
import net.mamoe.mirai.contact.OtherClientInfo
import net.mamoe.mirai.contact.Platform
import net.mamoe.mirai.internal.utils.io.JceStruct
import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId
@Serializable
internal data class SvcDevLoginInfo(
@JvmField @TarsId(0) val iAppId: Long,
// @JvmField @TarsId(1) val vecGuid: ByteArray? = null,
@JvmField @TarsId(2) val iLoginTime: Long,
@JvmField @TarsId(3) val iLoginPlatform: Long? = null, // 1: ios, 2: android, 3: windows, 4: symbian, 5: feature
@JvmField @TarsId(4) val loginLocation: String? = "",
@JvmField @TarsId(5) val deviceName: String? = "",
@JvmField @TarsId(6) val deviceTypeInfo: String? = "",
// @JvmField @TarsId(7) val stDeviceItemDes: DeviceItemDes? = null,
@JvmField @TarsId(8) val iTerType: Long? = null, // 1:windows, 2: mobile, 3: ios
@JvmField @TarsId(9) val iProductType: Long? = null, // always 0
@JvmField @TarsId(10) val iCanBeKicked: Long? = null // isOnline
) : JceStruct
internal fun SvcDevLoginInfo.toOtherClientInfo() = OtherClientInfo(
iAppId.toInt(),
Platform.getByTerminalId(iTerType?.toInt() ?: 0) ?: Platform.UNKNOWN,
deviceName.orEmpty(),
deviceTypeInfo.orEmpty()
)
/*
vecCurrentLoginDevInfo=[SvcDevLoginInfo#1676411955 {
deviceName=mirai
deviceTypeInfo=mirai
iAppId=0x000000002002E738(537061176)
iCanBeKicked=0x0000000000000001(1)
iLoginPlatform=0x0000000000000002(2)
iLoginTime=0x000000005FE4A45C(1608819804)
iProductType=0x0000000000000000(0)
iTerType=0x0000000000000002(2)
}, SvcDevLoginInfo#1676411955 {
deviceName=xxx的iPad
deviceTypeInfo=iPad
iAppId=0x000000002002FB7C(537066364)
iCanBeKicked=0x0000000000000001(1)
iLoginPlatform=0x0000000000000001(1)
iLoginTime=0x000000005FE4A418(1608819736)
iProductType=0x0000000000000000(0)
iTerType=0x0000000000000003(3)
}, SvcDevLoginInfo#1676411955 {
deviceName=Mi 10 Pro
deviceTypeInfo=Mi 10 Pro
iAppId=0x000000002002FBB7(537066423)
iCanBeKicked=0x0000000000000001(1)
iLoginPlatform=0x0000000000000002(2)
iLoginTime=0x000000005FE4A628(1608820264)
iProductType=0x0000000000000000(0)
iTerType=0x0000000000000002(2)
}, SvcDevLoginInfo#1676411955 {
deviceName=DESKTOP-KMQEB7V
deviceTypeInfo=电脑
iAppId=0x0000000000000001(1)
iCanBeKicked=0x0000000000000001(1)
iLoginPlatform=0x0000000000000003(3)
iLoginTime=0x000000005FE4A5C1(1608820161)
iProductType=0x0000000000000000(0)
iTerType=0x0000000000000001(1)
loginLocation=中国湖北省武汉市
}]
*/
@Serializable
internal class SvcReqGetDevLoginInfo(
@JvmField @TarsId(0) val vecGuid: ByteArray,
@JvmField @TarsId(1) val appName: String = "",
@JvmField @TarsId(2) val iLoginType: Long = 1L,
@JvmField @TarsId(3) val iTimeStamp: Long,
@JvmField @TarsId(4) val iNextItemIndex: Long,
@JvmField @TarsId(5) val iRequireMax: Long,
@JvmField @TarsId(6) val iGetDevListType: Long? = 7L // 1: online list 2: recent list? 4: getAuthLoginDevList?
) : JceStruct

View File

@ -84,7 +84,7 @@ internal class RequestPushGroupMsg(
@JvmField @TarsId(13) val uAppShareID: Long? = null, @JvmField @TarsId(13) val uAppShareID: Long? = null,
@JvmField @TarsId(14) val vGPicInfo: List<GPicInfo>? = null, @JvmField @TarsId(14) val vGPicInfo: List<GPicInfo>? = null,
@JvmField @TarsId(15) val vAppShareCookie: ByteArray? = null, @JvmField @TarsId(15) val vAppShareCookie: ByteArray? = null,
@JvmField @TarsId(16) val stShareData: shareData? = null, @JvmField @TarsId(16) val stShareData: ShareData? = null,
@JvmField @TarsId(17) val fromInstId: Long? = null, @JvmField @TarsId(17) val fromInstId: Long? = null,
@JvmField @TarsId(18) val stGroupMsgHead: GroupMsgHead? = null, @JvmField @TarsId(18) val stGroupMsgHead: GroupMsgHead? = null,
@JvmField @TarsId(19) val wUserActive: Int? = null, @JvmField @TarsId(19) val wUserActive: Int? = null,

View File

@ -0,0 +1,26 @@
/*
* 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.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
@Serializable
internal class SvcRspGetDevLoginInfo(
@JvmField @TarsId(0) val iResult: Int,
@JvmField @TarsId(1) val result: String? = "",
@JvmField @TarsId(2) val iNextItemIndex: Long,
@JvmField @TarsId(3) val iTotalItemCount: Long,
@JvmField @TarsId(4) val vecCurrentLoginDevInfo: List<SvcDevLoginInfo>? = null,
@JvmField @TarsId(5) val vecHistoryLoginDevInfo: List<SvcDevLoginInfo>? = null,
@JvmField @TarsId(6) val vecAuthLoginDevInfo: List<SvcDevLoginInfo>? = null
) : JceStruct

View File

@ -23,6 +23,7 @@ import net.mamoe.mirai.Mirai
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.contact.NormalMember import net.mamoe.mirai.contact.NormalMember
import net.mamoe.mirai.contact.appId
import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.AbstractEvent import net.mamoe.mirai.event.AbstractEvent
import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.Event
@ -409,7 +410,7 @@ private suspend fun MsgComm.Msg.transform(bot: QQAndroidBot): Packet? {
with(data.msgHeader ?: return null) { with(data.msgHeader ?: return null) {
if (srcUin != dstUin || dstUin != bot.id) return null if (srcUin != dstUin || dstUin != bot.id) return null
val client = bot.otherClients.find { it.instanceInfo.iAppId == srcInstId } val client = bot.otherClients.find { it.appId == srcInstId }
?: return null// don't compare with dstAppId. diff. ?: return null// don't compare with dstAppId. diff.
val chain = buildMessageChain { val chain = buildMessageChain {

View File

@ -13,7 +13,9 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.ByteReadPacket
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import net.mamoe.mirai.Mirai
import net.mamoe.mirai.contact.ClientKind import net.mamoe.mirai.contact.ClientKind
import net.mamoe.mirai.contact.appId
import net.mamoe.mirai.event.events.BotOfflineEvent import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.OtherClientOfflineEvent import net.mamoe.mirai.event.events.OtherClientOfflineEvent
import net.mamoe.mirai.event.events.OtherClientOnlineEvent import net.mamoe.mirai.event.events.OtherClientOnlineEvent
@ -22,6 +24,7 @@ import net.mamoe.mirai.internal.createOtherClient
import net.mamoe.mirai.internal.message.contextualBugReportException import net.mamoe.mirai.internal.message.contextualBugReportException
import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidClient import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.getRandomByteArray
import net.mamoe.mirai.internal.network.guid import net.mamoe.mirai.internal.network.guid
import net.mamoe.mirai.internal.network.protocol.data.jce.* import net.mamoe.mirai.internal.network.protocol.data.jce.*
import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x769 import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x769
@ -29,6 +32,7 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.StatSvcGetOnline
import net.mamoe.mirai.internal.network.protocol.packet.* import net.mamoe.mirai.internal.network.protocol.packet.*
import net.mamoe.mirai.internal.utils.* import net.mamoe.mirai.internal.utils.*
import net.mamoe.mirai.internal.utils.io.serialization.* import net.mamoe.mirai.internal.utils.io.serialization.*
import net.mamoe.mirai.utils.currentTimeMillis
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
@Suppress("EnumEntryName", "unused") @Suppress("EnumEntryName", "unused")
@ -107,9 +111,9 @@ internal class StatSvc {
writeJceStruct( writeJceStruct(
RequestPacket.serializer(), RequestPacket.serializer(),
RequestPacket( RequestPacket(
sServantName = "PushService", servantName = "PushService",
sFuncName = "SvcReqRegister", funcName = "SvcReqRegister",
iRequestId = 0, requestId = 0,
sBuffer = jceRequestSBuffer( sBuffer = jceRequestSBuffer(
"SvcReqRegister", "SvcReqRegister",
SvcReqRegister.serializer(), SvcReqRegister.serializer(),
@ -205,9 +209,9 @@ internal class StatSvc {
writeJceStruct( writeJceStruct(
RequestPacket.serializer(), RequestPacket.serializer(),
RequestPacket( RequestPacket(
sServantName = "StatSvc", servantName = "StatSvc",
sFuncName = "RspMSFForceOffline", funcName = "RspMSFForceOffline",
iRequestId = 0, requestId = 0,
sBuffer = jceRequestSBuffer( sBuffer = jceRequestSBuffer(
"RspMSFForceOffline", "RspMSFForceOffline",
RspMSFForceOffline.serializer(), RspMSFForceOffline.serializer(),
@ -230,25 +234,29 @@ internal class StatSvc {
bot.otherClientsLock.withLock { bot.otherClientsLock.withLock {
val notify = readUniPacket(SvcReqMSFLoginNotifyData.serializer()) val notify = readUniPacket(SvcReqMSFLoginNotifyData.serializer())
val kind = notify.iClientType?.toInt()?.let(ClientKind::get) ?: return null val appId = notify.iAppId.toInt()
when (notify.status.toInt()) { when (notify.status.toInt()) {
1 -> { 1 -> { // online
if (bot.otherClients.any { it.kind == kind }) return null if (bot.otherClients.any { it.appId == appId }) return null
val client = bot.createOtherClient(
kind, val info = Mirai.getOnlineOtherClientsList(bot).find { it.appId == appId }
notify.vecInstanceList?.find { it.iClientType == notify.iClientType }
?: throw contextualBugReportException( ?: throw contextualBugReportException(
"decode SvcReqMSFLoginNotify (OtherClient online)", "SvcReqMSFLoginNotify (OtherClient online)",
notify._miraiContentToString(), notify._miraiContentToString(),
additional = "Failed to find corresponding instanceInfo." additional = "Failed to find corresponding instanceInfo."
)) )
val client = bot.createOtherClient(info)
bot.otherClients.delegate.add(client) bot.otherClients.delegate.add(client)
OtherClientOnlineEvent(client) OtherClientOnlineEvent(
client,
ClientKind[notify.iClientType?.toInt() ?: 0]
)
} }
2 -> { 2 -> { // off
val client = bot.otherClients.find { it.kind == kind } ?: return null val client = bot.otherClients.find { it.appId == appId } ?: return null
client.cancel(CancellationException("Offline")) client.cancel(CancellationException("Offline"))
bot.otherClients.delegate.remove(client) bot.otherClients.delegate.remove(client)
OtherClientOfflineEvent(client) OtherClientOfflineEvent(client)
@ -262,4 +270,46 @@ internal class StatSvc {
} }
} }
} }
internal object GetDevLoginInfo : OutgoingPacketFactory<GetDevLoginInfo.Response>("StatSvc.GetDevLoginInfo") {
@Suppress("unused") // false positive
data class Response(
val deviceList: List<SvcDevLoginInfo>,
) : Packet {
override fun toString(): String {
return "StatSvc.GetDevLoginInfo.Response(deviceList.size=${deviceList.size})"
}
}
operator fun invoke(
client: QQAndroidClient,
) = buildOutgoingUniPacket(client) {
writeJceRequestPacket(
servantName = "StatSvc",
funcName = "SvcReqGetDevLoginInfo",
serializer = SvcReqGetDevLoginInfo.serializer(),
body = SvcReqGetDevLoginInfo(
iLoginType = 2,
iRequireMax = 20,
iTimeStamp = currentTimeMillis(),
iGetDevListType = 1,
vecGuid = getRandomByteArray(16), // 服务器防止频繁查询
iNextItemIndex = 0,
appName = client.protocol.apkId //"com.tencent.mobileqq"
)
)
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
val resp = readUniPacket(SvcRspGetDevLoginInfo.serializer())
// result 62 maybe too frequent
return Response(
resp.vecCurrentLoginDevInfo?.takeIf { it.isNotEmpty() }
?: resp.vecAuthLoginDevInfo?.takeIf { it.isNotEmpty() }
?: resp.vecHistoryLoginDevInfo.orEmpty()
)
}
}
} }

View File

@ -191,7 +191,7 @@ internal class TarsInput(
Tars.BYTE -> input.readByte().toInt() Tars.BYTE -> input.readByte().toInt()
Tars.SHORT -> input.readShort().toInt() Tars.SHORT -> input.readShort().toInt()
Tars.INT -> input.readInt() Tars.INT -> input.readInt()
else -> error("type mismatch: $head") else -> error("type mismatch: $head, expecting int.")
} }
} }
@ -200,7 +200,7 @@ internal class TarsInput(
Tars.ZERO_TYPE -> 0 Tars.ZERO_TYPE -> 0
Tars.BYTE -> input.readByte().toShort() Tars.BYTE -> input.readByte().toShort()
Tars.SHORT -> input.readShort() Tars.SHORT -> input.readShort()
else -> error("type mismatch: $head") else -> error("type mismatch: $head, expecting short.")
} }
} }
@ -211,7 +211,7 @@ internal class TarsInput(
Tars.SHORT -> input.readShort().toLong() Tars.SHORT -> input.readShort().toLong()
Tars.INT -> input.readInt().toLong() Tars.INT -> input.readInt().toLong()
Tars.LONG -> input.readLong() Tars.LONG -> input.readLong()
else -> error("type mismatch ${head.type}") else -> error("type mismatch ${head}, expecting long.")
} }
} }
@ -220,7 +220,7 @@ internal class TarsInput(
return when (head.type) { return when (head.type) {
Tars.ZERO_TYPE -> 0 Tars.ZERO_TYPE -> 0
Tars.BYTE -> input.readByte() Tars.BYTE -> input.readByte()
else -> error("type mismatch: $head") else -> error("type mismatch: $head, expecting byte.")
} }
} }
@ -228,7 +228,7 @@ internal class TarsInput(
return when (head.type) { return when (head.type) {
Tars.ZERO_TYPE -> 0f Tars.ZERO_TYPE -> 0f
Tars.FLOAT -> input.readFloat() Tars.FLOAT -> input.readFloat()
else -> error("type mismatch: $head") else -> error("type mismatch: $head, expecting float.")
} }
} }

View File

@ -96,18 +96,18 @@ internal fun <T : ProtoBuf> ByteReadPacket.readUniPacket(
} }
} }
private fun <K, V> Map<K, V>.firstValue(): V = this.entries.first().value private fun <K, V> Map<K, V>.singleValue(): V = this.entries.single().value
private fun <R> ByteReadPacket.decodeUniRequestPacketAndDeserialize(name: String? = null, block: (ByteArray) -> R): R { internal fun <R> ByteReadPacket.decodeUniRequestPacketAndDeserialize(name: String? = null, block: (ByteArray) -> R): R {
val request = this.readJceStruct(RequestPacket.serializer()) val request = this.readJceStruct(RequestPacket.serializer())
return block(if (name == null) when (request.version?.toInt() ?: 3) { return block(if (name == null) when (request.version?.toInt() ?: 3) {
2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.firstValue().firstValue() 2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.singleValue().singleValue()
3 -> request.sBuffer.loadAs(RequestDataVersion3.serializer()).map.firstValue() 3 -> request.sBuffer.loadAs(RequestDataVersion3.serializer()).map.singleValue()
else -> error("unsupported version ${request.version}") else -> error("unsupported version ${request.version}")
} else when (request.version?.toInt() ?: 3) { } else when (request.version?.toInt() ?: 3) {
2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.getOrElse(name) { error("cannot find $name") } 2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.getOrElse(name) { error("cannot find $name") }
.firstValue() .singleValue()
3 -> request.sBuffer.loadAs(RequestDataVersion3.serializer()).map.getOrElse(name) { error("cannot find $name") } 3 -> request.sBuffer.loadAs(RequestDataVersion3.serializer()).map.getOrElse(name) { error("cannot find $name") }
else -> error("unsupported version ${request.version}") else -> error("unsupported version ${request.version}")
}) })