refactor: 优化调整了模块,抽象出些工具,以便更好的模块编程开发
This commit is contained in:
parent
2b5af6b52c
commit
95e21f8b84
183
src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt
Normal file
183
src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.module
|
||||
|
||||
class HelpModule {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.module
|
||||
|
||||
class ModGroupHandleModule {
|
||||
}
|
||||
|
|
@ -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 ?: ""
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package top.r3944realms.ltdmanager.module.common
|
||||
|
||||
data class CooldownResult(
|
||||
val canTrigger: Boolean,
|
||||
val remainingSeconds: Long = 0
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package top.r3944realms.ltdmanager.module.common
|
||||
|
||||
sealed class CooldownScope {
|
||||
object Global : CooldownScope()
|
||||
object PerUser : CooldownScope()
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package top.r3944realms.ltdmanager.module.common
|
||||
|
||||
interface CooldownStateProvider<S> {
|
||||
fun load(): S
|
||||
fun save(state: S)
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter
|
||||
|
||||
interface MessageFilter {
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter.type
|
||||
|
||||
class CommandFilter {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter.type
|
||||
|
||||
class CooldownFilter {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter.type
|
||||
|
||||
class IgnoreSelfFilter {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter.type
|
||||
|
||||
class KeywordFilter {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter.type
|
||||
|
||||
class NewMessageFilter {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.utils
|
||||
|
||||
object FileNameFilter {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.utils
|
||||
|
||||
enum class SystemType {
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
package top.r394realms.ltdmanagertest.misc
|
||||
|
||||
Loading…
Reference in New Issue
Block a user