LTD-ManaagerBot/src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt

184 lines
7.4 KiB
Kotlin

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