Support friend list cache, close #408

This commit is contained in:
Him188 2021-02-06 22:52:41 +08:00
parent 6cb3aed2f0
commit 90d4030fe6
14 changed files with 401 additions and 107 deletions

View File

@ -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
/**
* 好友输入状态改变的事件当开始输入文字退出聊天窗口或清空输入框时会触发此事件

View File

@ -90,6 +90,8 @@ public interface FriendEvent : BotEvent, UserEvent {
override val user: Friend get() = friend
}
internal interface FriendInfoChangeEvent : BotEvent // for cache
/**
* 有关陌生人的事件
*/

View File

@ -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].
*

View File

@ -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)
String(this.readBytes(length.toInt()), charset = charset)
public fun File.createFileIfNotExists() {
if (!this.exists()) {
this.parentFile.mkdirs()
this.createNewFile()
}
}

View 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())
}

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View 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(),
)

View File

@ -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)) {

View File

@ -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

View File

@ -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()
}

View File

@ -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)
)
)
)

View 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()
}
}
)
}
}
}
}

View 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()
}
}
}