Support group list cache, close #987

This commit is contained in:
Him188 2021-02-07 12:49:12 +08:00
parent 90d4030fe6
commit 7fac83702a
9 changed files with 243 additions and 72 deletions

View File

@ -9,7 +9,9 @@
@file:JvmMultifileClass
@file:JvmName("BotEventsKt")
@file:Suppress("unused", "FunctionName", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "DEPRECATION_ERROR")
@file:Suppress("unused", "FunctionName", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "DEPRECATION_ERROR",
"MemberVisibilityCanBePrivate"
)
package net.mamoe.mirai.event.events
@ -28,8 +30,8 @@ import java.util.concurrent.atomic.AtomicBoolean
/**
* 机器人被踢出群或在其他客户端主动退出一个群. 在事件广播前 [Bot.groups] 就已删除这个群.
*/
public sealed class BotLeaveEvent : BotEvent, Packet, AbstractEvent() {
public abstract val group: Group
public sealed class BotLeaveEvent : BotEvent, Packet, AbstractEvent(), GroupMemberInfoChangeEvent {
public abstract override val group: Group
/**
* 机器人主动退出一个群.
@ -64,7 +66,7 @@ public data class BotGroupPermissionChangeEvent @MiraiInternalApi constructor(
public override val group: Group,
public val origin: MemberPermission,
public val new: MemberPermission
) : BotPassiveEvent, GroupEvent, Packet, AbstractEvent()
) : BotPassiveEvent, GroupEvent, Packet, AbstractEvent(), GroupMemberInfoChangeEvent
/**
* Bot 被禁言
@ -75,7 +77,7 @@ public data class BotMuteEvent @MiraiInternalApi constructor(
* 操作人.
*/
public val operator: NormalMember
) : GroupEvent, Packet, BotPassiveEvent, AbstractEvent() {
) : GroupEvent, Packet, BotPassiveEvent, AbstractEvent(), GroupMemberInfoChangeEvent {
public override val group: Group
get() = operator.group
}
@ -88,7 +90,7 @@ public data class BotUnmuteEvent @MiraiInternalApi constructor(
* 操作人.
*/
public val operator: NormalMember
) : GroupEvent, Packet, BotPassiveEvent, AbstractEvent() {
) : GroupEvent, Packet, BotPassiveEvent, AbstractEvent(), GroupMemberInfoChangeEvent {
public override val group: Group
get() = operator.group
}
@ -96,7 +98,7 @@ public data class BotUnmuteEvent @MiraiInternalApi constructor(
/**
* Bot 成功加入了一个新群
*/
public sealed class BotJoinGroupEvent : GroupEvent, BotPassiveEvent, Packet, AbstractEvent() {
public sealed class BotJoinGroupEvent : GroupEvent, BotPassiveEvent, Packet, AbstractEvent(), GroupMemberInfoChangeEvent {
public abstract override val group: Group
/**
@ -164,7 +166,7 @@ public data class GroupNameChangeEvent @MiraiInternalApi constructor(
* 操作人. null 时则是机器人操作
*/
public override val operator: NormalMember?
) : GroupSettingChangeEvent<String>, Packet, GroupOperableEvent, AbstractEvent()
) : GroupSettingChangeEvent<String>, Packet, GroupOperableEvent, AbstractEvent(), GroupMemberInfoChangeEvent
/**
* 入群公告改变. 此事件广播前修改就已经完成.
@ -177,7 +179,7 @@ public data class GroupEntranceAnnouncementChangeEvent @MiraiInternalApi constru
* 操作人. null 时则是机器人操作
*/
public override val operator: NormalMember?
) : GroupSettingChangeEvent<String>, Packet, GroupOperableEvent, AbstractEvent()
) : GroupSettingChangeEvent<String>, Packet, GroupOperableEvent, AbstractEvent(), GroupMemberInfoChangeEvent
/**
@ -191,7 +193,7 @@ public data class GroupMuteAllEvent @MiraiInternalApi constructor(
* 操作人. null 时则是机器人操作
*/
public override val operator: NormalMember?
) : GroupSettingChangeEvent<Boolean>, Packet, GroupOperableEvent, AbstractEvent()
) : GroupSettingChangeEvent<Boolean>, Packet, GroupOperableEvent, AbstractEvent(), GroupMemberInfoChangeEvent
/**
@ -205,7 +207,7 @@ public data class GroupAllowAnonymousChatEvent @MiraiInternalApi constructor(
* 操作人. null 时则是机器人操作
*/
public override val operator: NormalMember?
) : GroupSettingChangeEvent<Boolean>, Packet, GroupOperableEvent, AbstractEvent()
) : GroupSettingChangeEvent<Boolean>, Packet, GroupOperableEvent, AbstractEvent(), GroupMemberInfoChangeEvent
/**
@ -216,7 +218,7 @@ public data class GroupAllowConfessTalkEvent @MiraiInternalApi constructor(
public override val new: Boolean,
public override val group: Group,
public val isByBot: Boolean // 无法获取操作人
) : GroupSettingChangeEvent<Boolean>, Packet, AbstractEvent()
) : GroupSettingChangeEvent<Boolean>, Packet, AbstractEvent(), GroupMemberInfoChangeEvent
/**
* "允许群员邀请好友加群" 功能状态改变. 此事件广播前修改就已经完成.
@ -229,7 +231,7 @@ public data class GroupAllowMemberInviteEvent @MiraiInternalApi constructor(
* 操作人. null 时则是机器人操作
*/
public override val operator: NormalMember?
) : GroupSettingChangeEvent<Boolean>, Packet, GroupOperableEvent, AbstractEvent()
) : GroupSettingChangeEvent<Boolean>, Packet, GroupOperableEvent, AbstractEvent(), GroupMemberInfoChangeEvent
// endregion
@ -245,7 +247,7 @@ public data class GroupAllowMemberInviteEvent @MiraiInternalApi constructor(
public sealed class MemberJoinEvent(
public override val member: NormalMember
) : GroupMemberEvent, BotPassiveEvent, Packet,
AbstractEvent() {
AbstractEvent(), GroupMemberInfoChangeEvent {
/**
* 被邀请加入群
*/
@ -282,7 +284,7 @@ public sealed class MemberJoinEvent(
/**
* 成员已经离开群的事件. 在事件广播前成员就已经从 [Group.members] 中删除
*/
public sealed class MemberLeaveEvent : GroupMemberEvent, AbstractEvent() {
public sealed class MemberLeaveEvent : GroupMemberEvent, AbstractEvent(), GroupMemberInfoChangeEvent {
/**
* 成员被踢出群. 成员不可能是机器人自己.
*/
@ -320,13 +322,13 @@ public data class BotInvitedJoinGroupRequestEvent @MiraiInternalApi constructor(
* 邀请入群的账号的 id
*/
public val invitorId: Long,
public val groupId: Long,
public override val groupId: Long,
public val groupName: String,
/**
* 邀请人昵称
*/
public val invitorNick: String
) : BotEvent, Packet, AbstractEvent() {
) : BotEvent, Packet, AbstractEvent(), BaseGroupMemberInfoChangeEvent {
/**
* 邀请人. 若在事件发生后邀请人已经被删除好友, [invitor] `null`.
*/
@ -360,7 +362,7 @@ public data class MemberJoinRequestEvent @MiraiInternalApi constructor(
* 申请入群的账号的 id
*/
val fromId: Long,
val groupId: Long,
override val groupId: Long,
val groupName: String,
/**
* 申请人昵称
@ -370,7 +372,7 @@ public data class MemberJoinRequestEvent @MiraiInternalApi constructor(
* 邀请人 id如果是邀请入群
*/
val invitorId: Long? = null
) : BotEvent, Packet, AbstractEvent() {
) : BotEvent, Packet, AbstractEvent(), BaseGroupMemberInfoChangeEvent {
/**
* 相关群. 若在事件发生后机器人退出这个群, [group] `null`.
*/
@ -471,7 +473,7 @@ public data class MemberCardChangeEvent @MiraiInternalApi constructor(
public val new: String,
public override val member: NormalMember
) : GroupMemberEvent, Packet, AbstractEvent()
) : GroupMemberEvent, Packet, AbstractEvent(), GroupMemberInfoChangeEvent
/**
* 成员群头衔改动. 一定为群主操作
@ -495,7 +497,7 @@ public data class MemberSpecialTitleChangeEvent @MiraiInternalApi constructor(
* null 时则是机器人操作.
*/
public override val operator: NormalMember?
) : GroupMemberEvent, GroupOperableEvent, AbstractEvent()
) : GroupMemberEvent, GroupOperableEvent, AbstractEvent(), GroupMemberInfoChangeEvent
// endregion
@ -509,7 +511,7 @@ public data class MemberPermissionChangeEvent @MiraiInternalApi constructor(
public override val member: NormalMember,
public val origin: MemberPermission,
public val new: MemberPermission
) : GroupMemberEvent, BotPassiveEvent, Packet, AbstractEvent()
) : GroupMemberEvent, BotPassiveEvent, Packet, AbstractEvent(), GroupMemberInfoChangeEvent
// endregion
@ -528,7 +530,7 @@ public data class MemberMuteEvent @MiraiInternalApi constructor(
* 操作人. null 则为机器人操作
*/
public override val operator: Member?
) : GroupMemberEvent, Packet, GroupOperableEvent, AbstractEvent()
) : GroupMemberEvent, Packet, GroupOperableEvent, AbstractEvent(), GroupMemberInfoChangeEvent
/**
* 群成员被取消禁言事件. 被禁言的成员都不可能是机器人本人
@ -541,7 +543,7 @@ public data class MemberUnmuteEvent @MiraiInternalApi constructor(
* 操作人. null 则为机器人操作
*/
public override val operator: Member?
) : GroupMemberEvent, Packet, GroupOperableEvent, AbstractEvent()
) : GroupMemberEvent, Packet, GroupOperableEvent, AbstractEvent(), GroupMemberInfoChangeEvent
// endregion

View File

@ -16,6 +16,7 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.utils.MiraiInternalApi
/**
* 有关一个 [Bot] 的事件
@ -110,6 +111,19 @@ public interface GroupMemberEvent : GroupEvent, UserEvent {
override val user: Member get() = member
}
/**
* 用于更新缓存, 请勿使用.
*/
@MiraiInternalApi
internal interface BaseGroupMemberInfoChangeEvent : BotEvent {
val groupId: Long
} // for cache
@MiraiInternalApi
internal interface GroupMemberInfoChangeEvent : BotEvent, GroupEvent, BaseGroupMemberInfoChangeEvent {
override val groupId: Long get() = group.id
} // for cache
public interface OtherClientEvent : BotEvent, Packet {
public val client: OtherClient
override val bot: Bot get() = client.bot

View File

@ -347,6 +347,39 @@ public open class BotConfiguration { // open for Java
}
/**
* `null` 时启用群成员列表缓存, 加快初始化速度. 在启用后将会在下载群成员列表后保存到文件, 并在修改时自动保存.
* @since 2.4
* @see enableGroupMemberListCache
*/
public var groupMemberListCache: GroupMemberListCache? = GroupMemberListCache()
/**
* 群成员列表缓存设置.
* @since 2.4
* @see groupMemberListCache
*/
public class GroupMemberListCache @JvmOverloads constructor(
/**
* 缓存文件位置, 相对于 [workingDir] 的路径.
*/
public val cacheDir: File = File("cache"),
/**
* 在有好友列表修改是
*/
public val saveIntervalMillis: Long = 60_000,
)
/**
* 启用群成员列表缓存.
* @since 2.4
* @see BotConfiguration.enableGroupMemberListCache
*/
public fun enableGroupMemberListCache() {
friendListCache = FriendListCache()
}
/**
* 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext].
*

View File

@ -20,6 +20,7 @@ import kotlinx.coroutines.withContext
import kotlinx.io.core.discardExact
import kotlinx.io.core.readBytes
import kotlinx.serialization.json.*
import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.*
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.*

View File

@ -78,19 +78,24 @@ 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()
configuration.friendListCache?.cacheFile?.let { cacheFile ->
val ret = configuration.workingDir.resolve(cacheFile).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
}
}
val groupMemberListCaches: GroupMemberListCaches? by lazy {
if (configuration.groupMemberListCache!= null) {
GroupMemberListCaches(this)
} else null
}
private val friendListSaver by lazy {
configuration.friendListCache?.let { friendListCache: BotConfiguration.FriendListCache ->
@ -99,11 +104,10 @@ internal class QQAndroidBot constructor(
}
}
}
fun saveFriendCache() {
val friendListCache = friendListCache
if (friendListCache != null) {
configuration.friendListCache?.cacheFile?.run {
configuration.friendListCache?.cacheFile?.let { configuration.workingDir.resolve(it) }?.run {
createFileIfNotExists()
writeText(JsonForCache.encodeToString(FriendListCache.serializer(), friendListCache))
bot.network.logger.info { "Saved ${friendListCache.list.size} friends to local cache." }

View File

@ -0,0 +1,126 @@
/*
* 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.QQAndroidBot
import net.mamoe.mirai.internal.contact.info.FriendInfoImpl
import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
import net.mamoe.mirai.internal.network.protocol.data.jce.StTroopNum
import net.mamoe.mirai.internal.utils.ScheduledJob
import net.mamoe.mirai.utils.createFileIfNotExists
import net.mamoe.mirai.utils.info
import net.mamoe.mirai.utils.runBIO
import java.io.File
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.time.milliseconds
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(),
)
@Serializable
internal data class GroupMemberListCache(
var troopMemberNumSeq: Long,
var list: List<MemberInfoImpl> = emptyList(),
)
internal fun GroupMemberListCache.isValid(stTroopNum: StTroopNum): Boolean {
return this.list.size == stTroopNum.dwMemberNum?.toInt() && this.troopMemberNumSeq == stTroopNum.dwMemberNumSeq
}
internal class GroupMemberListCaches(
private val bot: QQAndroidBot,
) {
init {
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
bot.eventChannel.parentScope(bot)
.subscribeAlways<net.mamoe.mirai.event.events.BaseGroupMemberInfoChangeEvent> {
groupListSaver.notice()
}
}
private val changedGroups: MutableCollection<Long> = ConcurrentLinkedQueue()
private val groupListSaver by lazy {
ScheduledJob(bot.coroutineContext, bot.configuration.groupMemberListCache!!.saveIntervalMillis.milliseconds) {
runBIO { saveGroupCaches() }
}
}
fun reportChanged(groupCode: Long) {
changedGroups.add(groupCode)
groupListSaver.notice()
}
private fun takeCurrentChangedGroups(): Map<Long, GroupMemberListCache> {
val ret = HashMap<Long, GroupMemberListCache>()
changedGroups.removeIf {
ret[it] = get(it)
true
}
return ret
}
private val cacheDir by lazy {
bot.configuration.groupMemberListCache!!.cacheDir.let { bot.configuration.workingDir.resolve(it) }
}
private fun resolveCacheFile(groupCode: Long): File {
cacheDir.mkdirs()
return cacheDir.resolve("$groupCode.json")
}
fun saveGroupCaches() {
val currentChanged = takeCurrentChangedGroups()
if (currentChanged.isNotEmpty()) {
for ((id, cache) in currentChanged) {
val file = resolveCacheFile(id)
file.createFileIfNotExists()
file.writeText(JsonForCache.encodeToString(GroupMemberListCache.serializer(), cache))
}
bot.network.logger.info { "Saved ${currentChanged.size} groups to local cache." }
}
}
val map: MutableMap<Long, GroupMemberListCache> = ConcurrentHashMap()
fun retainAll(list: Collection<Long>) {
this.map.keys.retainAll(list)
}
operator fun get(id: Long): GroupMemberListCache {
return map.getOrPut(id) {
val file = resolveCacheFile(id)
if (file.exists() && file.isFile) {
val text = file.readText()
if (text.isNotBlank()) {
return JsonForCache.decodeFromString(GroupMemberListCache.serializer(), text)
}
}
GroupMemberListCache(0, emptyList())
}
}
}

View File

@ -18,11 +18,13 @@ 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.data.MemberInfo
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.MemberInfoImpl
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
@ -141,19 +143,38 @@ internal class ContactUpdaterImpl(
private fun addFriendToBot(it: FriendInfo) =
bot.friends.delegate.add(FriendImpl(bot, bot.coroutineContext, it))
private suspend fun addGroupToBot(stTroopNum: StTroopNum) {
private suspend fun addGroupToBot(stTroopNum: StTroopNum) = stTroopNum.run {
suspend fun refreshGroupMemberList(): Sequence<MemberInfo> {
return Mirai.getRawGroupMemberList(
bot,
groupUin,
groupCode,
dwGroupOwnerUin
)
}
val cache = bot.groupMemberListCaches?.get(groupCode)
val members = if (cache != null) {
if (cache.isValid(stTroopNum)) {
cache.list.asSequence().also {
bot.network.logger.info { "Loaded ${cache.list.size} members from local cache for group ${groupName} (${groupCode})" }
}
} else refreshGroupMemberList().also { sequence ->
cache.troopMemberNumSeq = dwMemberNumSeq ?: 0
cache.list = sequence.mapTo(ArrayList()) { it as MemberInfoImpl }
bot.groupMemberListCaches!!.reportChanged(groupCode)
}
} else {
refreshGroupMemberList()
}
bot.groups.delegate.add(
GroupImpl(
bot = bot,
coroutineContext = bot.coroutineContext,
id = stTroopNum.groupCode,
id = groupCode,
groupInfo = GroupInfoImpl(stTroopNum),
members = Mirai.getRawGroupMemberList(
bot,
stTroopNum.groupUin,
stTroopNum.groupCode,
stTroopNum.dwGroupOwnerUin
)
members = members
)
)
}
@ -184,9 +205,7 @@ internal class ContactUpdaterImpl(
if (initGroupOk) {
return
}
logger.info { "Start syncing group config..." }
TroopManagement.GetTroopConfig(bot.client).sendAndExpect<TroopManagement.GetTroopConfig.Response>()
logger.info { "Successfully synced group config." }
logger.info { "Start loading group list..." }
val troopListData = FriendList.GetTroopListSimplify(bot.client)
@ -203,7 +222,9 @@ internal class ContactUpdaterImpl(
}
}
}
logger.info { "Successfully loaded group list: ${troopListData.groups.size} in total." }
bot.groupMemberListCaches?.saveGroupCaches()
initGroupOk = true
}

View File

@ -1,30 +0,0 @@
/*
* 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

@ -30,7 +30,7 @@ import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
internal class StrangerList {
object GetStrangerList : OutgoingPacketFactory<GetStrangerList.Response>("OidbSvc.0x5d2_0") {
class Response(val result: Int, val strangerList: List<Oidb0x5d2.FriendEntry>) : Packet {
class Response(val result: Int, val strangerList: List<Oidb0x5d2.FriendEntry>, val origin: Oidb0x5d2.RspGetList?) : Packet {
override fun toString(): String {
return "StrangerList.GetStrangerList.Response(result=$result)"
}
@ -61,10 +61,10 @@ internal class StrangerList {
if (pkg.result == 0) {
pkg.bodybuffer.loadAs(Oidb0x5d2.RspBody.serializer()).rspGetList!!.let {
bot.client.strangerSeq = it.seq
return Response(pkg.result, it.list)
return Response(pkg.result, it.list, it)
}
}
return Response(pkg.result, emptyList())
return Response(pkg.result, emptyList(), null)
}
}