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

View File

@ -12,21 +12,21 @@
package net.mamoe.mirai.contact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.events.OtherClientOnlineEvent
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.BotConfiguration.MiraiProtocol.ANDROID_PAD
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PHONE
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 或其他设备登录.
*/
public interface OtherClient : Contact {
/**
* 设备类型
*/
public val kind: ClientKind
public val info: OtherClientInfo
/**
* 此设备属于的 [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 val id: Int,
@MiraiInternalApi public val id: Int,
) {
ANDROID_PAD(68104),
AOL_CHAOJIHUIYUAN(73730),
@ -76,9 +137,7 @@ public enum class ClientKind(
QQ_SERVICE(71170),
TV_QQ(69130),
WIN8(69899),
WINPHONE(65804),
UNKNOWN(-1);
WINPHONE(65804);
public companion object {
public operator fun get(id: Int): ClientKind? = values().find { it.id == id }

View File

@ -9,6 +9,7 @@
package net.mamoe.mirai.contact
import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.MiraiInternalApi
import java.util.concurrent.ConcurrentLinkedQueue
@ -16,8 +17,9 @@ public class OtherClientList internal constructor(
@MiraiInternalApi @JvmField
public val delegate: MutableCollection<OtherClient> = ConcurrentLinkedQueue()
) : 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 =
get(kind) ?: throw NoSuchElementException("OtherClient with kind=$kind not found.")
public fun getOrFail(appId: Int): OtherClient =
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 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
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.ClientKind
import net.mamoe.mirai.contact.OtherClient
import net.mamoe.mirai.event.AbstractEvent
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.utils.MiraiInternalApi
public interface OtherClientEvent : BotEvent, Packet {
public val client: OtherClient
@ -23,13 +25,17 @@ public interface OtherClientEvent : BotEvent, Packet {
/**
* 其他设备上线
*/
public data class OtherClientOnlineEvent(
override val client: OtherClient
public data class OtherClientOnlineEvent @MiraiInternalApi constructor(
override val client: OtherClient,
/**
* 详细设备类型通常非 `null`.
*/
val kind: ClientKind?
) : OtherClientEvent, AbstractEvent()
/**
* 其他设备离线
*/
public data class OtherClientOfflineEvent(
override val client: OtherClient
override val client: OtherClient,
) : 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.voice.PttStore
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.encodeToString
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
@ -156,6 +157,15 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
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) {
checkGroupPermission(event.bot, event.groupId) { event::class.simpleName ?: "<anonymous class>" }
@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.network.QQAndroidBotNetworkHandler
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.message.data.*
import net.mamoe.mirai.network.LoginFailedException
@ -41,10 +40,9 @@ internal fun Bot.asQQAndroidBot(): QQAndroidBot {
}
internal fun QQAndroidBot.createOtherClient(
kind: ClientKind,
instanceInfo: InstanceInfo,
info: OtherClientInfo,
): OtherClientImpl {
return OtherClientImpl(this, coroutineContext, kind, instanceInfo)
return OtherClientImpl(this, coroutineContext, info)
}
@Suppress("INVISIBLE_MEMBER", "BooleanLiteralArgument", "OverridingDeprecatedMember")

View File

@ -10,23 +10,18 @@
package net.mamoe.mirai.internal.contact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.ClientKind
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.data.Image
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.cast
import kotlin.coroutines.CoroutineContext
internal val OtherClient.instanceInfo: InstanceInfo get() = this.cast<OtherClientImpl>().instanceInfo
internal class OtherClientImpl(
bot: Bot,
coroutineContext: CoroutineContext,
override val kind: ClientKind,
val instanceInfo: InstanceInfo
override val info: OtherClientInfo,
) : OtherClient, AbstractContact(bot, coroutineContext) {
override suspend fun sendMessage(message: Message): MessageReceipt<OtherClient> {
throw UnsupportedOperationException("OtherClientImpl.sendMessage is not yet supported.")
@ -37,7 +32,7 @@ internal class OtherClientImpl(
}
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()
}
is net.mamoe.mirai.event.events.OtherClientMessageEvent -> bot.logger.verbose {
"${client.kind} -> $message".replaceMagicCodes()
"${client.platform} -> $message".replaceMagicCodes()
}
is GroupMessageSyncEvent -> bot.logger.verbose {
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.readBytes
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.events.BotOfflineEvent
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.internal.QQAndroidBot
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.proto.MsgSvc
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()}")
registerClientOnline()
startHeartbeatJobOrKill()
bot.otherClientsLock.withLock {
updateOtherClientsList()
}
}
private suspend fun registerClientOnline(timeoutMillis: Long = 3000) {
StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>(timeoutMillis)
private suspend fun registerClientOnline() {
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

View File

@ -6,18 +6,13 @@
*
* 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 net.mamoe.mirai.internal.utils.io.JceStruct
import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId
@Suppress("ClassName", "SpellCheckingInspection")
@Serializable
internal class shareData(
@JvmField @TarsId(0) val pkgname: String = "",
@JvmField @TarsId(1) val msgtail: String = "",
@JvmField @TarsId(2) val picurl: String = "",
@JvmField @TarsId(3) val url: String = ""
internal class DeviceItemDes(
@JvmField @TarsId(0) val vecItemDes: ByteArray
) : 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(14) val vGPicInfo: List<GPicInfo>? = 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(18) val stGroupMsgHead: GroupMsgHead? = 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.MemberPermission
import net.mamoe.mirai.contact.NormalMember
import net.mamoe.mirai.contact.appId
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.AbstractEvent
import net.mamoe.mirai.event.Event
@ -409,7 +410,7 @@ private suspend fun MsgComm.Msg.transform(bot: QQAndroidBot): Packet? {
with(data.msgHeader ?: 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.
val chain = buildMessageChain {

View File

@ -13,7 +13,9 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.sync.withLock
import kotlinx.io.core.ByteReadPacket
import kotlinx.serialization.protobuf.ProtoBuf
import net.mamoe.mirai.Mirai
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.OtherClientOfflineEvent
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.network.Packet
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.protocol.data.jce.*
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.utils.*
import net.mamoe.mirai.internal.utils.io.serialization.*
import net.mamoe.mirai.utils.currentTimeMillis
import java.util.concurrent.CancellationException
@Suppress("EnumEntryName", "unused")
@ -107,9 +111,9 @@ internal class StatSvc {
writeJceStruct(
RequestPacket.serializer(),
RequestPacket(
sServantName = "PushService",
sFuncName = "SvcReqRegister",
iRequestId = 0,
servantName = "PushService",
funcName = "SvcReqRegister",
requestId = 0,
sBuffer = jceRequestSBuffer(
"SvcReqRegister",
SvcReqRegister.serializer(),
@ -205,9 +209,9 @@ internal class StatSvc {
writeJceStruct(
RequestPacket.serializer(),
RequestPacket(
sServantName = "StatSvc",
sFuncName = "RspMSFForceOffline",
iRequestId = 0,
servantName = "StatSvc",
funcName = "RspMSFForceOffline",
requestId = 0,
sBuffer = jceRequestSBuffer(
"RspMSFForceOffline",
RspMSFForceOffline.serializer(),
@ -230,25 +234,29 @@ internal class StatSvc {
bot.otherClientsLock.withLock {
val notify = readUniPacket(SvcReqMSFLoginNotifyData.serializer())
val kind = notify.iClientType?.toInt()?.let(ClientKind::get) ?: return null
val appId = notify.iAppId.toInt()
when (notify.status.toInt()) {
1 -> {
if (bot.otherClients.any { it.kind == kind }) return null
val client = bot.createOtherClient(
kind,
notify.vecInstanceList?.find { it.iClientType == notify.iClientType }
?: throw contextualBugReportException(
"decode SvcReqMSFLoginNotify (OtherClient online)",
notify._miraiContentToString(),
additional = "Failed to find corresponding instanceInfo."
))
1 -> { // online
if (bot.otherClients.any { it.appId == appId }) return null
val info = Mirai.getOnlineOtherClientsList(bot).find { it.appId == appId }
?: throw contextualBugReportException(
"SvcReqMSFLoginNotify (OtherClient online)",
notify._miraiContentToString(),
additional = "Failed to find corresponding instanceInfo."
)
val client = bot.createOtherClient(info)
bot.otherClients.delegate.add(client)
OtherClientOnlineEvent(client)
OtherClientOnlineEvent(
client,
ClientKind[notify.iClientType?.toInt() ?: 0]
)
}
2 -> {
val client = bot.otherClients.find { it.kind == kind } ?: return null
2 -> { // off
val client = bot.otherClients.find { it.appId == appId } ?: return null
client.cancel(CancellationException("Offline"))
bot.otherClients.delegate.remove(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.SHORT -> input.readShort().toInt()
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.BYTE -> input.readByte().toShort()
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.INT -> input.readInt().toLong()
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) {
Tars.ZERO_TYPE -> 0
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) {
Tars.ZERO_TYPE -> 0f
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())
return block(if (name == null) when (request.version?.toInt() ?: 3) {
2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.firstValue().firstValue()
3 -> request.sBuffer.loadAs(RequestDataVersion3.serializer()).map.firstValue()
2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.singleValue().singleValue()
3 -> request.sBuffer.loadAs(RequestDataVersion3.serializer()).map.singleValue()
else -> error("unsupported version ${request.version}")
} else when (request.version?.toInt() ?: 3) {
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") }
else -> error("unsupported version ${request.version}")
})