diff --git a/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt b/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt index bc309ce55..5a38dd007 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt @@ -32,7 +32,7 @@ public data class FriendRemarkChangeEvent internal constructor( public override val friend: Friend, public val oldRemark: String, public val newRemark: String -) : FriendEvent, Packet, AbstractEvent() +) : FriendEvent, Packet, AbstractEvent(), FriendInfoChangeEvent /** * 成功添加了一个新好友的事件 @@ -42,14 +42,14 @@ public data class FriendAddEvent @MiraiInternalApi constructor( * 新好友. 已经添加到 [Bot.friends] */ public override val friend: Friend -) : FriendEvent, Packet, AbstractEvent() +) : FriendEvent, Packet, AbstractEvent(), FriendInfoChangeEvent /** * 好友已被删除或主动删除的事件. */ public data class FriendDeleteEvent internal constructor( public override val friend: Friend -) : FriendEvent, Packet, AbstractEvent() +) : FriendEvent, Packet, AbstractEvent(), FriendInfoChangeEvent /** * 一个账号请求添加机器人为好友的事件 @@ -77,7 +77,7 @@ public data class NewFriendRequestEvent internal constructor( * 群名片或好友昵称 */ public val fromNick: String -) : BotEvent, Packet, AbstractEvent() { +) : BotEvent, Packet, AbstractEvent(), FriendInfoChangeEvent { @JvmField internal val responded: AtomicBoolean = AtomicBoolean(false) @@ -109,7 +109,7 @@ public data class FriendNickChangedEvent internal constructor( public override val friend: Friend, public val from: String, public val to: String -) : FriendEvent, Packet, AbstractEvent() +) : FriendEvent, Packet, AbstractEvent(), FriendInfoChangeEvent /** * 好友输入状态改变的事件,当开始输入文字、退出聊天窗口或清空输入框时会触发此事件 diff --git a/mirai-core-api/src/commonMain/kotlin/event/events/types.kt b/mirai-core-api/src/commonMain/kotlin/event/events/types.kt index 69b846251..9e90afb58 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/events/types.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/events/types.kt @@ -90,6 +90,8 @@ public interface FriendEvent : BotEvent, UserEvent { override val user: Friend get() = friend } +internal interface FriendInfoChangeEvent : BotEvent // for cache + /** * 有关陌生人的事件 */ diff --git a/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt b/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt index ac011c3b7..806fb727b 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt @@ -310,6 +310,43 @@ public open class BotConfiguration { // open for Java botLoggerSupplier = { _ -> SilentLogger } } + /////////////////////////////////////////////////////////////////////////// + // Cache + ////////////////////////////////////////////////////////////////////////// + + /** + * 非 `null` 时启用好友列表缓存, 加快初始化速度. 在启用后将会在下载好友列表后保存到文件, 并在修改时自动保存. + * @since 2.4 + * @see enableFriendListCache + */ + public var friendListCache: FriendListCache? = FriendListCache() + + /** + * 好友列表缓存设置. + * @since 2.4 + * @see friendListCache + */ + public class FriendListCache @JvmOverloads constructor( + /** + * 缓存文件位置, 相对于 [workingDir] 的路径. + */ + public val cacheFile: File = File("cache/friendList.json"), + /** + * 在有好友列表修改是 + */ + public val saveIntervalMillis: Long = 60_000, + ) + + /** + * 启用好友列表缓存. + * @since 2.4 + * @see BotConfiguration.enableFriendListCache + */ + public fun enableFriendListCache() { + friendListCache = FriendListCache() + } + + /** * 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext]. * diff --git a/mirai-core-utils/src/commonMain/kotlin/IO.kt b/mirai-core-utils/src/commonMain/kotlin/IO.kt index 2969e81d0..0899301b2 100644 --- a/mirai-core-utils/src/commonMain/kotlin/IO.kt +++ b/mirai-core-utils/src/commonMain/kotlin/IO.kt @@ -16,6 +16,7 @@ package net.mamoe.mirai.utils import io.ktor.utils.io.charsets.* import kotlinx.io.core.* +import java.io.File import kotlin.text.Charsets @@ -123,4 +124,11 @@ public inline fun Input.readString(length: UShort, charset: Charset = Charsets.U String(this.readBytes(length.toInt()), charset = charset) public inline fun Input.readString(length: Byte, charset: Charset = Charsets.UTF_8): String = - String(this.readBytes(length.toInt()), charset = charset) \ No newline at end of file + String(this.readBytes(length.toInt()), charset = charset) + +public fun File.createFileIfNotExists() { + if (!this.exists()) { + this.parentFile.mkdirs() + this.createNewFile() + } +} \ No newline at end of file diff --git a/mirai-core-utils/src/commonMain/kotlin/Serialization.kt b/mirai-core-utils/src/commonMain/kotlin/Serialization.kt new file mode 100644 index 000000000..fc2c64f20 --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/Serialization.kt @@ -0,0 +1,24 @@ +/* + * Copyright 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.utils + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.StringFormat +import java.io.File + +public fun File.loadAs( + serializer: DeserializationStrategy, + stringFormat: StringFormat, +): T? { + if (!this.exists() || this.length() == 0L) { + return null + } + return stringFormat.decodeFromString(serializer, this.readText()) +} diff --git a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt index 8dc5277fa..57d5eef1d 100644 --- a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt +++ b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt @@ -18,26 +18,25 @@ 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.info.FriendInfoImpl import net.mamoe.mirai.internal.contact.OtherClientImpl -import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl import net.mamoe.mirai.internal.contact.checkIsGroupImpl +import net.mamoe.mirai.internal.contact.info.FriendInfoImpl +import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl import net.mamoe.mirai.internal.contact.uin import net.mamoe.mirai.internal.message.* -import net.mamoe.mirai.internal.network.Packet -import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.* import net.mamoe.mirai.internal.network.handler.QQAndroidBotNetworkHandler import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType import net.mamoe.mirai.internal.network.protocol.packet.chat.* import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc -import net.mamoe.mirai.internal.network.useNextServers +import net.mamoe.mirai.internal.utils.ScheduledJob import net.mamoe.mirai.message.data.* import net.mamoe.mirai.network.LoginFailedException import net.mamoe.mirai.utils.* import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext -import net.mamoe.mirai.internal.network.protocol.data.jce.FriendInfo as JceFriendInfo +import kotlin.time.milliseconds internal fun Bot.asQQAndroidBot(): QQAndroidBot { contract { @@ -59,7 +58,7 @@ internal class QQAndroidBot constructor( configuration: BotConfiguration ) : AbstractBot(configuration, account.id) { var client: QQAndroidClient = initClient() - private set + private set fun initClient(): QQAndroidClient { client = QQAndroidClient( @@ -78,6 +77,40 @@ internal class QQAndroidBot constructor( override val friends: ContactList = ContactList() + val friendListCache: FriendListCache? by lazy { + configuration.friendListCache?.cacheFile?.run { + val ret = loadAs(FriendListCache.serializer(), JsonForCache) ?: FriendListCache() + + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + bot.eventChannel.parentScope(this@QQAndroidBot) + .subscribeAlways { + friendListSaver?.notice() + } + + ret + } + } + + private val friendListSaver by lazy { + configuration.friendListCache?.let { friendListCache: BotConfiguration.FriendListCache -> + + ScheduledJob(coroutineContext, friendListCache.saveIntervalMillis.milliseconds) { + runBIO { saveFriendCache() } + } + } + } + + fun saveFriendCache() { + val friendListCache = friendListCache + if (friendListCache != null) { + configuration.friendListCache?.cacheFile?.run { + createFileIfNotExists() + writeText(JsonForCache.encodeToString(FriendListCache.serializer(), friendListCache)) + bot.network.logger.info { "Saved ${friendListCache.list.size} friends to local cache." } + } + } + } + override lateinit var nick: String override val asFriend: Friend by lazy { @@ -100,7 +133,7 @@ internal class QQAndroidBot constructor( override suspend fun sendLogout() { network.run { - StatSvc.Register.offline(client). sendWithoutExpect() + StatSvc.Register.offline(client).sendWithoutExpect() } } diff --git a/mirai-core/src/commonMain/kotlin/network/ContactUpdater.kt b/mirai-core/src/commonMain/kotlin/network/ContactUpdater.kt index 43e5a0144..2d50bdf06 100644 --- a/mirai-core/src/commonMain/kotlin/network/ContactUpdater.kt +++ b/mirai-core/src/commonMain/kotlin/network/ContactUpdater.kt @@ -17,13 +17,17 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import net.mamoe.mirai.Mirai +import net.mamoe.mirai.data.FriendInfo import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.contact.FriendImpl import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.contact.info.FriendInfoImpl import net.mamoe.mirai.internal.contact.info.GroupInfoImpl import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl import net.mamoe.mirai.internal.contact.toMiraiFriendInfo import net.mamoe.mirai.internal.network.protocol.data.jce.StTroopNum +import net.mamoe.mirai.internal.network.protocol.data.jce.SvcRespRegister +import net.mamoe.mirai.internal.network.protocol.data.jce.isValid import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopManagement import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList import net.mamoe.mirai.internal.network.protocol.packet.list.StrangerList @@ -31,11 +35,8 @@ import net.mamoe.mirai.utils.info import net.mamoe.mirai.utils.retryCatching import net.mamoe.mirai.utils.verbose -internal interface ContactCache { -} - internal interface ContactUpdater { - suspend fun loadAll() + suspend fun loadAll(registerResp: SvcRespRegister) fun closeAllContacts(e: CancellationException) } @@ -44,9 +45,9 @@ internal class ContactUpdaterImpl( val bot: QQAndroidBot, ) : ContactUpdater { @Synchronized - override suspend fun loadAll() { + override suspend fun loadAll(registerResp: SvcRespRegister) { coroutineScope { - launch { reloadFriendList() } + launch { reloadFriendList(registerResp) } launch { reloadGroupList() } launch { reloadStrangerList() } } @@ -78,48 +79,80 @@ internal class ContactUpdaterImpl( /** * Don't use concurrently */ - private suspend fun reloadFriendList() = bot.network.run { + private suspend fun reloadFriendList(registerResp: SvcRespRegister) = bot.network.run { if (initFriendOk) { return } - logger.info { "Start loading friend list..." } - var currentFriendCount = 0 - var totalFriendCount: Short - while (true) { - val data = FriendList.GetFriendGroupList( - bot.client, currentFriendCount, 150, 0, 0 - ).sendAndExpect(timeoutMillis = 5000, retry = 2) + val friendListCache = bot.friendListCache - totalFriendCount = data.totalFriendCount - data.friendList.forEach { - // atomic - bot.friends.delegate.add( - FriendImpl(bot, bot.coroutineContext, it.toMiraiFriendInfo()) - ).also { currentFriendCount++ } + fun updateCacheSeq(list: List) { + bot.friendListCache?.apply { + friendListSeq = registerResp.iLargeSeq + timeStamp = registerResp.timeStamp + this.list = list + bot.saveFriendCache() } - logger.verbose { "Loading friend list: ${currentFriendCount}/${totalFriendCount}" } - if (currentFriendCount >= totalFriendCount) { - break - } - // delay(200) } - logger.info { "Successfully loaded friend list: $currentFriendCount in total" } + + suspend fun refreshFriendList(): List { + logger.info { "Start loading friend list..." } + val friendInfos = mutableListOf() + + var count = 0 + var total: Short + while (true) { + val data = FriendList.GetFriendGroupList( + bot.client, count, 150, 0, 0 + ).sendAndExpect(timeoutMillis = 5000, retry = 2) + + total = data.totalFriendCount + + for (jceInfo in data.friendList) { + friendInfos.add(jceInfo.toMiraiFriendInfo()) + } + + count += data.friendList.size + logger.verbose { "Loading friend list: ${count}/${total}" } + if (count >= total) break + } + logger.info { "Successfully loaded friend list: $count in total" } + return friendInfos + } + + val list = if (friendListCache?.isValid(registerResp) == true) { + val list = friendListCache.list + bot.network.logger.info { "Loaded ${list.size} friends from local cache." } + list + } else { + refreshFriendList().also { + updateCacheSeq(it) + } + } + + for (friendInfoImpl in list) { + addFriendToBot(friendInfoImpl) + } + + initFriendOk = true } - private suspend fun StTroopNum.reloadGroup() { + private fun addFriendToBot(it: FriendInfo) = + bot.friends.delegate.add(FriendImpl(bot, bot.coroutineContext, it)) + + private suspend fun addGroupToBot(stTroopNum: StTroopNum) { bot.groups.delegate.add( GroupImpl( bot = bot, coroutineContext = bot.coroutineContext, - id = groupCode, - groupInfo = GroupInfoImpl(this), + id = stTroopNum.groupCode, + groupInfo = GroupInfoImpl(stTroopNum), members = Mirai.getRawGroupMemberList( bot, - groupUin, - groupCode, - dwGroupOwnerUin + stTroopNum.groupUin, + stTroopNum.groupCode, + stTroopNum.dwGroupOwnerUin ) ) ) @@ -165,7 +198,7 @@ internal class ContactUpdaterImpl( troopListData.groups.forEach { group -> launch { semaphore.withPermit { - retryCatching(5) { group.reloadGroup() }.getOrThrow() + retryCatching(5) { addGroupToBot(group) }.getOrThrow() } } } diff --git a/mirai-core/src/commonMain/kotlin/network/FriendListCache.kt b/mirai-core/src/commonMain/kotlin/network/FriendListCache.kt new file mode 100644 index 000000000..b4786c15d --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/FriendListCache.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2021 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 + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import net.mamoe.mirai.internal.contact.info.FriendInfoImpl + +internal val JsonForCache = Json { + encodeDefaults = true + ignoreUnknownKeys = true + isLenient = true +} + +@Serializable +internal data class FriendListCache( + var friendListSeq: Long = 0, + /** + * 实际上是个序列号, 不是时间 + */ + var timeStamp: Long = 0, + var list: List = emptyList(), +) \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/handler/QQAndroidBotNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/network/handler/QQAndroidBotNetworkHandler.kt index aed759b6d..fee835efa 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/QQAndroidBotNetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/QQAndroidBotNetworkHandler.kt @@ -265,13 +265,8 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo // println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}") registerClientOnline() - startHeartbeatJobOrKill() - bot.otherClientsLock.withLock { - updateOtherClientsList() - } - launch { while (isActive) { bot.client.wLoginSigInfo.sKey.run { @@ -292,17 +287,17 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo WtLogin15(bot.client).sendAndExpect() } - private suspend fun registerClientOnline() { + private suspend fun registerClientOnline(): StatSvc.Register.Response { // object : OutgoingPacketFactory("push.proxyUnRegister") { // override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Packet? { // return null // } // }.buildOutgoingUniPacket(bot.client) {}.sendWithoutExpect() - kotlin.runCatching { - StatSvc.Register.offline(bot.client).sendAndExpect() - }.getOrElse { logger.warning(it) } + // kotlin.runCatching { + // StatSvc.Register.offline(bot.client).sendAndExpect() + // }.getOrElse { logger.warning(it) } - StatSvc.Register.online(bot.client).sendAndExpect() + return StatSvc.Register.online(bot.client).sendAndExpect() } private suspend fun updateOtherClientsList() { @@ -332,23 +327,34 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo check(bot.isActive) { "bot is dead therefore network can't init." } check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't init." } - contactUpdater.closeAllContacts(CancellationException("re-init")) + contactUpdater.closeAllContacts(CancellationException("re-init")) if (!pendingEnabled) { pendingIncomingPackets = ConcurrentLinkedQueue() _pendingEnabled.value = true } - contactUpdater.loadAll() + val registerResp = registerClientOnline() this@QQAndroidBotNetworkHandler.launch(CoroutineName("Awaiting ConfigPushSvc.PushReq"), block= ConfigPushSyncer()) - syncMessageSvc() + launch { + syncMessageSvc() + } + + launch { + bot.otherClientsLock.withLock { + updateOtherClientsList() + } + } + + contactUpdater.loadAll(registerResp.origin) bot.firstLoginSucceed = true postInitActions() } + @Suppress("FunctionName") private fun BotNetworkHandler.ConfigPushSyncer(): suspend CoroutineScope.() -> Unit = launch@{ logger.info { "Awaiting ConfigPushSvc.PushReq." } when (val resp: ConfigPushSvc.PushReq.PushReqResponse? = nextEventOrNull(20_000)) { diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcReqRegister.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcReqRegister.kt index dd1c5aae6..9d1ff5990 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcReqRegister.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcReqRegister.kt @@ -15,41 +15,41 @@ import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId @Serializable internal class SvcReqRegister( - @TarsId(0) @JvmField val lUin: Long = 0L, - @TarsId(1) @JvmField val lBid: Long = 0L, - @TarsId(2) @JvmField val cConnType: Byte = 0, - @TarsId(3) @JvmField val sOther: String = "", - @TarsId(4) @JvmField val iStatus: Int = 11, - @TarsId(5) @JvmField val bOnlinePush: Byte = 0, - @TarsId(6) @JvmField val bIsOnline: Byte = 0, - @TarsId(7) @JvmField val bIsShowOnline: Byte = 0, - @TarsId(8) @JvmField val bKikPC: Byte = 0, - @TarsId(9) @JvmField val bKikWeak: Byte = 0, - @TarsId(10) @JvmField val timeStamp: Long = 0L, - @TarsId(11) @JvmField val iOSVersion: Long = 0L, - @TarsId(12) @JvmField val cNetType: Byte = 0, - @TarsId(13) @JvmField val sBuildVer: String? = "", - @TarsId(14) @JvmField val bRegType: Byte = 0, - @TarsId(15) @JvmField val vecDevParam: ByteArray? = null, - @TarsId(16) @JvmField val vecGuid: ByteArray? = null, - @TarsId(17) @JvmField val iLocaleID: Int = 2052, - @TarsId(18) @JvmField val bSlientPush: Byte = 0, - @TarsId(19) @JvmField val strDevName: String? = null, - @TarsId(20) @JvmField val strDevType: String? = null, - @TarsId(21) @JvmField val strOSVer: String? = null, - @TarsId(22) @JvmField val bOpenPush: Byte, - @TarsId(23) @JvmField val iLargeSeq: Long, - @TarsId(24) @JvmField val iLastWatchStartTime: Long = 0L, - @TarsId(26) @JvmField val uOldSSOIp: Long = 0L, - @TarsId(27) @JvmField val uNewSSOIp: Long = 0L, - @TarsId(28) @JvmField val sChannelNo: String? = null, - @TarsId(29) @JvmField val lCpId: Long = 0L, - @TarsId(30) @JvmField val strVendorName: String? = null, - @TarsId(31) @JvmField val strVendorOSName: String? = null, - @TarsId(32) @JvmField val strIOSIdfa: String? = null, - @TarsId(33) @JvmField val bytes_0x769_reqbody: ByteArray? = null, - @TarsId(34) @JvmField val bIsSetStatus: Byte = 0, - @TarsId(35) @JvmField val vecServerBuf: ByteArray? = null, - @TarsId(36) @JvmField val bSetMute: Byte = 0 + @TarsId(0) @JvmField var lUin: Long = 0L, + @TarsId(1) @JvmField var lBid: Long = 0L, + @TarsId(2) @JvmField var cConnType: Byte = 0, + @TarsId(3) @JvmField var sOther: String = "", + @TarsId(4) @JvmField var iStatus: Int = 11, + @TarsId(5) @JvmField var bOnlinePush: Byte = 0, + @TarsId(6) @JvmField var bIsOnline: Byte = 0, + @TarsId(7) @JvmField var bIsShowOnline: Byte = 0, + @TarsId(8) @JvmField var bKikPC: Byte = 0, + @TarsId(9) @JvmField var bKikWeak: Byte = 0, + @TarsId(10) @JvmField var timeStamp: Long = 0L, + @TarsId(11) @JvmField var iOSVersion: Long = 0L, + @TarsId(12) @JvmField var cNetType: Byte = 0, + @TarsId(13) @JvmField var sBuildVer: String? = "", + @TarsId(14) @JvmField var bRegType: Byte = 0, + @TarsId(15) @JvmField var vecDevParam: ByteArray? = null, + @TarsId(16) @JvmField var vecGuid: ByteArray? = null, + @TarsId(17) @JvmField var iLocaleID: Int = 2052, + @TarsId(18) @JvmField var bSlientPush: Byte = 0, + @TarsId(19) @JvmField var strDevName: String? = null, + @TarsId(20) @JvmField var strDevType: String? = null, + @TarsId(21) @JvmField var strOSVer: String? = null, + @TarsId(22) @JvmField var bOpenPush: Byte, + @TarsId(23) @JvmField var iLargeSeq: Long, + @TarsId(24) @JvmField var iLastWatchStartTime: Long = 0L, + @TarsId(26) @JvmField var uOldSSOIp: Long = 0L, + @TarsId(27) @JvmField var uNewSSOIp: Long = 0L, + @TarsId(28) @JvmField var sChannelNo: String? = null, + @TarsId(29) @JvmField var lCpId: Long = 0L, + @TarsId(30) @JvmField var strVendorName: String? = null, + @TarsId(31) @JvmField var strVendorOSName: String? = null, + @TarsId(32) @JvmField var strIOSIdfa: String? = null, + @TarsId(33) @JvmField var bytes_0x769_reqbody: ByteArray? = null, + @TarsId(34) @JvmField var bIsSetStatus: Byte = 0, + @TarsId(35) @JvmField var vecServerBuf: ByteArray? = null, + @TarsId(36) @JvmField var bSetMute: Byte = 0 // @SerialId(25) var vecBindUin: ArrayList<*>? = null // ?? 未知泛型 ) : JceStruct \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcRespRegister.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcRespRegister.kt index 0053f81ed..308d493c7 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcRespRegister.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/SvcRespRegister.kt @@ -10,6 +10,7 @@ package net.mamoe.mirai.internal.network.protocol.data.jce import kotlinx.serialization.Serializable +import net.mamoe.mirai.internal.network.FriendListCache import net.mamoe.mirai.internal.utils.io.JceStruct import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId @@ -30,7 +31,15 @@ internal class SvcRespRegister( @JvmField @TarsId(11) val iClientPort: Int = 0, @JvmField @TarsId(12) val iHelloInterval: Int = 300, @JvmField @TarsId(13) val iLargeSeq: Long = 0L, + /** + * =1 好友列表更新 + */ @JvmField @TarsId(14) val largeSeqUpdate: Byte = 0, @JvmField @TarsId(15) val bytes_0x769_rspBody: ByteArray? = null, @JvmField @TarsId(16) val iStatus: Int? = 0 ) : JceStruct + +internal fun FriendListCache.isValid(svcRespRegister: SvcRespRegister): Boolean { + return svcRespRegister.iLargeSeq == friendListSeq && svcRespRegister.timeStamp == timeStamp +// return this.largeSeqUpdate != 0.toByte() +} \ No newline at end of file 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 da02eb756..c4706eca1 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 @@ -25,6 +25,7 @@ import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.contact.appId import net.mamoe.mirai.internal.createOtherClient import net.mamoe.mirai.internal.message.contextualBugReportException +import net.mamoe.mirai.internal.network.FriendListCache import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.QQAndroidClient import net.mamoe.mirai.internal.network.getRandomByteArray @@ -94,29 +95,30 @@ internal class StatSvc { internal object Register : OutgoingPacketFactory("StatSvc.register") { - internal object Response : Packet { + internal class Response( + val origin: SvcRespRegister + ) : Packet { override fun toString(): String = "Response(StatSvc.register)" } override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { val packet = readUniPacket(SvcRespRegister.serializer()) - if (packet.updateFlag.toInt() == 1) { - //TODO 加载好友列表 - } - if (packet.largeSeqUpdate.toInt() == 1) { - //TODO 刷新好友列表 - } packet.iHelloInterval.let { bot.configuration.heartbeatPeriodMillis = it.times(1000).toLong() } - return Response + return Response(packet) } fun online( client: QQAndroidClient, regPushReason: RegPushReason = RegPushReason.appRegister - ) = impl(client, 1 or 2 or 4, client.onlineStatus, regPushReason) + ) = impl(client, 1 or 2 or 4, client.onlineStatus, regPushReason) { + client.bot.friendListCache?.let { friendListCache: FriendListCache -> + iLargeSeq = friendListCache.friendListSeq + // timeStamp = friendListCache.timeStamp + } + } fun offline( client: QQAndroidClient, @@ -127,7 +129,8 @@ internal class StatSvc { client: QQAndroidClient, bid: Long, status: OnlineStatus, - regPushReason: RegPushReason = RegPushReason.appRegister + regPushReason: RegPushReason = RegPushReason.appRegister, + applyAction: SvcReqRegister.() -> Unit = {} ) = buildLoginOutgoingPacket( client, bodyType = 1, @@ -198,7 +201,7 @@ internal class StatSvc { ) ), bSetMute = 0 - ) + ).apply(applyAction) ) ) ) diff --git a/mirai-core/src/commonMain/kotlin/utils/ScheduledJob.kt b/mirai-core/src/commonMain/kotlin/utils/ScheduledJob.kt new file mode 100644 index 000000000..4a5fa0d5b --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/utils/ScheduledJob.kt @@ -0,0 +1,71 @@ +/* + * Copyright 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.utils + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.sample +import kotlin.coroutines.CoroutineContext +import kotlin.time.Duration + +@OptIn(FlowPreview::class) +internal class ScheduledJob( + coroutineContext: CoroutineContext, + val interval: Duration, + private val task: suspend () -> Unit +) : CoroutineScope by CoroutineScope(coroutineContext + SupervisorJob(coroutineContext[Job])) { + private val coroutineExceptionHandler = + coroutineContext[CoroutineExceptionHandler].also { + requireNotNull(it) { + "Could not init ScheduledJob, coroutineExceptionHandler == null" + } + } + + private val channel = Channel(Channel.CONFLATED) + + fun notice() { + if (interval == Duration.ZERO) { + launch { task() } + } else channel.offer(Unit) + } + + private suspend fun doTask() { + runCatching { + task() + }.onFailure { + coroutineExceptionHandler!!.handleException(currentCoroutineContext(), it) + } + } + + init { + if (interval != Duration.ZERO) { + launch { + channel.receiveAsFlow() + .runCatching { + sample(interval.toLongMilliseconds()) + } + .fold( + onSuccess = { flow -> + flow.collect { doTask() } + }, + onFailure = { + // binary change + while (isActive) { + delay(interval) + task() + } + } + ) + } + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/ScheduledJobTest.kt b/mirai-core/src/commonTest/kotlin/ScheduledJobTest.kt new file mode 100644 index 000000000..d46c84aa3 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/ScheduledJobTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2021 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.utils + +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertEquals +import kotlin.time.seconds + +internal class ScheduledJobTest { + @Test + fun testScheduledJob() { + runBlocking { + val scope = CoroutineScope(CoroutineExceptionHandler { _, throwable -> + throwable.printStackTrace() + }) + val invoked = AtomicInteger(0) + val job = ScheduledJob(scope.coroutineContext, 1.seconds) { + invoked.incrementAndGet() + } + delay(100) + assertEquals(0, invoked.get()) + job.notice() + job.notice() + job.notice() + } + } +} \ No newline at end of file