refactor: 优化调整了模块,抽象出些工具,以便更好的模块编程开发

This commit is contained in:
叁玖领域 2025-09-13 03:20:53 +08:00
parent 2b5af6b52c
commit 95e21f8b84
18 changed files with 395 additions and 0 deletions

View File

@ -0,0 +1,183 @@
package top.r3944realms.ltdmanager.module
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import top.r3944realms.ltdmanager.module.common.CommandParser
import top.r3944realms.ltdmanager.module.common.filter.TriggerMessageFilter
import top.r3944realms.ltdmanager.module.common.filter.type.CommandFilter
import top.r3944realms.ltdmanager.module.common.filter.type.IgnoreSelfFilter
import top.r3944realms.ltdmanager.module.common.filter.type.NewMessageFilter
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageElement
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.request.group.SetGroupBanRequest
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
import top.r3944realms.ltdmanager.utils.LoggerUtil
import java.io.File
import kotlin.random.Random
/**
* 指令触发禁言模块
*/
class CommandBanModule(
moduleName: String,
private val groupMessagePollingModule : GroupMessagePollingModule,
private val selfId: Long,
commandPrefixList: List<String> = listOf("/mute"), // 默认命令前缀
private val minBanMinutes: Int = 1,
private val maxBanMinutes: Int = 15
) : BaseModule("CommandBanModule", moduleName), PersistentState<CommandBanModule.BanState> {
private val commandParser = CommandParser(commandPrefixList)
private val commandFilter = CommandFilter(commandParser)
private val banState = loadState()
override fun getState(): BanState = banState
private val triggerFilter by lazy {
TriggerMessageFilter(
listOf(
IgnoreSelfFilter(selfId),
NewMessageFilter { _ -> banState.lastTriggerTime to banState.lastTriggerRealId },
commandFilter
)
)
}
private var scope: CoroutineScope? = null
private val stateFile: File = getStateFileInternal("command_ban_state.json", name)
private val stateBackupFile: File = getStateFileInternal("command_ban_state.json.bak", name)
override fun getStateFileInternal(): File = stateFile
override fun onLoad() {
LoggerUtil.logger.info("[$name] 模块已装载,监听群组: ${groupMessagePollingModule.targetGroupId}")
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope!!.launch {
LoggerUtil.logger.info("[$name] 启动消息监听协程")
groupMessagePollingModule.messagesFlow.collect { messages ->
handleMessages(messages)
}
}
}
override suspend fun onUnload() {
LoggerUtil.logger.info("[$name] 模块卸载,取消协程")
scope?.cancel()
}
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
// 先过一遍过滤器,只有符合条件的才进入后续处理
val filtered = triggerFilter.filter(messages)
for (msg in filtered) {
processBanCommand(msg)
}
}
/**
* SpecificMsg 中的 message 段拼成一条可解析文本
* - text 段直接拼接
* - 如果消息段里包含 @ MessageData 中为 qq 字段则拼成 "@{qq}"方便 parseMentionToUserId 解析
*/
private fun GetFriendMsgHistoryEvent.SpecificMsg.plainText(): String {
return this.message.joinToString(" ") { seg ->
// 如果 message element 包含 qq 字段(即@用户),优先使用它
seg.data.qq?.let { "@${it}" } ?: (seg.data.text ?: "")
}.trim()
}
private suspend fun processBanCommand(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
try {
val parsed = commandParser.parseCommand(msg.plainText()) ?: return
val (command, argument) = parsed
// 参数格式: [分钟]
// 示例:/mute 5 → 自己禁言 5 分钟
// /mute → 自己随机禁言
val parts = argument.split(" ").filter { it.isNotBlank() }
val durationMinutes = parts.getOrNull(0)?.toIntOrNull()
?: Random.nextInt(minBanMinutes, maxBanMinutes + 1)
val durationSeconds = durationMinutes.coerceIn(minBanMinutes, maxBanMinutes) * 60
val targetUserId = msg.sender.userId
banUser(targetUserId, groupMessagePollingModule.targetGroupId, durationSeconds)
sendGroupMessage("✅ 你已被禁言 $durationMinutes 分钟", msg.realId)
// 更新状态(保证状态保存正确)
banState.lastTriggerRealId = msg.realId
banState.lastTriggerTime = msg.time
saveState(banState)
} catch (e: Exception) {
LoggerUtil.logger.error("[$name] 执行禁言指令失败", e)
sendGroupMessage("❌ 执行禁言失败,请检查指令格式或权限", msg.realId)
}
}
private suspend fun banUser(userId: Long, groupId: Long, seconds: Int) {
val request = SetGroupBanRequest(
duration = seconds.toDouble(),
groupId = ID.long(groupId),
userId = ID.long(userId)
)
napCatClient.sendUnit(request)
LoggerUtil.logger.info("[$name] 已对用户 $userId 执行 $seconds 秒禁言")
}
private suspend fun sendGroupMessage(text: String, replyTo: Long? = null) {
val request = SendGroupMsgRequest(
MessageElement.reply(ID.long(replyTo ?: 0), text),
ID.long(groupMessagePollingModule.targetGroupId)
)
napCatClient.sendUnit(request)
}
override fun info(): String {
return "[$name] 指令禁言模块:用户发送 ${commandParser.getCommands().joinToString("、")} 来禁言自己," +
"支持指定分钟数或随机分钟数,范围 $minBanMinutes-$maxBanMinutes 分钟。"
}
override fun help(): String {
return buildString {
appendLine("📖 [$name] 使用帮助:")
appendLine(" - ${commandParser.getCommands().joinToString("、")} [分钟]")
appendLine(" · 不写分钟数 → 随机禁言 (范围 $minBanMinutes-$maxBanMinutes 分钟)")
appendLine(" · 写分钟数 → 自己禁言指定分钟数")
appendLine()
appendLine("示例:")
appendLine(" - /mute → 随机禁言自己")
appendLine(" - /mute 5 → 禁言自己 5 分钟")
}
}
// ---------------- 持久化 ----------------
@Serializable
data class BanState(
var lastTriggerRealId: Long = -1,
var lastTriggerTime: Long = 0
)
override fun saveState(state: BanState) {
try {
if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true)
stateFile.writeText(Json.encodeToString(state))
} catch (e: Exception) {
LoggerUtil.logger.error("[$name] 保存状态失败", e)
}
}
override fun loadState(): BanState {
return try {
val fileToRead = when {
stateFile.exists() -> stateFile
stateBackupFile.exists() -> stateBackupFile
else -> null
} ?: return BanState()
Json.decodeFromString<BanState>(fileToRead.readText())
} catch (e: Exception) {
LoggerUtil.logger.warn("[$name] 读取状态失败", e)
BanState()
}
}
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.module
class HelpModule {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.module
class ModGroupHandleModule {
}

View File

@ -0,0 +1,52 @@
package top.r3944realms.ltdmanager.module.util
/**
* 命令解析器
* 严格模式只支持命令后带空格的情况避免误读
*/
class CommandParser(private val commands: List<String>) {
/**
* 解析命令
* @param text 输入的文本
* @return Pair<命令, 参数> null如果不是有效命令
*/
fun parseCommand(text: String): Pair<String, String>? {
val trimmedText = text.trim()
// 查找匹配的命令(必须后面跟着空格或字符串结束)
val matchedCommand = commands.firstOrNull { command ->
trimmedText.startsWith("$command ") || trimmedText == command
} ?: return null
// 获取参数部分
val argument = if (trimmedText.length > matchedCommand.length) {
trimmedText.substring(matchedCommand.length).trim()
} else {
""
}
return Pair(matchedCommand, argument)
}
/**
* 检查文本是否包含有效命令
*/
fun containsCommand(text: String): Boolean {
return parseCommand(text.trim()) != null
}
/**
* 获取命令部分不包含参数
*/
fun getCommandOnly(text: String): String? {
return parseCommand(text.trim())?.first
}
/**
* 获取参数部分不包含命令
*/
fun getArgumentOnly(text: String): String {
return parseCommand(text.trim())?.second ?: ""
}
}

View File

@ -0,0 +1,30 @@
package top.r3944realms.ltdmanager.module.common
class CooldownManager<S>(
private val cooldownMillis: Long,
private val scope: CooldownScope,
private val stateProvider: CooldownStateProvider<S>,
private val getLastTrigger: (S, Long?) -> Pair<Long, Long>, // (time, realId)
private val updateTrigger: (S, Long?, Long, Long) -> S, // (qq, realId, time)
private val updateCooldownRealId: (S, Long?, Long) -> S // (qq, realId)
) {
private var state: S = stateProvider.load()
fun check(userId: Long?, realId: Long, msgTime: Long): CooldownResult {
val (lastTime, lastCooldownRealId) = getLastTrigger(state, if (scope == CooldownScope.PerUser) userId else null)
val nowSec = System.currentTimeMillis() / 1000
val cooldownSec = cooldownMillis / 1000
return if (lastTime == -1L || nowSec - lastTime >= cooldownSec) {
state = updateTrigger(state, userId, realId, msgTime)
stateProvider.save(state)
CooldownResult(true)
} else {
if (realId != lastCooldownRealId) {
state = updateCooldownRealId(state, userId, realId)
stateProvider.save(state)
}
CooldownResult(false, cooldownSec - (nowSec - lastTime))
}
}
}

View File

@ -0,0 +1,6 @@
package top.r3944realms.ltdmanager.module.common
data class CooldownResult(
val canTrigger: Boolean,
val remainingSeconds: Long = 0
)

View File

@ -0,0 +1,6 @@
package top.r3944realms.ltdmanager.module.common
sealed class CooldownScope {
object Global : CooldownScope()
object PerUser : CooldownScope()
}

View File

@ -0,0 +1,6 @@
package top.r3944realms.ltdmanager.module.common
interface CooldownStateProvider<S> {
fun load(): S
fun save(state: S)
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.module.common.filter
interface MessageFilter {
}

View File

@ -0,0 +1,70 @@
package top.r3944realms.ltdmanager.module.common
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
class TriggerMessageFilter(private val filters: List<MessageFilter>) {
suspend fun filter(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>)
: List<GetFriendMsgHistoryEvent.SpecificMsg> {
val result = mutableListOf<GetFriendMsgHistoryEvent.SpecificMsg>()
for (msg in messages) {
if (filters.all { it.test(msg) }) {
result.add(msg)
}
}
return result
}
}
interface MessageFilter {
suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean
}
/** 忽略机器人自己的消息 */
class IgnoreSelfFilter(private val selfId: Long) : MessageFilter {
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
return msg.userId != selfId
}
}
/** 只保留比上次触发更新的消息 */
class NewMessageFilter(
private val getLastTrigger: (Long) -> Pair<Long, Long> // (time, realId)
) : MessageFilter {
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
val (lastTime, lastRealId) = getLastTrigger(msg.userId)
return msg.time > lastTime || (msg.time == lastTime && msg.realId > lastRealId)
}
}
/** 文本关键词匹配 */
class KeywordFilter(private val keywords: Set<String>) : MessageFilter {
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
return msg.message.any { seg ->
seg.type == MessageType.Text && seg.data.text?.let { it in keywords } == true
}
}
}
/** 命令解析器匹配 */
class CommandFilter(private val parser: CommandParser) : MessageFilter {
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
return msg.message.any { seg ->
seg.type == MessageType.Text && seg.data.text?.let { parser.containsCommand(it) } == true
}
}
}
/** 冷却检查 */
class CooldownFilter(
private val cooldownManager: CooldownManager<*>,
private val sendCooldown: suspend (GetFriendMsgHistoryEvent.SpecificMsg, Long) -> Unit
) : MessageFilter {
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
val result = cooldownManager.check(msg.userId, msg.realId, msg.time)
if (!result.canTrigger) {
sendCooldown(msg, result.remainingSeconds)
}
return result.canTrigger
}
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.module.common.filter.type
class CommandFilter {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.module.common.filter.type
class CooldownFilter {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.module.common.filter.type
class IgnoreSelfFilter {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.module.common.filter.type
class KeywordFilter {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.module.common.filter.type
class NewMessageFilter {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.utils
object FileNameFilter {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.utils
enum class SystemType {
}

View File

@ -0,0 +1,2 @@
package top.r394realms.ltdmanagertest.misc