mirror of
https://github.com/mamoe/mirai.git
synced 2025-03-03 15:10:14 +08:00
Support friend list cache, close #408
This commit is contained in:
parent
6cb3aed2f0
commit
90d4030fe6
@ -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
|
||||
|
||||
/**
|
||||
* 好友输入状态改变的事件,当开始输入文字、退出聊天窗口或清空输入框时会触发此事件
|
||||
|
@ -90,6 +90,8 @@ public interface FriendEvent : BotEvent, UserEvent {
|
||||
override val user: Friend get() = friend
|
||||
}
|
||||
|
||||
internal interface FriendInfoChangeEvent : BotEvent // for cache
|
||||
|
||||
/**
|
||||
* 有关陌生人的事件
|
||||
*/
|
||||
|
@ -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].
|
||||
*
|
||||
|
@ -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
|
||||
|
||||
|
||||
@ -124,3 +125,10 @@ public inline fun Input.readString(length: UShort, charset: Charset = Charsets.U
|
||||
|
||||
public inline fun Input.readString(length: Byte, charset: Charset = Charsets.UTF_8): String =
|
||||
String(this.readBytes(length.toInt()), charset = charset)
|
||||
|
||||
public fun File.createFileIfNotExists() {
|
||||
if (!this.exists()) {
|
||||
this.parentFile.mkdirs()
|
||||
this.createNewFile()
|
||||
}
|
||||
}
|
24
mirai-core-utils/src/commonMain/kotlin/Serialization.kt
Normal file
24
mirai-core-utils/src/commonMain/kotlin/Serialization.kt
Normal file
@ -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 <T> File.loadAs(
|
||||
serializer: DeserializationStrategy<T>,
|
||||
stringFormat: StringFormat,
|
||||
): T? {
|
||||
if (!this.exists() || this.length() == 0L) {
|
||||
return null
|
||||
}
|
||||
return stringFormat.decodeFromString(serializer, this.readText())
|
||||
}
|
@ -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<QQAndroidBotNetworkHandler>(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<Friend> = 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<net.mamoe.mirai.event.events.FriendInfoChangeEvent> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<FriendList.GetFriendGroupList.Response>(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<FriendInfoImpl>) {
|
||||
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<FriendInfoImpl> {
|
||||
logger.info { "Start loading friend list..." }
|
||||
val friendInfos = mutableListOf<FriendInfoImpl>()
|
||||
|
||||
var count = 0
|
||||
var total: Short
|
||||
while (true) {
|
||||
val data = FriendList.GetFriendGroupList(
|
||||
bot.client, count, 150, 0, 0
|
||||
).sendAndExpect<FriendList.GetFriendGroupList.Response>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
30
mirai-core/src/commonMain/kotlin/network/FriendListCache.kt
Normal file
30
mirai-core/src/commonMain/kotlin/network/FriendListCache.kt
Normal file
@ -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<FriendInfoImpl> = emptyList(),
|
||||
)
|
@ -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<Packet?>("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)) {
|
||||
|
@ -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
|
@ -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()
|
||||
}
|
@ -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<Register.Response>("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)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
71
mirai-core/src/commonMain/kotlin/utils/ScheduledJob.kt
Normal file
71
mirai-core/src/commonMain/kotlin/utils/ScheduledJob.kt
Normal file
@ -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<Unit>(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()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
mirai-core/src/commonTest/kotlin/ScheduledJobTest.kt
Normal file
38
mirai-core/src/commonTest/kotlin/ScheduledJobTest.kt
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user