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 override val friend: Friend,
|
||||||
public val oldRemark: String,
|
public val oldRemark: String,
|
||||||
public val newRemark: String
|
public val newRemark: String
|
||||||
) : FriendEvent, Packet, AbstractEvent()
|
) : FriendEvent, Packet, AbstractEvent(), FriendInfoChangeEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 成功添加了一个新好友的事件
|
* 成功添加了一个新好友的事件
|
||||||
@ -42,14 +42,14 @@ public data class FriendAddEvent @MiraiInternalApi constructor(
|
|||||||
* 新好友. 已经添加到 [Bot.friends]
|
* 新好友. 已经添加到 [Bot.friends]
|
||||||
*/
|
*/
|
||||||
public override val friend: Friend
|
public override val friend: Friend
|
||||||
) : FriendEvent, Packet, AbstractEvent()
|
) : FriendEvent, Packet, AbstractEvent(), FriendInfoChangeEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 好友已被删除或主动删除的事件.
|
* 好友已被删除或主动删除的事件.
|
||||||
*/
|
*/
|
||||||
public data class FriendDeleteEvent internal constructor(
|
public data class FriendDeleteEvent internal constructor(
|
||||||
public override val friend: Friend
|
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
|
public val fromNick: String
|
||||||
) : BotEvent, Packet, AbstractEvent() {
|
) : BotEvent, Packet, AbstractEvent(), FriendInfoChangeEvent {
|
||||||
@JvmField
|
@JvmField
|
||||||
internal val responded: AtomicBoolean = AtomicBoolean(false)
|
internal val responded: AtomicBoolean = AtomicBoolean(false)
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ public data class FriendNickChangedEvent internal constructor(
|
|||||||
public override val friend: Friend,
|
public override val friend: Friend,
|
||||||
public val from: String,
|
public val from: String,
|
||||||
public val to: 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
|
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 }
|
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].
|
* 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext].
|
||||||
*
|
*
|
||||||
|
@ -16,6 +16,7 @@ package net.mamoe.mirai.utils
|
|||||||
|
|
||||||
import io.ktor.utils.io.charsets.*
|
import io.ktor.utils.io.charsets.*
|
||||||
import kotlinx.io.core.*
|
import kotlinx.io.core.*
|
||||||
|
import java.io.File
|
||||||
import kotlin.text.Charsets
|
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)
|
String(this.readBytes(length.toInt()), charset = charset)
|
||||||
|
|
||||||
public inline fun Input.readString(length: Byte, charset: Charset = Charsets.UTF_8): String =
|
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()
|
||||||
|
}
|
||||||
|
}
|
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.Mirai
|
||||||
import net.mamoe.mirai.contact.*
|
import net.mamoe.mirai.contact.*
|
||||||
import net.mamoe.mirai.data.*
|
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.OtherClientImpl
|
||||||
import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl
|
|
||||||
import net.mamoe.mirai.internal.contact.checkIsGroupImpl
|
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.contact.uin
|
||||||
import net.mamoe.mirai.internal.message.*
|
import net.mamoe.mirai.internal.message.*
|
||||||
import net.mamoe.mirai.internal.network.Packet
|
import net.mamoe.mirai.internal.network.*
|
||||||
import net.mamoe.mirai.internal.network.QQAndroidClient
|
|
||||||
import net.mamoe.mirai.internal.network.handler.QQAndroidBotNetworkHandler
|
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.OutgoingPacket
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
|
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.chat.*
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
|
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.message.data.*
|
||||||
import net.mamoe.mirai.network.LoginFailedException
|
import net.mamoe.mirai.network.LoginFailedException
|
||||||
import net.mamoe.mirai.utils.*
|
import net.mamoe.mirai.utils.*
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
import kotlin.coroutines.CoroutineContext
|
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 {
|
internal fun Bot.asQQAndroidBot(): QQAndroidBot {
|
||||||
contract {
|
contract {
|
||||||
@ -59,7 +58,7 @@ internal class QQAndroidBot constructor(
|
|||||||
configuration: BotConfiguration
|
configuration: BotConfiguration
|
||||||
) : AbstractBot<QQAndroidBotNetworkHandler>(configuration, account.id) {
|
) : AbstractBot<QQAndroidBotNetworkHandler>(configuration, account.id) {
|
||||||
var client: QQAndroidClient = initClient()
|
var client: QQAndroidClient = initClient()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun initClient(): QQAndroidClient {
|
fun initClient(): QQAndroidClient {
|
||||||
client = QQAndroidClient(
|
client = QQAndroidClient(
|
||||||
@ -78,6 +77,40 @@ internal class QQAndroidBot constructor(
|
|||||||
|
|
||||||
override val friends: ContactList<Friend> = ContactList()
|
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 lateinit var nick: String
|
||||||
|
|
||||||
override val asFriend: Friend by lazy {
|
override val asFriend: Friend by lazy {
|
||||||
@ -100,7 +133,7 @@ internal class QQAndroidBot constructor(
|
|||||||
|
|
||||||
override suspend fun sendLogout() {
|
override suspend fun sendLogout() {
|
||||||
network.run {
|
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.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import net.mamoe.mirai.Mirai
|
import net.mamoe.mirai.Mirai
|
||||||
|
import net.mamoe.mirai.data.FriendInfo
|
||||||
import net.mamoe.mirai.internal.QQAndroidBot
|
import net.mamoe.mirai.internal.QQAndroidBot
|
||||||
import net.mamoe.mirai.internal.contact.FriendImpl
|
import net.mamoe.mirai.internal.contact.FriendImpl
|
||||||
import net.mamoe.mirai.internal.contact.GroupImpl
|
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.GroupInfoImpl
|
||||||
import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl
|
import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl
|
||||||
import net.mamoe.mirai.internal.contact.toMiraiFriendInfo
|
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.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.chat.TroopManagement
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
|
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
|
||||||
import net.mamoe.mirai.internal.network.protocol.packet.list.StrangerList
|
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.retryCatching
|
||||||
import net.mamoe.mirai.utils.verbose
|
import net.mamoe.mirai.utils.verbose
|
||||||
|
|
||||||
internal interface ContactCache {
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface ContactUpdater {
|
internal interface ContactUpdater {
|
||||||
suspend fun loadAll()
|
suspend fun loadAll(registerResp: SvcRespRegister)
|
||||||
|
|
||||||
fun closeAllContacts(e: CancellationException)
|
fun closeAllContacts(e: CancellationException)
|
||||||
}
|
}
|
||||||
@ -44,9 +45,9 @@ internal class ContactUpdaterImpl(
|
|||||||
val bot: QQAndroidBot,
|
val bot: QQAndroidBot,
|
||||||
) : ContactUpdater {
|
) : ContactUpdater {
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override suspend fun loadAll() {
|
override suspend fun loadAll(registerResp: SvcRespRegister) {
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
launch { reloadFriendList() }
|
launch { reloadFriendList(registerResp) }
|
||||||
launch { reloadGroupList() }
|
launch { reloadGroupList() }
|
||||||
launch { reloadStrangerList() }
|
launch { reloadStrangerList() }
|
||||||
}
|
}
|
||||||
@ -78,48 +79,80 @@ internal class ContactUpdaterImpl(
|
|||||||
/**
|
/**
|
||||||
* Don't use concurrently
|
* Don't use concurrently
|
||||||
*/
|
*/
|
||||||
private suspend fun reloadFriendList() = bot.network.run {
|
private suspend fun reloadFriendList(registerResp: SvcRespRegister) = bot.network.run {
|
||||||
if (initFriendOk) {
|
if (initFriendOk) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info { "Start loading friend list..." }
|
val friendListCache = bot.friendListCache
|
||||||
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)
|
|
||||||
|
|
||||||
totalFriendCount = data.totalFriendCount
|
fun updateCacheSeq(list: List<FriendInfoImpl>) {
|
||||||
data.friendList.forEach {
|
bot.friendListCache?.apply {
|
||||||
// atomic
|
friendListSeq = registerResp.iLargeSeq
|
||||||
bot.friends.delegate.add(
|
timeStamp = registerResp.timeStamp
|
||||||
FriendImpl(bot, bot.coroutineContext, it.toMiraiFriendInfo())
|
this.list = list
|
||||||
).also { currentFriendCount++ }
|
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
|
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(
|
bot.groups.delegate.add(
|
||||||
GroupImpl(
|
GroupImpl(
|
||||||
bot = bot,
|
bot = bot,
|
||||||
coroutineContext = bot.coroutineContext,
|
coroutineContext = bot.coroutineContext,
|
||||||
id = groupCode,
|
id = stTroopNum.groupCode,
|
||||||
groupInfo = GroupInfoImpl(this),
|
groupInfo = GroupInfoImpl(stTroopNum),
|
||||||
members = Mirai.getRawGroupMemberList(
|
members = Mirai.getRawGroupMemberList(
|
||||||
bot,
|
bot,
|
||||||
groupUin,
|
stTroopNum.groupUin,
|
||||||
groupCode,
|
stTroopNum.groupCode,
|
||||||
dwGroupOwnerUin
|
stTroopNum.dwGroupOwnerUin
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -165,7 +198,7 @@ internal class ContactUpdaterImpl(
|
|||||||
troopListData.groups.forEach { group ->
|
troopListData.groups.forEach { group ->
|
||||||
launch {
|
launch {
|
||||||
semaphore.withPermit {
|
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()}")
|
// println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
|
||||||
registerClientOnline()
|
registerClientOnline()
|
||||||
|
|
||||||
startHeartbeatJobOrKill()
|
startHeartbeatJobOrKill()
|
||||||
|
|
||||||
bot.otherClientsLock.withLock {
|
|
||||||
updateOtherClientsList()
|
|
||||||
}
|
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
bot.client.wLoginSigInfo.sKey.run {
|
bot.client.wLoginSigInfo.sKey.run {
|
||||||
@ -292,17 +287,17 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
|
|||||||
WtLogin15(bot.client).sendAndExpect()
|
WtLogin15(bot.client).sendAndExpect()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun registerClientOnline() {
|
private suspend fun registerClientOnline(): StatSvc.Register.Response {
|
||||||
// object : OutgoingPacketFactory<Packet?>("push.proxyUnRegister") {
|
// object : OutgoingPacketFactory<Packet?>("push.proxyUnRegister") {
|
||||||
// override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Packet? {
|
// override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Packet? {
|
||||||
// return null
|
// return null
|
||||||
// }
|
// }
|
||||||
// }.buildOutgoingUniPacket(bot.client) {}.sendWithoutExpect()
|
// }.buildOutgoingUniPacket(bot.client) {}.sendWithoutExpect()
|
||||||
kotlin.runCatching {
|
// kotlin.runCatching {
|
||||||
StatSvc.Register.offline(bot.client).sendAndExpect()
|
// StatSvc.Register.offline(bot.client).sendAndExpect()
|
||||||
}.getOrElse { logger.warning(it) }
|
// }.getOrElse { logger.warning(it) }
|
||||||
|
|
||||||
StatSvc.Register.online(bot.client).sendAndExpect()
|
return StatSvc.Register.online(bot.client).sendAndExpect()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateOtherClientsList() {
|
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(bot.isActive) { "bot is dead therefore network can't init." }
|
||||||
check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore 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) {
|
if (!pendingEnabled) {
|
||||||
pendingIncomingPackets = ConcurrentLinkedQueue()
|
pendingIncomingPackets = ConcurrentLinkedQueue()
|
||||||
_pendingEnabled.value = true
|
_pendingEnabled.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
contactUpdater.loadAll()
|
val registerResp = registerClientOnline()
|
||||||
|
|
||||||
this@QQAndroidBotNetworkHandler.launch(CoroutineName("Awaiting ConfigPushSvc.PushReq"), block= ConfigPushSyncer())
|
this@QQAndroidBotNetworkHandler.launch(CoroutineName("Awaiting ConfigPushSvc.PushReq"), block= ConfigPushSyncer())
|
||||||
|
|
||||||
syncMessageSvc()
|
launch {
|
||||||
|
syncMessageSvc()
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
bot.otherClientsLock.withLock {
|
||||||
|
updateOtherClientsList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contactUpdater.loadAll(registerResp.origin)
|
||||||
|
|
||||||
bot.firstLoginSucceed = true
|
bot.firstLoginSucceed = true
|
||||||
postInitActions()
|
postInitActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
private fun BotNetworkHandler.ConfigPushSyncer(): suspend CoroutineScope.() -> Unit = launch@{
|
private fun BotNetworkHandler.ConfigPushSyncer(): suspend CoroutineScope.() -> Unit = launch@{
|
||||||
logger.info { "Awaiting ConfigPushSvc.PushReq." }
|
logger.info { "Awaiting ConfigPushSvc.PushReq." }
|
||||||
when (val resp: ConfigPushSvc.PushReq.PushReqResponse? = nextEventOrNull(20_000)) {
|
when (val resp: ConfigPushSvc.PushReq.PushReqResponse? = nextEventOrNull(20_000)) {
|
||||||
|
@ -15,41 +15,41 @@ import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
internal class SvcReqRegister(
|
internal class SvcReqRegister(
|
||||||
@TarsId(0) @JvmField val lUin: Long = 0L,
|
@TarsId(0) @JvmField var lUin: Long = 0L,
|
||||||
@TarsId(1) @JvmField val lBid: Long = 0L,
|
@TarsId(1) @JvmField var lBid: Long = 0L,
|
||||||
@TarsId(2) @JvmField val cConnType: Byte = 0,
|
@TarsId(2) @JvmField var cConnType: Byte = 0,
|
||||||
@TarsId(3) @JvmField val sOther: String = "",
|
@TarsId(3) @JvmField var sOther: String = "",
|
||||||
@TarsId(4) @JvmField val iStatus: Int = 11,
|
@TarsId(4) @JvmField var iStatus: Int = 11,
|
||||||
@TarsId(5) @JvmField val bOnlinePush: Byte = 0,
|
@TarsId(5) @JvmField var bOnlinePush: Byte = 0,
|
||||||
@TarsId(6) @JvmField val bIsOnline: Byte = 0,
|
@TarsId(6) @JvmField var bIsOnline: Byte = 0,
|
||||||
@TarsId(7) @JvmField val bIsShowOnline: Byte = 0,
|
@TarsId(7) @JvmField var bIsShowOnline: Byte = 0,
|
||||||
@TarsId(8) @JvmField val bKikPC: Byte = 0,
|
@TarsId(8) @JvmField var bKikPC: Byte = 0,
|
||||||
@TarsId(9) @JvmField val bKikWeak: Byte = 0,
|
@TarsId(9) @JvmField var bKikWeak: Byte = 0,
|
||||||
@TarsId(10) @JvmField val timeStamp: Long = 0L,
|
@TarsId(10) @JvmField var timeStamp: Long = 0L,
|
||||||
@TarsId(11) @JvmField val iOSVersion: Long = 0L,
|
@TarsId(11) @JvmField var iOSVersion: Long = 0L,
|
||||||
@TarsId(12) @JvmField val cNetType: Byte = 0,
|
@TarsId(12) @JvmField var cNetType: Byte = 0,
|
||||||
@TarsId(13) @JvmField val sBuildVer: String? = "",
|
@TarsId(13) @JvmField var sBuildVer: String? = "",
|
||||||
@TarsId(14) @JvmField val bRegType: Byte = 0,
|
@TarsId(14) @JvmField var bRegType: Byte = 0,
|
||||||
@TarsId(15) @JvmField val vecDevParam: ByteArray? = null,
|
@TarsId(15) @JvmField var vecDevParam: ByteArray? = null,
|
||||||
@TarsId(16) @JvmField val vecGuid: ByteArray? = null,
|
@TarsId(16) @JvmField var vecGuid: ByteArray? = null,
|
||||||
@TarsId(17) @JvmField val iLocaleID: Int = 2052,
|
@TarsId(17) @JvmField var iLocaleID: Int = 2052,
|
||||||
@TarsId(18) @JvmField val bSlientPush: Byte = 0,
|
@TarsId(18) @JvmField var bSlientPush: Byte = 0,
|
||||||
@TarsId(19) @JvmField val strDevName: String? = null,
|
@TarsId(19) @JvmField var strDevName: String? = null,
|
||||||
@TarsId(20) @JvmField val strDevType: String? = null,
|
@TarsId(20) @JvmField var strDevType: String? = null,
|
||||||
@TarsId(21) @JvmField val strOSVer: String? = null,
|
@TarsId(21) @JvmField var strOSVer: String? = null,
|
||||||
@TarsId(22) @JvmField val bOpenPush: Byte,
|
@TarsId(22) @JvmField var bOpenPush: Byte,
|
||||||
@TarsId(23) @JvmField val iLargeSeq: Long,
|
@TarsId(23) @JvmField var iLargeSeq: Long,
|
||||||
@TarsId(24) @JvmField val iLastWatchStartTime: Long = 0L,
|
@TarsId(24) @JvmField var iLastWatchStartTime: Long = 0L,
|
||||||
@TarsId(26) @JvmField val uOldSSOIp: Long = 0L,
|
@TarsId(26) @JvmField var uOldSSOIp: Long = 0L,
|
||||||
@TarsId(27) @JvmField val uNewSSOIp: Long = 0L,
|
@TarsId(27) @JvmField var uNewSSOIp: Long = 0L,
|
||||||
@TarsId(28) @JvmField val sChannelNo: String? = null,
|
@TarsId(28) @JvmField var sChannelNo: String? = null,
|
||||||
@TarsId(29) @JvmField val lCpId: Long = 0L,
|
@TarsId(29) @JvmField var lCpId: Long = 0L,
|
||||||
@TarsId(30) @JvmField val strVendorName: String? = null,
|
@TarsId(30) @JvmField var strVendorName: String? = null,
|
||||||
@TarsId(31) @JvmField val strVendorOSName: String? = null,
|
@TarsId(31) @JvmField var strVendorOSName: String? = null,
|
||||||
@TarsId(32) @JvmField val strIOSIdfa: String? = null,
|
@TarsId(32) @JvmField var strIOSIdfa: String? = null,
|
||||||
@TarsId(33) @JvmField val bytes_0x769_reqbody: ByteArray? = null,
|
@TarsId(33) @JvmField var bytes_0x769_reqbody: ByteArray? = null,
|
||||||
@TarsId(34) @JvmField val bIsSetStatus: Byte = 0,
|
@TarsId(34) @JvmField var bIsSetStatus: Byte = 0,
|
||||||
@TarsId(35) @JvmField val vecServerBuf: ByteArray? = null,
|
@TarsId(35) @JvmField var vecServerBuf: ByteArray? = null,
|
||||||
@TarsId(36) @JvmField val bSetMute: Byte = 0
|
@TarsId(36) @JvmField var bSetMute: Byte = 0
|
||||||
// @SerialId(25) var vecBindUin: ArrayList<*>? = null // ?? 未知泛型
|
// @SerialId(25) var vecBindUin: ArrayList<*>? = null // ?? 未知泛型
|
||||||
) : JceStruct
|
) : JceStruct
|
@ -10,6 +10,7 @@
|
|||||||
package net.mamoe.mirai.internal.network.protocol.data.jce
|
package net.mamoe.mirai.internal.network.protocol.data.jce
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import net.mamoe.mirai.internal.network.FriendListCache
|
||||||
import net.mamoe.mirai.internal.utils.io.JceStruct
|
import net.mamoe.mirai.internal.utils.io.JceStruct
|
||||||
import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId
|
import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId
|
||||||
|
|
||||||
@ -30,7 +31,15 @@ internal class SvcRespRegister(
|
|||||||
@JvmField @TarsId(11) val iClientPort: Int = 0,
|
@JvmField @TarsId(11) val iClientPort: Int = 0,
|
||||||
@JvmField @TarsId(12) val iHelloInterval: Int = 300,
|
@JvmField @TarsId(12) val iHelloInterval: Int = 300,
|
||||||
@JvmField @TarsId(13) val iLargeSeq: Long = 0L,
|
@JvmField @TarsId(13) val iLargeSeq: Long = 0L,
|
||||||
|
/**
|
||||||
|
* =1 好友列表更新
|
||||||
|
*/
|
||||||
@JvmField @TarsId(14) val largeSeqUpdate: Byte = 0,
|
@JvmField @TarsId(14) val largeSeqUpdate: Byte = 0,
|
||||||
@JvmField @TarsId(15) val bytes_0x769_rspBody: ByteArray? = null,
|
@JvmField @TarsId(15) val bytes_0x769_rspBody: ByteArray? = null,
|
||||||
@JvmField @TarsId(16) val iStatus: Int? = 0
|
@JvmField @TarsId(16) val iStatus: Int? = 0
|
||||||
) : JceStruct
|
) : 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.contact.appId
|
||||||
import net.mamoe.mirai.internal.createOtherClient
|
import net.mamoe.mirai.internal.createOtherClient
|
||||||
import net.mamoe.mirai.internal.message.contextualBugReportException
|
import net.mamoe.mirai.internal.message.contextualBugReportException
|
||||||
|
import net.mamoe.mirai.internal.network.FriendListCache
|
||||||
import net.mamoe.mirai.internal.network.Packet
|
import net.mamoe.mirai.internal.network.Packet
|
||||||
import net.mamoe.mirai.internal.network.QQAndroidClient
|
import net.mamoe.mirai.internal.network.QQAndroidClient
|
||||||
import net.mamoe.mirai.internal.network.getRandomByteArray
|
import net.mamoe.mirai.internal.network.getRandomByteArray
|
||||||
@ -94,29 +95,30 @@ internal class StatSvc {
|
|||||||
|
|
||||||
internal object Register : OutgoingPacketFactory<Register.Response>("StatSvc.register") {
|
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 fun toString(): String = "Response(StatSvc.register)"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
|
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
|
||||||
val packet = readUniPacket(SvcRespRegister.serializer())
|
val packet = readUniPacket(SvcRespRegister.serializer())
|
||||||
if (packet.updateFlag.toInt() == 1) {
|
|
||||||
//TODO 加载好友列表
|
|
||||||
}
|
|
||||||
if (packet.largeSeqUpdate.toInt() == 1) {
|
|
||||||
//TODO 刷新好友列表
|
|
||||||
}
|
|
||||||
packet.iHelloInterval.let {
|
packet.iHelloInterval.let {
|
||||||
bot.configuration.heartbeatPeriodMillis = it.times(1000).toLong()
|
bot.configuration.heartbeatPeriodMillis = it.times(1000).toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response
|
return Response(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun online(
|
fun online(
|
||||||
client: QQAndroidClient,
|
client: QQAndroidClient,
|
||||||
regPushReason: RegPushReason = RegPushReason.appRegister
|
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(
|
fun offline(
|
||||||
client: QQAndroidClient,
|
client: QQAndroidClient,
|
||||||
@ -127,7 +129,8 @@ internal class StatSvc {
|
|||||||
client: QQAndroidClient,
|
client: QQAndroidClient,
|
||||||
bid: Long,
|
bid: Long,
|
||||||
status: OnlineStatus,
|
status: OnlineStatus,
|
||||||
regPushReason: RegPushReason = RegPushReason.appRegister
|
regPushReason: RegPushReason = RegPushReason.appRegister,
|
||||||
|
applyAction: SvcReqRegister.() -> Unit = {}
|
||||||
) = buildLoginOutgoingPacket(
|
) = buildLoginOutgoingPacket(
|
||||||
client,
|
client,
|
||||||
bodyType = 1,
|
bodyType = 1,
|
||||||
@ -198,7 +201,7 @@ internal class StatSvc {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
bSetMute = 0
|
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