fix: 修正BanModule 中初始化顺序导致的NPE问题
、
This commit is contained in:
parent
95e21f8b84
commit
9f83026e56
|
|
@ -3,4 +3,4 @@ org.gradle.downloadSources=false
|
|||
org.gradle.parallel=true
|
||||
org.gradle.degree_of_parallelism=16
|
||||
project_group=top.r3944realms.ltdmanager
|
||||
project_version=1.3-SNAPSHOT
|
||||
project_version=1.6-SNAPSHOT
|
||||
|
|
|
|||
|
|
@ -11,16 +11,25 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
val selfNickName = "闲趣老土豆"
|
||||
// 创建模块实例
|
||||
val groupModule = GroupRequestHandlerModule(
|
||||
moduleName = "WhiteListGroup",
|
||||
client = GlobalManager.napCatClient,
|
||||
targetGroupId = groupId
|
||||
)
|
||||
val groupMsgPollingModule = GroupMessagePollingModule(
|
||||
moduleName = "WhiteListGroup",
|
||||
targetGroupId = groupId,
|
||||
pollIntervalMillis = 5_000L,
|
||||
msgHistoryCheck = 15
|
||||
)
|
||||
val helpModule = HelpModule(
|
||||
moduleName = "WhiteListGroup",
|
||||
groupMessagePollingModule = groupMsgPollingModule,
|
||||
selfId = selfQQId,
|
||||
selfNickName = selfNickName,
|
||||
)
|
||||
val toolConfig = YamlConfigLoader.loadToolConfig()
|
||||
val rconModule = RconPlayerListModule(
|
||||
moduleName = "WhiteListGroup",
|
||||
groupMessagePollingModule = groupMsgPollingModule,
|
||||
rconTimeOut = 2_000L,
|
||||
cooldownMillis = 10_000L,
|
||||
|
|
@ -38,13 +47,15 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
)
|
||||
val mailConfig = YamlConfigLoader.loadMailConfig()
|
||||
val mailModule = MailModule(
|
||||
host = mailConfig.host.toString(),
|
||||
authToken = mailConfig.decryptedPassword.toString(),
|
||||
port = mailConfig.port!!,
|
||||
senderEmailAddress = mailConfig.mailAddress!!,
|
||||
moduleName = "WhiteListGroup",
|
||||
host = mailConfig.host.toString(),
|
||||
authToken = mailConfig.decryptedPassword.toString(),
|
||||
port = mailConfig.port!!,
|
||||
senderEmailAddress = mailConfig.mailAddress!!,
|
||||
)
|
||||
val blessingSkinConfig = YamlConfigLoader.loadBlessingSkinServerConfig()
|
||||
val invitationCodesModule = InvitationCodesModule(
|
||||
moduleName = "WhiteListGroup",
|
||||
groupMessagePollingModule = groupMsgPollingModule,
|
||||
mailModule = mailModule,
|
||||
apiToken = blessingSkinConfig.invitationApi?.decryptedToken!!,
|
||||
|
|
@ -57,9 +68,10 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
)
|
||||
)
|
||||
val mcServerStatusModule = McServerStatusModule(
|
||||
moduleName = "WhiteListGroup",
|
||||
groupMessagePollingModule = groupMsgPollingModule,
|
||||
selfId = selfQQId,
|
||||
cooldownSeconds = 20,
|
||||
cooldownMillis = 20_000L,
|
||||
selfNickName = selfNickName,
|
||||
commands = listOf("/m", "/mcs", "seek", "s"),
|
||||
presetServer = mapOf(
|
||||
|
|
@ -67,6 +79,19 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
setOf("土豆", "老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
||||
)
|
||||
)
|
||||
val banModule = BanModule(
|
||||
moduleName = "WhiteListGroup",
|
||||
groupMessagePollingModule = groupMsgPollingModule,
|
||||
selfId = selfQQId,
|
||||
commandPrefixList = listOf("口球", "mute", "杂鱼三九"),
|
||||
minBanMinutes = 1,
|
||||
maxBanMinutes = 15,
|
||||
)
|
||||
val modGroupHandlerModule = ModGroupHandlerModule(
|
||||
moduleName = "ModGrouup",
|
||||
targetGroupId = 339340846,
|
||||
answers = listOf("戏鸢", "一只戏鸢", "折戏鸢", "LostInLinearPast", "lostinlinearpast"),
|
||||
)
|
||||
|
||||
// 注册模块到全局模块管理器
|
||||
GlobalManager.moduleManager.registerModule(groupModule)
|
||||
|
|
@ -75,6 +100,9 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
GlobalManager.moduleManager.registerModule(rconModule)
|
||||
GlobalManager.moduleManager.registerModule(mailModule)
|
||||
GlobalManager.moduleManager.registerModule(invitationCodesModule)
|
||||
GlobalManager.moduleManager.registerModule(helpModule)
|
||||
GlobalManager.moduleManager.registerModule(banModule)
|
||||
GlobalManager.moduleManager.registerModule(modGroupHandlerModule)
|
||||
|
||||
// 加载模块
|
||||
GlobalManager.moduleManager.loadModule(groupModule.name)
|
||||
|
|
@ -83,4 +111,7 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
GlobalManager.moduleManager.loadModule(rconModule.name)
|
||||
GlobalManager.moduleManager.loadModule(mailModule.name)
|
||||
GlobalManager.moduleManager.loadModule(invitationCodesModule.name)
|
||||
GlobalManager.moduleManager.loadModule(helpModule.name)
|
||||
GlobalManager.moduleManager.loadModule(banModule.name)
|
||||
GlobalManager.moduleManager.loadModule(modGroupHandlerModule.name)
|
||||
}
|
||||
|
|
@ -21,33 +21,36 @@ import kotlin.random.Random
|
|||
/**
|
||||
* 指令触发禁言模块
|
||||
*/
|
||||
class CommandBanModule(
|
||||
class BanModule(
|
||||
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> {
|
||||
) : BaseModule("BanModule", moduleName), PersistentState<BanModule.BanState> {
|
||||
|
||||
private val commandParser = CommandParser(commandPrefixList)
|
||||
private val commandFilter = CommandFilter(commandParser)
|
||||
private val banState = loadState()
|
||||
private val stateFile: File = getStateFileInternal("command_ban_state.json", name)
|
||||
private val stateBackupFile: File = getStateFileInternal("command_ban_state.json.bak", name)
|
||||
private var banState = loadState()
|
||||
override fun getState(): BanState = banState
|
||||
|
||||
private val triggerFilter by lazy {
|
||||
TriggerMessageFilter(
|
||||
listOf(
|
||||
IgnoreSelfFilter(selfId),
|
||||
NewMessageFilter { _ -> banState.lastTriggerTime to banState.lastTriggerRealId },
|
||||
NewMessageFilter { userId ->
|
||||
banState.getLastTriggerTime(userId) to banState.getLastTriggerRealId(userId)
|
||||
},
|
||||
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
|
||||
|
||||
|
|
@ -105,8 +108,8 @@ class CommandBanModule(
|
|||
sendGroupMessage("✅ 你已被禁言 $durationMinutes 分钟", msg.realId)
|
||||
|
||||
// 更新状态(保证状态保存正确)
|
||||
banState.lastTriggerRealId = msg.realId
|
||||
banState.lastTriggerTime = msg.time
|
||||
// 禁言成功后更新状态
|
||||
banState = banState.updateLastTrigger(targetUserId, msg.realId, msg.time)
|
||||
saveState(banState)
|
||||
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -152,11 +155,28 @@ class CommandBanModule(
|
|||
|
||||
// ---------------- 持久化 ----------------
|
||||
@Serializable
|
||||
data class BanState(
|
||||
var lastTriggerRealId: Long = -1,
|
||||
var lastTriggerTime: Long = 0
|
||||
data class UserBanDetail(
|
||||
val realId: Long,
|
||||
val time: Long,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BanState(
|
||||
val map: Map<Long, UserBanDetail> = emptyMap()
|
||||
) {
|
||||
fun getLastTriggerTime(userId: Long): Long = map[userId]?.time ?: -1
|
||||
fun getLastTriggerRealId(userId: Long): Long = map[userId]?.realId ?: -1
|
||||
|
||||
fun updateLastTrigger(userId: Long, realId: Long, time: Long = -1): BanState {
|
||||
val old = map[userId]
|
||||
val newTime = if (time != -1L) time else old?.time ?: -1
|
||||
val newMap = map.toMutableMap().apply {
|
||||
put(userId, UserBanDetail(realId, newTime))
|
||||
}
|
||||
return copy(map = newMap)
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveState(state: BanState) {
|
||||
try {
|
||||
if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true)
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ import kotlin.coroutines.cancellation.CancellationException
|
|||
* 模块抽象基类
|
||||
* 所有功能模块都继承该类
|
||||
*/
|
||||
abstract class BaseModule {
|
||||
abstract class BaseModule(baseName : String = "BaseModule", idName : String = "") {
|
||||
|
||||
/**
|
||||
* 模块名称
|
||||
*/
|
||||
abstract val name: String
|
||||
val name: String = "$baseName-#$idName";
|
||||
|
||||
/**
|
||||
* 停止信号
|
||||
|
|
@ -73,6 +73,13 @@ abstract class BaseModule {
|
|||
} catch (_: CancellationException) {}
|
||||
LoggerUtil.syncInfo("[$name] 模块已安全停止")
|
||||
}
|
||||
/**
|
||||
* 模块说明 / 帮助信息
|
||||
* 默认返回空字符串,子类可重写提供具体帮助文本
|
||||
*/
|
||||
open fun help(): String = ""
|
||||
/** 模块基础信息,用于 HelpModule 显示 */
|
||||
open fun info(): String = "模块 $name 未提供详细信息"
|
||||
/**
|
||||
* 提供访问全局 NapCatClient 的快捷方式
|
||||
*/
|
||||
|
|
@ -85,9 +92,14 @@ abstract class BaseModule {
|
|||
* 提供访问全局 mcSrvStatusClient 的快捷方式
|
||||
*/
|
||||
protected val mcSrvStatusClient get() = GlobalManager.mcSrvStatusClient
|
||||
/**
|
||||
* 提供访问全局 加载模块 的快捷方式
|
||||
*/
|
||||
protected val moduleMap get() = GlobalManager.moduleManager.getModules()
|
||||
/**
|
||||
* 获取数据库连接
|
||||
* 使用 try-with-resources 时会自动关闭
|
||||
*/
|
||||
protected fun getConnection() = GlobalManager.getConnection()
|
||||
|
||||
}
|
||||
|
|
@ -11,12 +11,11 @@ import top.r3944realms.ltdmanager.napcat.request.message.GetGroupMsgHistoryReque
|
|||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||
|
||||
class GroupMessagePollingModule(
|
||||
moduleName: String,
|
||||
val targetGroupId: Long,
|
||||
private val pollIntervalMillis: Long = 5_000L,
|
||||
private val msgHistoryCheck: Int = 15
|
||||
) : BaseModule() {
|
||||
|
||||
override val name: String = "MessagePollingModule"
|
||||
private val msgHistoryCheck: Int = 15,
|
||||
) : BaseModule("MessagePollingModule", moduleName) {
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
// 用 Flow 存消息,其他模块可以订阅
|
||||
|
|
|
|||
|
|
@ -11,12 +11,11 @@ import top.r3944realms.ltdmanager.napcat.request.group.SetGroupAddRequestRequest
|
|||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||
|
||||
class GroupRequestHandlerModule(
|
||||
moduleName: String,
|
||||
private val client: NapCatClient,
|
||||
private val targetGroupId: Long,
|
||||
private val pollIntervalMillis: Long = 30_000L,
|
||||
) : BaseModule() {
|
||||
|
||||
override val name: String = "GroupRequestHandlerModule"
|
||||
) : BaseModule("GroupRequestHandlerModule", moduleName) {
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
|
|
@ -176,4 +175,7 @@ class GroupRequestHandlerModule(
|
|||
return data.invitedRequest + data.joinRequests
|
||||
}
|
||||
}
|
||||
override fun info(): String = "模块: $name\n功能: 自动处理群组加群请求\n版本: 1.0"
|
||||
|
||||
override fun help(): String = "本模块会轮询群组加群请求并根据数据库白名单自动同意或拒绝"
|
||||
}
|
||||
|
|
@ -1,4 +1,245 @@
|
|||
package top.r3944realms.ltdmanager.module
|
||||
|
||||
class HelpModule {
|
||||
}
|
||||
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.cooldown.CooldownManager
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownStateProvider
|
||||
import top.r3944realms.ltdmanager.module.common.filter.TriggerMessageFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.CooldownFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.IgnoreSelfFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.KeywordFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.NewMessageFilter
|
||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||||
import top.r3944realms.ltdmanager.napcat.data.MessageType
|
||||
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
|
||||
import top.r3944realms.ltdmanager.napcat.request.message.SendForwardMsgRequest
|
||||
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* HelpModule 提供全局模块帮助信息
|
||||
*/
|
||||
class HelpModule(
|
||||
moduleName: String,
|
||||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||
private val selfId: Long,
|
||||
private val selfNickName: String,
|
||||
private val keywords: List<String> = listOf("help", "帮助"),
|
||||
private val cooldownMillis: Long = 30_000L
|
||||
) : BaseModule("HelpModule", moduleName), PersistentState<HelpModule.HelpState> {
|
||||
|
||||
// 命令解析器
|
||||
private val commandParser = CommandParser(keywords)
|
||||
private val GetFriendMsgHistoryEvent.SpecificMsg.textContent: String
|
||||
get() = message.joinToString("") { it.data.text ?: "" }
|
||||
|
||||
// 持久化文件
|
||||
private val stateFile: File = getStateFileInternal("help_module_state.json", name)
|
||||
private val stateBackupFile: File = getStateFileInternal("help_module_state.json.bak", name)
|
||||
|
||||
@Serializable
|
||||
data class HelpState(var lastTriggeredRealId: Long = -1L, var lastTriggerTime: Long = 0L)
|
||||
|
||||
private var lastTriggerState: HelpState = loadState()
|
||||
|
||||
// 冷却管理器
|
||||
private val cooldownManager by lazy {
|
||||
CooldownManager(
|
||||
cooldownMillis = cooldownMillis,
|
||||
scope = CooldownScope.Global,
|
||||
stateProvider = object : CooldownStateProvider<HelpState> {
|
||||
override fun load() = loadState()
|
||||
override fun save(state: HelpState) = saveState(state)
|
||||
},
|
||||
getLastTrigger = { state, _ -> state.lastTriggerTime to state.lastTriggeredRealId },
|
||||
updateTrigger = { state, _, realId, time -> state.copy(lastTriggeredRealId = realId, lastTriggerTime = time) },
|
||||
updateCooldownRealId = { state, _, realId -> state.copy(lastTriggeredRealId = realId) },
|
||||
groupId = groupMessagePollingModule.targetGroupId
|
||||
)
|
||||
}
|
||||
|
||||
// 触发过滤器
|
||||
private val triggerFilter by lazy {
|
||||
TriggerMessageFilter(
|
||||
listOf(
|
||||
IgnoreSelfFilter(selfId),
|
||||
NewMessageFilter { _ -> lastTriggerState.lastTriggerTime to lastTriggerState.lastTriggeredRealId },
|
||||
KeywordFilter(keywords.toSet()),
|
||||
CooldownFilter(cooldownManager) { msg, remain -> sendCooldownMessage(napCatClient, msg.realId, remain) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
override fun getStateFileInternal(): File = stateFile
|
||||
|
||||
override fun getState(): HelpState = lastTriggerState
|
||||
|
||||
override fun onLoad() {
|
||||
LoggerUtil.logger.info("[$name] 模块已加载,监听 help 指令")
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
scope!!.launch {
|
||||
groupMessagePollingModule.messagesFlow.collect { messages ->
|
||||
if (loaded) handleMessages(messages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onUnload() {
|
||||
LoggerUtil.logger.info("[$name] 模块卸载,取消协程...")
|
||||
scope?.cancel()
|
||||
saveState(lastTriggerState)
|
||||
LoggerUtil.logger.info("[$name] 模块已卸载完成")
|
||||
}
|
||||
|
||||
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
|
||||
val filtered = triggerFilter.filter(messages)
|
||||
val triggerMsg = filtered.maxByOrNull { it.time } ?: return
|
||||
|
||||
val cmdPair = commandParser.parseCommand(triggerMsg.textContent)
|
||||
if (cmdPair != null) {
|
||||
val (_, arg) = cmdPair
|
||||
if (arg.isNotEmpty()) {
|
||||
val module = moduleMap[arg]
|
||||
if (module != null) sendModuleHelp(triggerMsg, arg, module)
|
||||
else sendText(triggerMsg, "未找到模块: $arg")
|
||||
} else {
|
||||
sendAllModulesHelp(triggerMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendAllModulesHelp(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
|
||||
val messages = moduleMap.map { (name, module) ->
|
||||
val textBuilder = StringBuilder()
|
||||
textBuilder.appendLine("===== $name =====")
|
||||
textBuilder.appendLine(module.info())
|
||||
val helpText = module.help()
|
||||
if (helpText.isNotEmpty()) textBuilder.appendLine(helpText)
|
||||
textBuilder.appendLine().appendLine()
|
||||
SendForwardMsgRequest.Message(
|
||||
data = SendForwardMsgRequest.PurpleData(textBuilder.toString()),
|
||||
type = MessageType.Text
|
||||
)
|
||||
}
|
||||
|
||||
val topMessage = SendForwardMsgRequest.TopForwardMsg(
|
||||
data = SendForwardMsgRequest.MessageData(
|
||||
content = messages,
|
||||
nickname = selfNickName,
|
||||
userId = ID.long(selfId)
|
||||
),
|
||||
type = MessageType.Node
|
||||
)
|
||||
|
||||
val request = SendForwardMsgRequest(
|
||||
groupId = ID.long(groupMessagePollingModule.targetGroupId),
|
||||
messages = listOf(topMessage),
|
||||
news = listOf(SendForwardMsgRequest.ForwardModelNews("点击查看所有模块信息")),
|
||||
prompt = "全局模块信息",
|
||||
source = "📚 HelpModule",
|
||||
summary = "信息,共 ${messages.size} 个模块"
|
||||
)
|
||||
|
||||
napCatClient.sendUnit(request)
|
||||
updateTriggerState(msg)
|
||||
}
|
||||
|
||||
private suspend fun sendModuleHelp(msg: GetFriendMsgHistoryEvent.SpecificMsg, moduleName: String, module: BaseModule) {
|
||||
val textBuilder = StringBuilder()
|
||||
textBuilder.appendLine("===== $moduleName =====")
|
||||
textBuilder.appendLine(module.info())
|
||||
val helpText = module.help()
|
||||
if (helpText.isNotEmpty()) textBuilder.appendLine(helpText)
|
||||
|
||||
val message = SendForwardMsgRequest.Message(
|
||||
data = SendForwardMsgRequest.PurpleData(textBuilder.toString()),
|
||||
type = MessageType.Text
|
||||
)
|
||||
|
||||
val topMessage = SendForwardMsgRequest.TopForwardMsg(
|
||||
data = SendForwardMsgRequest.MessageData(
|
||||
content = listOf(message),
|
||||
nickname = selfNickName,
|
||||
userId = ID.long(selfId)
|
||||
),
|
||||
type = MessageType.Node
|
||||
)
|
||||
|
||||
val request = SendForwardMsgRequest(
|
||||
groupId = ID.long(groupMessagePollingModule.targetGroupId),
|
||||
messages = listOf(topMessage),
|
||||
news = listOf(SendForwardMsgRequest.ForwardModelNews("点击查看模块 $moduleName 帮助")),
|
||||
prompt = "模块 $moduleName 帮助",
|
||||
source = "📚 HelpModule",
|
||||
summary = "模块 $moduleName 帮助信息"
|
||||
)
|
||||
|
||||
napCatClient.sendUnit(request)
|
||||
updateTriggerState(msg)
|
||||
}
|
||||
|
||||
private suspend fun sendText(msg: GetFriendMsgHistoryEvent.SpecificMsg, text: String) {
|
||||
val request = SendGroupMsgRequest(
|
||||
MessageElement.reply(ID.long(msg.realId), text),
|
||||
ID.long(groupMessagePollingModule.targetGroupId)
|
||||
)
|
||||
napCatClient.sendUnit(request)
|
||||
updateTriggerState(msg)
|
||||
}
|
||||
|
||||
private fun updateTriggerState(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
|
||||
lastTriggerState.lastTriggeredRealId = msg.realId
|
||||
lastTriggerState.lastTriggerTime = msg.time
|
||||
saveState(lastTriggerState)
|
||||
}
|
||||
|
||||
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, remaining: Long) {
|
||||
val msg = "⏳ Help 查询过于频繁,请稍后再试(剩余 $remaining 秒)"
|
||||
LoggerUtil.logger.info("[$name] 发送冷却提示: $msg")
|
||||
client.sendUnit(
|
||||
SendGroupMsgRequest(
|
||||
MessageElement.reply(ID.long(realId), msg),
|
||||
ID.long(groupMessagePollingModule.targetGroupId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------- 持久化 ----------------
|
||||
override fun saveState(state: HelpState) {
|
||||
try {
|
||||
if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true)
|
||||
stateFile.writeText(Json.encodeToString(state))
|
||||
LoggerUtil.logger.info("[$name] 已保存状态: lastTriggeredRealId=${state.lastTriggeredRealId}, lastTriggerTime=${state.lastTriggerTime}")
|
||||
} catch (e: Exception) {
|
||||
LoggerUtil.logger.error("[$name] 保存状态失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadState(): HelpState {
|
||||
return try {
|
||||
val fileToRead = when {
|
||||
stateFile.exists() -> stateFile
|
||||
stateBackupFile.exists() -> stateBackupFile
|
||||
else -> null
|
||||
}
|
||||
if (fileToRead == null) return HelpState()
|
||||
Json.decodeFromString<HelpState>(fileToRead.readText())
|
||||
} catch (e: Exception) {
|
||||
LoggerUtil.logger.warn("[$name] 读取状态失败,使用默认值", e)
|
||||
HelpState()
|
||||
}
|
||||
}
|
||||
|
||||
override fun help(): String = "发送 'help' 获取所有模块帮助信息"
|
||||
|
||||
override fun info(): String = "模块: $name\n功能: 提供全局模块帮助信息\n版本: 1.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,18 @@ import top.r3944realms.ltdmanager.blessingskin.request.invitecode.GenerateInvita
|
|||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||
import top.r3944realms.ltdmanager.core.mail.mail
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownStateProvider
|
||||
import top.r3944realms.ltdmanager.module.common.filter.TriggerMessageFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.CooldownFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.IgnoreSelfFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.KeywordFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.NewMessageFilter
|
||||
import top.r3944realms.ltdmanager.module.exception.InvitationCodeException
|
||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||||
import top.r3944realms.ltdmanager.napcat.data.MessageType
|
||||
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
|
||||
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
||||
import top.r3944realms.ltdmanager.utils.HtmlTemplateUtil
|
||||
|
|
@ -63,24 +70,67 @@ api格式 https://skins.r3944realms.top/api/invitation-codes/generate?token=XXXX
|
|||
*/
|
||||
|
||||
class InvitationCodesModule(
|
||||
moduleName: String,
|
||||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||
private val mailModule: MailModule,
|
||||
private val apiToken: String,
|
||||
private val selfId: Long,
|
||||
selfId: Long,
|
||||
private val cooldownMillis: Long = 120_000,
|
||||
private val keywords: Set<String> = setOf("申请邀请码")
|
||||
) : BaseModule(), PersistentState<InvitationCodesModule.LastTriggerMapState> {
|
||||
) : BaseModule("InvitationCodesModule", moduleName), PersistentState<InvitationCodesModule.LastTriggerMapState> {
|
||||
|
||||
override val name: String = "InvitationCodesModule"
|
||||
private var scope: CoroutineScope? = null
|
||||
private val stateFile: File = getStateFileInternal("invitation_codes_quarry_state.json", name)
|
||||
private val stateBackupFile: File = getStateFileInternal("invitation_codes_quarry_state.json.bak", name)
|
||||
private val cooldownManager by lazy{ CooldownManager(
|
||||
cooldownMillis = cooldownMillis,
|
||||
scope = CooldownScope.PerUser,
|
||||
stateProvider = object : CooldownStateProvider<LastTriggerMapState> {
|
||||
override fun load() = loadState()
|
||||
override fun save(state: LastTriggerMapState) = saveState(state)
|
||||
},
|
||||
getLastTrigger = { state, qq ->
|
||||
val detail = state.map[qq]
|
||||
(detail?.time ?: -1L) to (detail?.lastCooldownRealId ?: -1L)
|
||||
},
|
||||
updateTrigger = { state, qq, realId, time ->
|
||||
val id = requireNotNull(qq)
|
||||
state.updateLastTrigger(id, realId, time)
|
||||
},
|
||||
updateCooldownRealId = { state, qq, realId ->
|
||||
val id = requireNotNull(qq)
|
||||
state.updateLastCooldownRealId(id, realId)
|
||||
},
|
||||
groupId = groupMessagePollingModule.targetGroupId
|
||||
)
|
||||
}
|
||||
// 在 InvitationCodesModule 类里添加:
|
||||
private val triggerFilter = TriggerMessageFilter(
|
||||
listOf(
|
||||
IgnoreSelfFilter(selfId),
|
||||
NewMessageFilter { qq ->
|
||||
lastTriggerMapState.getLastTriggerTime(qq) to lastTriggerMapState.getLastTriggerRealId(qq)
|
||||
},
|
||||
KeywordFilter(keywords),
|
||||
CooldownFilter(
|
||||
cooldownManager = cooldownManager,
|
||||
sendCooldown = { msg, remain ->
|
||||
sendCooldownMessage(
|
||||
napCatClient,
|
||||
msg.userId,
|
||||
msg.realId,
|
||||
"⏳ 申请邀请码过于频繁(剩余 $remain 秒后自动申请)"
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
// 持久化文件(带锁 + 备份)
|
||||
private val stateFile = getStateFile("mc_server_status_state.json")
|
||||
private val stateBackupFile = getStateFile("invitation_codes_quarry_state.json.bak")
|
||||
private val fileLock = ReentrantLock()
|
||||
|
||||
private var lastTriggerMapState = loadState()
|
||||
override fun getStateFile(): File = stateFile
|
||||
override fun getStateFileInternal(): File = stateFile
|
||||
override fun getState(): LastTriggerMapState = lastTriggerMapState
|
||||
override fun onLoad() {
|
||||
LoggerUtil.logger.info("[$name] 模块已装载,目标群组: ${groupMessagePollingModule.targetGroupId}")
|
||||
|
|
@ -135,29 +185,17 @@ class InvitationCodesModule(
|
|||
}
|
||||
|
||||
/** 过滤出符合条件的触发消息 */
|
||||
private fun filterTriggerMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>)
|
||||
: List<GetFriendMsgHistoryEvent.SpecificMsg> {
|
||||
private suspend fun filterTriggerMessages(
|
||||
messages: List<GetFriendMsgHistoryEvent.SpecificMsg>
|
||||
): List<GetFriendMsgHistoryEvent.SpecificMsg> {
|
||||
|
||||
val filtered = messages.asSequence()
|
||||
.filter { msg ->
|
||||
msg.userId != selfId &&
|
||||
(msg.time > lastTriggerMapState.getLastTriggerTime(msg.userId) ||
|
||||
(msg.time == lastTriggerMapState.getLastTriggerTime(msg.userId)
|
||||
&& msg.realId > lastTriggerMapState.getLastTriggerRealId(msg.userId))) &&
|
||||
msg.message.any { seg ->
|
||||
seg.type == MessageType.Text &&
|
||||
seg.data.text?.let { text -> keywords.any { keyword -> text == keyword } } == true
|
||||
}
|
||||
}
|
||||
// 先应用通用过滤器
|
||||
val filtered = triggerFilter.filter(messages)
|
||||
|
||||
// 再做 groupBy -> 只保留每个用户最新一条
|
||||
return filtered
|
||||
.groupBy { it.userId }
|
||||
.mapNotNull { (_, msgs) -> msgs.maxByOrNull { it.time } }
|
||||
.filter { runBlocking { filterCoolDownMessage(it) } }
|
||||
.toList()
|
||||
|
||||
if (filtered.isNotEmpty()) {
|
||||
LoggerUtil.logger.info("[$name] 待处理消息队列: $filtered")
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
private suspend fun getIdAndSelectSituation(msgs: List<GetFriendMsgHistoryEvent.SpecificMsg>,
|
||||
|
|
@ -358,33 +396,6 @@ class InvitationCodesModule(
|
|||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 冷却逻辑
|
||||
// =========================
|
||||
private suspend fun filterCoolDownMessage(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
|
||||
val triggerDetail = lastTriggerMapState.map[msg.userId]
|
||||
val lastTriggerTime = triggerDetail?.time ?: -1L
|
||||
val lastCooldownRealId = triggerDetail?.lastCooldownRealId ?: -1L
|
||||
val nowSec = System.currentTimeMillis() / 1000 // 转成秒
|
||||
|
||||
if (lastTriggerTime == -1L || nowSec - lastTriggerTime >= cooldownMillis / 1000) {
|
||||
// 正常触发
|
||||
return true
|
||||
}
|
||||
|
||||
// 冷却中,如果本消息未发送过冷却提示
|
||||
if (msg.realId != lastCooldownRealId) {
|
||||
val remaining = ((cooldownMillis / 1000) - (nowSec - lastTriggerTime)).coerceAtLeast(1)
|
||||
val msgText = "⏳ 申请邀请码过于频繁(剩余 $remaining 秒后将为你自动申请)"
|
||||
sendCooldownMessage(napCatClient, msg.userId, msg.realId, msgText)
|
||||
|
||||
// 记录这条消息已发送过冷却提示
|
||||
lastTriggerMapState = lastTriggerMapState.updateLastCooldownRealId(msg.userId, msg.realId)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun sendCooldownMessage(client: NapCatClient, qq: Long, realId: Long, msg: String) {
|
||||
val request = SendGroupMsgRequest(
|
||||
MessageElement.reply(ID.long(realId), msg),
|
||||
|
|
@ -646,5 +657,34 @@ class InvitationCodesModule(
|
|||
}
|
||||
}
|
||||
}
|
||||
// 在 InvitationCodesModule 类中补全:
|
||||
override fun info(): String {
|
||||
return """
|
||||
模块: $name
|
||||
功能: 自动处理群组内“申请邀请码”消息
|
||||
描述:
|
||||
1. 监听群消息,过滤关键词和冷却
|
||||
2. 根据QQ号查询白名单状态
|
||||
3. 自动创建或发送邀请码,并通过邮件发送
|
||||
4. 已触发和未触发状态会持久化保存
|
||||
关键词: $keywords
|
||||
冷却时间: ${cooldownMillis / 1000} 秒
|
||||
目标群组: ${groupMessagePollingModule.targetGroupId}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
override fun help(): String {
|
||||
return """
|
||||
使用说明:
|
||||
1. 在群里发送${keywords}触发本模块
|
||||
2. 模块会自动判断你的白名单状态
|
||||
- 若已使用过邀请码,会提醒你不要重复申请
|
||||
- 若已有邀请码但未使用,会重新发送邮件提醒
|
||||
- 若未生成邀请码,会调用API生成并发送邮件
|
||||
3. 请求过于频繁时,会有冷却提示
|
||||
4. 所有操作都有日志记录,可供管理员审计
|
||||
5. 异常情况会发送失败提示消息
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import java.util.concurrent.LinkedBlockingQueue
|
|||
import kotlin.concurrent.thread
|
||||
|
||||
class MailModule(
|
||||
moduleName: String,
|
||||
private val protocol: String = "SMTP",
|
||||
private val host: String,
|
||||
private val port: Int,
|
||||
|
|
@ -18,9 +19,7 @@ class MailModule(
|
|||
private val enableAuth: Boolean = true,
|
||||
private val enableTLS: Boolean = true,
|
||||
private val intervalMillis: Long = 2000L // 每封邮件之间的间隔(默认 2s)
|
||||
) : BaseModule() {
|
||||
|
||||
override val name: String = "MailModule"
|
||||
) : BaseModule("MailModule", moduleName) {
|
||||
|
||||
private lateinit var session: Session
|
||||
private val queue = LinkedBlockingQueue<Mail>() // 邮件队列
|
||||
|
|
@ -142,4 +141,36 @@ class MailModule(
|
|||
|
||||
Transport.send(message)
|
||||
}
|
||||
override fun info(): String {
|
||||
return buildString {
|
||||
appendLine("[$name] 邮件发送模块")
|
||||
appendLine("功能: 异步发送邮件,支持收件人/抄送/密送,支持 HTML 或纯文本邮件。")
|
||||
appendLine("SMTP 配置:")
|
||||
appendLine(" - 协议: $protocol")
|
||||
appendLine(" - 主机: $host")
|
||||
appendLine(" - 端口: $port")
|
||||
appendLine(" - 发件人邮箱: $senderEmailAddress")
|
||||
appendLine(" - 身份认证: ${if (enableAuth) "启用" else "禁用"}")
|
||||
appendLine(" - TLS/SSL: ${if (enableTLS) "启用" else "禁用"}")
|
||||
appendLine("队列行为:")
|
||||
appendLine(" - 邮件发送间隔: ${intervalMillis}ms")
|
||||
appendLine(" - 队列长度: ${queue.size}")
|
||||
appendLine(" - 当前发送线程状态: ${if (workerThread?.isAlive == true) "运行中" else "未运行"}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun help(): String {
|
||||
return buildString {
|
||||
appendLine("📖 [$name] 使用帮助:")
|
||||
appendLine("1. 创建 Mail 对象,设置收件人、主题和正文")
|
||||
appendLine(" 例如: Mail(to = listOf(\"example@mail.com\"), subject = \"测试\", body = \"Hello\")")
|
||||
appendLine("2. 调用 enqueue(mail) 加入发送队列")
|
||||
appendLine(" 邮件将异步发送,间隔 $intervalMillis ms")
|
||||
appendLine("3. 模块卸载时会自动停止发送线程")
|
||||
appendLine()
|
||||
appendLine("注意:")
|
||||
appendLine(" - 确保 SMTP 配置正确,否则发送失败")
|
||||
appendLine(" - 发件人邮箱需要允许 SMTP/授权码登录")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@ import kotlinx.coroutines.*
|
|||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import top.r3944realms.ltdmanager.mcserver.McServerStatus
|
||||
import top.r3944realms.ltdmanager.module.common.CommandParser
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownStateProvider
|
||||
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.CooldownFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.IgnoreSelfFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.NewMessageFilter
|
||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||||
|
|
@ -17,30 +26,72 @@ import java.util.concurrent.locks.ReentrantLock
|
|||
import kotlin.concurrent.withLock
|
||||
|
||||
class McServerStatusModule(
|
||||
moduleName: String,
|
||||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||
private val selfId: Long,
|
||||
private val selfNickName: String,
|
||||
private val cooldownSeconds: Long = 60,
|
||||
private val cooldownMillis: Long = 60_000L,
|
||||
private val commands: List<String> = listOf("/mcs", "/s"),
|
||||
private val presetServer: Map<Set<String>, String> = mapOf(
|
||||
setOf("hp", "hypixel") to "mc.hypixel.net",
|
||||
setOf("pm", "mineplex") to "play.mineplex.com"
|
||||
)
|
||||
) : BaseModule(), PersistentState<McServerStatusModule.CooldownState> {
|
||||
) : BaseModule("McServerStatusModule", moduleName), PersistentState<McServerStatusModule.CooldownState> {
|
||||
private val stateFile:File = getStateFileInternal("mc_server_status_state.json", name)
|
||||
private val stateBackupFile:File = getStateFileInternal("mc_server_status_state.json.bak", name)
|
||||
private val commandParser: CommandParser = CommandParser(commands)
|
||||
|
||||
private val cooldownManager by lazy {
|
||||
CooldownManager(
|
||||
cooldownMillis = cooldownMillis,
|
||||
scope = CooldownScope.PerUser,
|
||||
stateProvider = object : CooldownStateProvider<CooldownState> {
|
||||
override fun load() = loadState()
|
||||
override fun save(state: CooldownState) = saveState(state)
|
||||
},
|
||||
getLastTrigger = { state, qq ->
|
||||
val detail = state.map[qq]
|
||||
(detail?.time ?: -1L) to (detail?.lastCooldownRealId ?: -1L)
|
||||
},
|
||||
updateTrigger = { state, qq, realId, time ->
|
||||
val id = requireNotNull(qq) { "userId required for per-user cooldown" }
|
||||
state.updateLastTrigger(id, realId, time) }
|
||||
,
|
||||
updateCooldownRealId = { state, qq, realId ->
|
||||
val id = requireNotNull(qq) { "userId required for per-user cooldown" }
|
||||
state.updateLastCooldownRealId(id, realId)
|
||||
},
|
||||
groupId = groupMessagePollingModule.targetGroupId
|
||||
)
|
||||
}
|
||||
private val triggerFilter = TriggerMessageFilter(
|
||||
listOf(
|
||||
IgnoreSelfFilter(selfId),
|
||||
NewMessageFilter { qq ->
|
||||
cooldownState.getLastTriggerTime(qq) to cooldownState.getLastTriggerRealId(qq)
|
||||
},
|
||||
CommandFilter(commandParser),
|
||||
CooldownFilter(
|
||||
cooldownManager = cooldownManager,
|
||||
sendCooldown = { msg, remaining ->
|
||||
sendCooldownMessage(napCatClient, msg.realId, "⏳ 查询过于频繁, $remaining 秒后执行查询,切勿重复发送")
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
private val presetServerByAlias: Map<String, String> by lazy {
|
||||
presetServer.flatMap { (aliases, ip) ->
|
||||
aliases.map { it.lowercase() to ip }
|
||||
}.toMap()
|
||||
}
|
||||
fun getServerIp(alias: String): String? = presetServerByAlias[alias.lowercase()]
|
||||
override val name: String = "McServerStatusModule"
|
||||
private var scope: CoroutineScope? = null
|
||||
private val stateFile = getStateFile("mc_server_status_state.json")
|
||||
private val stateBackupFile = getStateFile("mc_server_status_state.json.bak")
|
||||
|
||||
private val fileLock = ReentrantLock()
|
||||
private var cooldownState = loadState()
|
||||
|
||||
override fun getStateFile(): File = stateFile
|
||||
override fun getStateFileInternal(): File = stateFile
|
||||
override fun getState(): CooldownState = cooldownState
|
||||
|
||||
override fun onLoad() {
|
||||
|
|
@ -76,32 +127,11 @@ class McServerStatusModule(
|
|||
saveState(cooldownState)
|
||||
}
|
||||
}
|
||||
private suspend fun filterTriggerMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>)
|
||||
: List<GetFriendMsgHistoryEvent.SpecificMsg> {
|
||||
|
||||
val filtered = messages.asSequence()
|
||||
.filter { msg ->
|
||||
// 忽略自己消息
|
||||
msg.userId != selfId &&
|
||||
// 新消息判断
|
||||
(msg.time > cooldownState.getLastTriggerTime(msg.userId) ||
|
||||
(msg.time == cooldownState.getLastTriggerTime(msg.userId) &&
|
||||
msg.realId > cooldownState.getLastTriggerRealId(msg.userId)))
|
||||
}
|
||||
.filter { msg ->
|
||||
// 检查命令
|
||||
msg.message.any { seg ->
|
||||
seg.type == MessageType.Text &&
|
||||
(
|
||||
seg.data.text?.let { text -> commands.any { cmd -> text.startsWith(cmd) } } == true
|
||||
)
|
||||
}
|
||||
}
|
||||
.filter { runBlocking { handleCooldown(it) } } // 这里处理冷却
|
||||
.toList()
|
||||
private suspend fun filterTriggerMessages(
|
||||
messages: List<GetFriendMsgHistoryEvent.SpecificMsg>
|
||||
): List<GetFriendMsgHistoryEvent.SpecificMsg> = triggerFilter.filter(messages)
|
||||
|
||||
return filtered
|
||||
}
|
||||
private suspend fun sendFailedMessage(
|
||||
client: NapCatClient,
|
||||
qq: Long? = null,
|
||||
|
|
@ -129,31 +159,7 @@ class McServerStatusModule(
|
|||
LoggerUtil.logger.info("[$name] 已发送 失败消息[无指定对象]")
|
||||
}
|
||||
}
|
||||
/** 冷却提示消息 */
|
||||
|
||||
private suspend fun handleCooldown(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
|
||||
val trigger = cooldownState.map[msg.userId]
|
||||
val lastTriggerTime = trigger?.time ?: -1L
|
||||
val lastCooldownRealId = trigger?.lastCooldownRealId ?: -1L
|
||||
val nowSec = System.currentTimeMillis() / 1000
|
||||
|
||||
// 未触发过或者已超过冷却
|
||||
if (lastTriggerTime == -1L || nowSec - lastTriggerTime >= cooldownSeconds) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 冷却中且未发送过冷却提示
|
||||
if (msg.realId != lastCooldownRealId) {
|
||||
val remaining = ((cooldownSeconds - (nowSec - lastTriggerTime))).coerceAtLeast(1)
|
||||
val msgText = "⏳ 查询过于频繁, $remaining 秒后执行查询,切勿重复发送"
|
||||
sendCooldownMessage(napCatClient, msg.userId, msg.realId, msgText)
|
||||
cooldownState = cooldownState.updateLastCooldownRealId(msg.userId, msg.realId)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun sendCooldownMessage(client: NapCatClient, qq: Long, realId: Long, text: String) {
|
||||
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, text: String) {
|
||||
val request = SendGroupMsgRequest(
|
||||
MessageElement.reply(ID.long(realId), text),
|
||||
ID.long(groupMessagePollingModule.targetGroupId)
|
||||
|
|
@ -171,16 +177,18 @@ class McServerStatusModule(
|
|||
?.trim()
|
||||
?: return
|
||||
|
||||
// 解析命令
|
||||
val matchedCommand = commands.firstOrNull { text.startsWith(it) } ?: return
|
||||
var address = text.removePrefix(matchedCommand).trim()
|
||||
// 使用命令解析器解析命令
|
||||
val parsedCommand = commandParser.parseCommand(text) ?: return
|
||||
val (_, address) = parsedCommand
|
||||
|
||||
// 使用预设别名替换
|
||||
presetServerByAlias[address.lowercase()]?.let { presetIp ->
|
||||
address = presetIp
|
||||
val finalAddress = if (address.isNotEmpty()) {
|
||||
presetServerByAlias[address.lowercase()] ?: address
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
if (address.isEmpty()) {
|
||||
if (finalAddress.isEmpty()) {
|
||||
sendFailedMessage(
|
||||
napCatClient,
|
||||
msg.userId,
|
||||
|
|
@ -192,9 +200,8 @@ class McServerStatusModule(
|
|||
}
|
||||
|
||||
try {
|
||||
val status = mcSrvStatusClient.getServerStatus(address) // 返回 McServerStatus
|
||||
val status = mcSrvStatusClient.getServerStatus(finalAddress)
|
||||
|
||||
// 检查是否查询失败
|
||||
if (!status.online) {
|
||||
sendFailedMessage(
|
||||
napCatClient, msg.userId, msg.realId, msg.time,
|
||||
|
|
@ -203,9 +210,7 @@ class McServerStatusModule(
|
|||
return
|
||||
}
|
||||
|
||||
// 查询成功,发送状态消息
|
||||
sendStatusForwardMessage(napCatClient, msg, address, status, msg.realId, msg.time)
|
||||
|
||||
sendStatusForwardMessage(napCatClient, msg, finalAddress, status, msg.realId, msg.time)
|
||||
} catch (e: Exception) {
|
||||
LoggerUtil.logger.error("查询服务器状态失败: $address", e)
|
||||
sendFailedMessage(
|
||||
|
|
@ -311,23 +316,36 @@ class McServerStatusModule(
|
|||
data class CooldownState(
|
||||
val map: Map<Long, TriggerDetail> = emptyMap()
|
||||
) {
|
||||
// 获取上次处理时间
|
||||
fun getLastTriggerTime(qq: Long): Long = map[qq]?.time ?: -1
|
||||
|
||||
// 获取上次处理消息ID
|
||||
fun getLastTriggerRealId(qq: Long): Long = map[qq]?.realId ?: -1
|
||||
fun updateLastTrigger(qq: Long, realId: Long, time: Long = -1): CooldownState {
|
||||
|
||||
// 获取上次冷却消息ID
|
||||
fun getLastCooldownRealId(qq: Long): Long = map[qq]?.lastCooldownRealId ?: -1
|
||||
|
||||
// 冷却结束,允许处理消息 → 更新 time 和 realId
|
||||
fun updateLastTrigger(qq: Long, realId: Long, time: Long): CooldownState {
|
||||
val old = map[qq]
|
||||
val newTime = if (time != -1L) time else old?.time ?: -1
|
||||
val newMap = map.toMutableMap().apply {
|
||||
put(qq, TriggerDetail(realId, newTime, old?.lastCooldownRealId ?: -1))
|
||||
put(qq, TriggerDetail(
|
||||
realId = realId, // 当前允许处理消息ID
|
||||
time = time, // 当前允许处理消息时间
|
||||
lastCooldownRealId = old?.lastCooldownRealId ?: -1 // 保留冷却中记录的消息ID
|
||||
))
|
||||
}
|
||||
return copy(map = newMap)
|
||||
}
|
||||
|
||||
// 冷却中消息 → 只更新 lastCooldownRealId,保留 time 和 realId
|
||||
fun updateLastCooldownRealId(qq: Long, realId: Long): CooldownState {
|
||||
val old = map[qq]
|
||||
val newMap = map.toMutableMap().apply {
|
||||
put(qq, TriggerDetail(
|
||||
realId = old?.realId ?: -1,
|
||||
time = old?.time ?: -1,
|
||||
lastCooldownRealId = realId
|
||||
realId = old?.realId ?: -1, // 保持上次允许处理的消息ID
|
||||
time = old?.time ?: -1, // 保持上次允许处理的时间
|
||||
lastCooldownRealId = realId // 更新当前冷却拒绝的消息ID
|
||||
))
|
||||
}
|
||||
return copy(map = newMap)
|
||||
|
|
@ -336,9 +354,9 @@ class McServerStatusModule(
|
|||
|
||||
@Serializable
|
||||
data class TriggerDetail(
|
||||
val realId: Long,
|
||||
val time: Long,
|
||||
val lastCooldownRealId: Long = -1L
|
||||
val realId: Long, // 上次允许处理消息ID
|
||||
val time: Long, // 上次允许处理消息时间(秒)
|
||||
val lastCooldownRealId: Long = -1 // 上次被冷却拒绝的消息ID
|
||||
)
|
||||
|
||||
override fun loadState(): CooldownState {
|
||||
|
|
@ -368,4 +386,33 @@ class McServerStatusModule(
|
|||
}
|
||||
}
|
||||
}
|
||||
override fun info(): String {
|
||||
return buildString {
|
||||
appendLine("模块名称: $name")
|
||||
appendLine("模块类型: McServerStatusModule")
|
||||
appendLine("目标群组: ${groupMessagePollingModule.targetGroupId}")
|
||||
appendLine("机器人昵称: $selfNickName (ID: $selfId)")
|
||||
appendLine("冷却时间: ${cooldownMillis / 1000} 秒")
|
||||
appendLine("支持命令: ${commands.joinToString(", ")}")
|
||||
appendLine("预设服务器别名:")
|
||||
presetServer.forEach { (aliases, ip) ->
|
||||
appendLine(" ${aliases.joinToString("/")} -> $ip")
|
||||
}
|
||||
appendLine("状态文件路径: ${stateFile.absolutePath}")
|
||||
appendLine("状态备份文件路径: ${stateBackupFile.absolutePath}")
|
||||
}
|
||||
}
|
||||
// 返回模块使用帮助
|
||||
override fun help(): String = buildString {
|
||||
appendLine("使用帮助 - McServerStatusModule")
|
||||
appendLine("指令格式: /mcs <服务器别名或IP> 或 /s <服务器别名或IP>")
|
||||
appendLine("示例:")
|
||||
presetServerByAlias.forEach { (alias, ip) ->
|
||||
appendLine(" /mcs $alias -> 查询服务器 $ip 状态")
|
||||
}
|
||||
appendLine("注意事项:")
|
||||
appendLine(" - 查询冷却时间为 ${cooldownMillis / 1000} 秒")
|
||||
appendLine(" - 输入服务器 IP 或别名均可")
|
||||
appendLine(" - 查询结果会以转发消息形式发送到群组")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,209 @@
|
|||
package top.r3944realms.ltdmanager.module
|
||||
|
||||
class ModGroupHandleModule {
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||||
import top.r3944realms.ltdmanager.napcat.event.NapCatEvent
|
||||
import top.r3944realms.ltdmanager.napcat.event.account.GetStrangerInfoEvent
|
||||
import top.r3944realms.ltdmanager.napcat.event.group.GetGroupIgnoredNotifiesEvent
|
||||
import top.r3944realms.ltdmanager.napcat.event.group.GetGroupSystemMsgEvent
|
||||
import top.r3944realms.ltdmanager.napcat.request.account.GetStrangerInfoRequest
|
||||
import top.r3944realms.ltdmanager.napcat.request.group.GetGroupIgnoredNotifiesRequest
|
||||
import top.r3944realms.ltdmanager.napcat.request.group.GetGroupSystemMsgRequest
|
||||
import top.r3944realms.ltdmanager.napcat.request.group.SetGroupAddRequestRequest
|
||||
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||
import java.io.File
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* 模块: 入群申请自动处理
|
||||
* 功能:
|
||||
* 1. 监听目标群的入群申请事件
|
||||
* 2. 根据 answers 列表自动同意或拒绝
|
||||
*/
|
||||
class ModGroupHandlerModule(
|
||||
moduleName: String,
|
||||
private val targetGroupId: Long,
|
||||
private val answers: List<String> = listOf("正确答案"),
|
||||
private val pollIntervalMillis: Long = 30_000L
|
||||
) : BaseModule("ModGroupHandlerModule", moduleName), PersistentState<ModGroupHandlerModule.RejectRecords> {
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
private val stateFile: File = getStateFileInternal("reject_records.json", name)
|
||||
private val fileLock = ReentrantLock()
|
||||
private var stateCache: RejectRecords? = null
|
||||
private val json = Json { prettyPrint = true; encodeDefaults = true }
|
||||
|
||||
@Serializable
|
||||
data class RejectRecord(
|
||||
val userId: Long,
|
||||
var reason: MutableList<String> = mutableListOf(),
|
||||
var rejectCount: Int = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* 记录所有被拒绝用户的Map,key = userId
|
||||
*/
|
||||
@Serializable
|
||||
data class RejectRecords(
|
||||
val records: MutableMap<Long, RejectRecord> = mutableMapOf()
|
||||
)
|
||||
|
||||
override fun getStateFileInternal(): File = stateFile
|
||||
|
||||
override fun getState(): RejectRecords {
|
||||
if (stateCache == null) stateCache = loadState()
|
||||
return stateCache!!
|
||||
}
|
||||
|
||||
override fun saveState(state: RejectRecords) {
|
||||
fileLock.withLock {
|
||||
try {
|
||||
stateFile.writeText(json.encodeToString(state))
|
||||
} catch (e: Exception) {
|
||||
LoggerUtil.logger.error("[$name] 保存拒绝记录失败", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadState(): RejectRecords {
|
||||
return try {
|
||||
if (!stateFile.exists()) return RejectRecords()
|
||||
val text = stateFile.readText()
|
||||
json.decodeFromString(RejectRecords.serializer(), text)
|
||||
} catch (e: Exception) {
|
||||
LoggerUtil.logger.warn("[$name] 拒绝记录加载失败,使用默认值", e)
|
||||
RejectRecords()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addReject(userId: Long, reason: String) {
|
||||
val state = getState()
|
||||
val record = state.records[userId]
|
||||
if (record != null) {
|
||||
record.rejectCount += 1
|
||||
record.reason.add(reason)
|
||||
} else {
|
||||
state.records[userId] = RejectRecord(userId, mutableListOf(reason), 1)
|
||||
}
|
||||
saveState(state)
|
||||
}
|
||||
fun getRejectRecord(userId: Long): RejectRecord? {
|
||||
return getState().records[userId]
|
||||
}
|
||||
|
||||
override fun onLoad() {
|
||||
LoggerUtil.logger.info("[$name] 模块已装载,目标群组: $targetGroupId")
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
scope!!.launch {
|
||||
LoggerUtil.logger.info("[$name] 轮询协程启动")
|
||||
while (isActive && loaded) {
|
||||
try {
|
||||
handleEvents()
|
||||
delay(pollIntervalMillis)
|
||||
} catch (e: Exception) {
|
||||
LoggerUtil.logger.error("[$name] 轮询异常", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onUnload() {
|
||||
LoggerUtil.logger.info("[$name] 模块卸载")
|
||||
scope?.cancel()
|
||||
}
|
||||
|
||||
private suspend fun handleEvents() {
|
||||
val systemEvent: GetGroupSystemMsgEvent = napCatClient.send(GetGroupSystemMsgRequest())
|
||||
handleEvent(systemEvent)
|
||||
|
||||
val ignoredEvent: GetGroupIgnoredNotifiesEvent = napCatClient.send(GetGroupIgnoredNotifiesRequest())
|
||||
handleEvent(ignoredEvent)
|
||||
}
|
||||
|
||||
private suspend fun handleEvent(event: Any) {
|
||||
if (!loaded) return
|
||||
val provider: GroupRequestProvider? = when (event) {
|
||||
is GetGroupSystemMsgEvent -> event.asProvider()
|
||||
is GetGroupIgnoredNotifiesEvent -> event.asProvider()
|
||||
else -> null
|
||||
}
|
||||
|
||||
provider?.getAllRequests()?.forEach { request ->
|
||||
if (!request.checked && request.groupId == targetGroupId) {
|
||||
LoggerUtil.logger.info("[$name] 处理请求: requestId=${request.requestId}, actor=${request.actor}")
|
||||
val answerAllow = answers.contains(request.message)
|
||||
if(answerAllow) {
|
||||
val info = napCatClient.send<GetStrangerInfoEvent>(GetStrangerInfoRequest(ID.long(request.requestId)))
|
||||
val levelAllow = info.data.qqLevel >= 16
|
||||
val setRequest = SetGroupAddRequestRequest(
|
||||
levelAllow,
|
||||
request.requestId.toString(),
|
||||
if(!levelAllow) "QQ等级低于16级" else ""
|
||||
)
|
||||
napCatClient.send<NapCatEvent>(setRequest)
|
||||
if (levelAllow) napCatClient.send<NapCatEvent>(SendGroupMsgRequest(listOf(MessageElement.text(formatRejectRecordMessage(request.requestId))), ID.long(targetGroupId)))
|
||||
LoggerUtil.logger.info("[$name] 已${if (levelAllow) "同意" else "拒绝"}请求${if(!levelAllow) ",等级不够,${info.data.qqLevel}" else "" }: ${request.requestId}")
|
||||
} else {
|
||||
napCatClient.sendUnit(SetGroupAddRequestRequest(false, request.requestId.toString(), "答案错误,拒绝次数:${getRejectRecord(request.requestId)?.rejectCount}"))
|
||||
addReject(request.actor, "答案错误:${request.message}")
|
||||
LoggerUtil.logger.info("[$name] 答案错误:${request.message},已拒绝请求: ${request.requestId}")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
fun formatRejectRecordMessage(userId: Long): String {
|
||||
val record = getRejectRecord(userId)
|
||||
return if (record != null) {
|
||||
"""
|
||||
用户QQ号:${record.userId}
|
||||
尝试次数:${record.rejectCount}
|
||||
最终评分:${rate(record.rejectCount)}
|
||||
尝试答案:【${record.reason.joinToString(",")}】
|
||||
""".trimIndent()
|
||||
} else {
|
||||
"""
|
||||
用户QQ号:${userId}
|
||||
尝试次数:0
|
||||
最终评分:SSS
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
private fun rate(count: Int): String = when (count) {
|
||||
0 -> "S"
|
||||
1 -> "A"
|
||||
2 -> "B"
|
||||
3 -> "C"
|
||||
4 -> "D"
|
||||
else -> "F"
|
||||
}
|
||||
interface GroupRequestProvider {
|
||||
fun getAllRequests(): List<GetGroupSystemMsgEvent.SystemInfo>
|
||||
}
|
||||
|
||||
private fun GetGroupSystemMsgEvent.asProvider(): GroupRequestProvider = object : GroupRequestProvider {
|
||||
override fun getAllRequests(): List<GetGroupSystemMsgEvent.SystemInfo> =
|
||||
data.invitedRequest + data.joinRequests
|
||||
}
|
||||
|
||||
private fun GetGroupIgnoredNotifiesEvent.asProvider(): GroupRequestProvider = object : GroupRequestProvider {
|
||||
override fun getAllRequests(): List<GetGroupSystemMsgEvent.SystemInfo> =
|
||||
data.invitedRequest + data.joinRequests
|
||||
}
|
||||
|
||||
override fun info(): String = """
|
||||
模块: $name
|
||||
功能: 自动处理指定群组的入群申请
|
||||
1. 根据答案列表自动同意或拒绝
|
||||
2. 拒绝记录会保存到本地,并可查询尝试次数和尝试答案
|
||||
3. 用户通过验证且等级满足要求时,会向群里发送消息,显示用户QQ号、尝试次数、评分和尝试答案
|
||||
版本: 1.0
|
||||
""".trimIndent()
|
||||
override fun help(): String = "轮询群组入群申请,根据答案列表自动同意或拒绝,并记录拒绝用户信息"
|
||||
}
|
||||
|
|
@ -6,6 +6,9 @@ class ModuleManager {
|
|||
|
||||
private val modules = mutableMapOf<String, BaseModule>()
|
||||
|
||||
fun getModules(): Map<String, BaseModule> {
|
||||
return (modules).toMap()
|
||||
}
|
||||
/**
|
||||
* 注册模块到管理器
|
||||
*/
|
||||
|
|
@ -74,6 +77,13 @@ class ModuleManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供获取所有模块信息的方法
|
||||
*/
|
||||
fun getAllModuleInfo(): Map<String, String> {
|
||||
return modules.mapValues { it.value.info() }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有模块名称
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
package top.r3944realms.ltdmanager.module
|
||||
|
||||
import top.r3944realms.ltdmanager.utils.FileNameFilter
|
||||
import java.io.File
|
||||
|
||||
interface PersistentState<T> {
|
||||
fun getStateFile(): File
|
||||
fun getStateFileInternal(): File
|
||||
fun getState(): T
|
||||
fun saveState(state: T)
|
||||
fun loadState(): T
|
||||
// 默认实现:统一管理 data 目录下的文件
|
||||
fun getStateFile(name: String): File {
|
||||
val dataDir = File("data")
|
||||
fun getStateFileInternal(name: String, moduleName: String): File {
|
||||
val dataDir = File("data", FileNameFilter.filterFileName(moduleName))
|
||||
if (!dataDir.exists()) dataDir.mkdirs()
|
||||
return File(dataDir, name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ import kotlinx.serialization.Serializable
|
|||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import top.r3944realms.ltdmanager.module.RconPlayerListModule.LastTriggerState
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownStateProvider
|
||||
import top.r3944realms.ltdmanager.module.common.filter.TriggerMessageFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.CooldownFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.IgnoreSelfFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.KeywordFilter
|
||||
import top.r3944realms.ltdmanager.module.common.filter.type.NewMessageFilter
|
||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||||
|
|
@ -18,26 +26,61 @@ import java.io.File
|
|||
import java.util.concurrent.TimeoutException
|
||||
|
||||
class RconPlayerListModule(
|
||||
moduleName: String,
|
||||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||
private val rconTimeOut: Long = 2_000L,
|
||||
private val cooldownMillis: Long = 30_000L,
|
||||
private var lastSuccessTime: Long = 0L,
|
||||
private val selfId: Long,
|
||||
private val selfNickName: String,
|
||||
private val rconPath: String,
|
||||
private val rconConfigPath: String,
|
||||
private val keywords: Set<String> = setOf("查看玩家列表", "玩家列表", "在线玩家")
|
||||
) : BaseModule(), PersistentState<LastTriggerState> {
|
||||
|
||||
override val name: String = "RconPlayerListModule"
|
||||
) : BaseModule("RconPlayerListModule", moduleName), PersistentState<LastTriggerState> {
|
||||
private val cooldownManager by lazy {
|
||||
CooldownManager(
|
||||
cooldownMillis = cooldownMillis,
|
||||
scope = CooldownScope.Global,
|
||||
stateProvider = object : CooldownStateProvider<LastTriggerState> {
|
||||
override fun load() = loadState()
|
||||
override fun save(state: LastTriggerState) = saveState(state)
|
||||
},
|
||||
getLastTrigger = { state, _ -> state.lastTriggerTime to state.lastTriggeredRealId },
|
||||
updateTrigger = { state, _, realId, time ->
|
||||
// ✅ 消息成功触发时更新状态
|
||||
state.updateTrigger(realId, time)
|
||||
state
|
||||
},
|
||||
updateCooldownRealId = { state, _, realId ->
|
||||
// ✅ 消息被冷却拒绝时更新 lastCooldownRealId
|
||||
state.updateCooldownRealId(realId)
|
||||
state
|
||||
},
|
||||
groupId = groupMessagePollingModule.targetGroupId
|
||||
)
|
||||
}
|
||||
/** 抽象过滤器组合 —— lazy 避免初始化顺序问题 */
|
||||
private val triggerFilter by lazy {
|
||||
TriggerMessageFilter(
|
||||
listOf(
|
||||
IgnoreSelfFilter(selfId),
|
||||
NewMessageFilter { _ ->
|
||||
lastTriggerState.lastTriggerTime to lastTriggerState.lastTriggeredRealId
|
||||
},
|
||||
KeywordFilter(keywords),
|
||||
CooldownFilter(cooldownManager) { msg, remain ->
|
||||
sendCooldownMessage(napCatClient, msg.realId, remain)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
private var scope : CoroutineScope? = null
|
||||
|
||||
// 持久化文件路径
|
||||
private val stateFile = getStateFile("rcon_playerlist_state.json")
|
||||
private val stateFile: File = getStateFileInternal("rcon_playerlist_state.json", name)
|
||||
|
||||
private val stateBackupFile = getStateFile("invitation_codes_quarry_state.json.bak")
|
||||
private val stateBackupFile: File = getStateFileInternal("rcon_playerlist_state.json.bak", name)
|
||||
|
||||
override fun getStateFile(): File = stateFile
|
||||
override fun getStateFileInternal(): File = stateFile
|
||||
|
||||
// 保存最新触发过的消息 realId 和 time
|
||||
private var lastTriggerState: LastTriggerState = loadState()
|
||||
|
|
@ -68,103 +111,54 @@ class RconPlayerListModule(
|
|||
}
|
||||
|
||||
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
|
||||
val triggerMessages = messages
|
||||
.asSequence() // 使用序列提高性能,特别是消息量大时
|
||||
.filter { msg ->
|
||||
((msg.time > lastTriggerState.lastTriggerTime ||
|
||||
(msg.time == lastTriggerState.lastTriggerTime && msg.realId > lastTriggerState.lastTriggeredRealId))
|
||||
&& msg.userId != selfId) &&
|
||||
msg.message.any { seg ->
|
||||
seg.type == MessageType.Text &&
|
||||
seg.data.text?.let { text -> keywords.any { keyword -> text == keyword } } == true
|
||||
}
|
||||
}.toList()
|
||||
val filtered = triggerFilter.filter(messages)
|
||||
|
||||
if (triggerMessages.isNotEmpty()) {
|
||||
val triggerMsg = triggerMessages.maxBy { it.time }
|
||||
LoggerUtil.logger.info("[$name] 找到触发消息 realId=${triggerMsg.realId}, time=${triggerMsg.time}, userId=${triggerMsg.userId}")
|
||||
processTrigger(triggerMsg)
|
||||
// RCON 模块只取最新的一条消息
|
||||
val triggerMsg = filtered.maxByOrNull { it.time }
|
||||
if (triggerMsg != null) {
|
||||
try {
|
||||
processTrigger(triggerMsg)
|
||||
} catch (e: Exception) {
|
||||
LoggerUtil.logger.error("[$name] 处理触发消息失败", e)
|
||||
sendFailedMessage(napCatClient, triggerMsg.realId, triggerMsg.time, "处理异常: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
private suspend fun processTrigger(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
|
||||
val now = System.currentTimeMillis()
|
||||
LoggerUtil.logger.info("[$name] 执行 RCON 查询")
|
||||
|
||||
// ✅ 冷却检查(首次触发直接允许)
|
||||
val canTrigger = (lastSuccessTime == 0L) || (now - lastSuccessTime >= cooldownMillis)
|
||||
if (!canTrigger) {
|
||||
val remaining = ((cooldownMillis - (now - lastSuccessTime)) / 1000).coerceAtLeast(1)
|
||||
LoggerUtil.logger.info("[$name] 冷却中,拒绝执行,剩余 $remaining 秒")
|
||||
sendCooldownMessage(napCatClient, msg.realId, msg.time)
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ 执行 RCON 命令
|
||||
val commands = listOf("forge tps", "list")
|
||||
LoggerUtil.logger.info("[$name] 执行 RCON 命令: $commands")
|
||||
|
||||
runCatching {
|
||||
val tpsOutput = runCatching {
|
||||
CmdUtil.runExeCommand(
|
||||
rconPath,
|
||||
"-c", rconConfigPath,
|
||||
"-T", (rconTimeOut / 1000).toString() + "s",
|
||||
"forge tps"
|
||||
)
|
||||
}.getOrElse { ex ->
|
||||
LoggerUtil.logger.warn("[$name] 执行 forge tps 失败: ${ex.message}")
|
||||
throw ex
|
||||
}
|
||||
|
||||
val listOutput = runCatching {
|
||||
CmdUtil.runExeCommand(
|
||||
rconPath,
|
||||
"-c", rconConfigPath,
|
||||
"-T", (rconTimeOut / 1000).toString() + "s",
|
||||
"list"
|
||||
)
|
||||
}.getOrElse { ex ->
|
||||
LoggerUtil.logger.warn("[$name] 执行 list 失败: ${ex.message}")
|
||||
throw ex
|
||||
}
|
||||
val tpsOutput = CmdUtil.runExeCommand(
|
||||
rconPath, "-c", rconConfigPath,
|
||||
"-T", (rconTimeOut / 1000).toString() + "s", "forge tps"
|
||||
)
|
||||
val listOutput = CmdUtil.runExeCommand(
|
||||
rconPath, "-c", rconConfigPath,
|
||||
"-T", (rconTimeOut / 1000).toString() + "s", "list"
|
||||
)
|
||||
|
||||
if (tpsOutput.contains("i/o timeout") || listOutput.contains("i/o timeout")) {
|
||||
throw TimeoutException()
|
||||
}
|
||||
|
||||
// 合并输出,后续一起解析
|
||||
buildString {
|
||||
appendLine(tpsOutput.trim())
|
||||
appendLine("--------")
|
||||
appendLine(listOutput.trim())
|
||||
}
|
||||
}.onFailure { ex ->
|
||||
lastSuccessTime = now // ✅ 成功/失败都要刷新冷却开始时间
|
||||
|
||||
LoggerUtil.logger.error("[$name] RCON 查询失败", ex)
|
||||
if (ex is TimeoutException) {
|
||||
LoggerUtil.logger.warn("[$name] RCON 连接超时: ${ex.message}")
|
||||
sendFailedMessage(napCatClient, msg.realId, msg.time)
|
||||
} else {
|
||||
LoggerUtil.logger.error("[$name] RCON 命令执行失败", ex)
|
||||
sendFailedMessage(
|
||||
napCatClient,
|
||||
msg.realId,
|
||||
msg.time,
|
||||
"系统内部错误请联系管理员:${ex.message}"
|
||||
)
|
||||
throw ex
|
||||
sendFailedMessage(napCatClient, msg.realId, msg.time, "⏳ RCON 连接超时")
|
||||
}
|
||||
throw ex
|
||||
}.onSuccess { output ->
|
||||
lastSuccessTime = now
|
||||
LoggerUtil.logger.info("[$name] RCON 命令执行成功,输出长度: ${output.length}")
|
||||
LoggerUtil.logger.debug("[$name] RCON 输出内容: $output")
|
||||
|
||||
val tpsInfo = parseTPS(output)
|
||||
val playerListInfo = parsePlayerList(output)
|
||||
|
||||
LoggerUtil.logger.info(
|
||||
"[$name] 解析成功: TPS=${tpsInfo.overall.meanTPS}, 在线 ${playerListInfo.onlineCount} 人"
|
||||
)
|
||||
|
||||
sendForwardMessage(napCatClient, tpsInfo, playerListInfo, msg.realId, msg.time)
|
||||
}
|
||||
|
||||
|
|
@ -175,11 +169,8 @@ class RconPlayerListModule(
|
|||
}
|
||||
|
||||
|
||||
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, time: Long) {
|
||||
val now = System.currentTimeMillis()
|
||||
val remaining = ((cooldownMillis - (now - lastSuccessTime)) / 1000).coerceAtLeast(1) // 至少显示 1 秒
|
||||
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, remaining: Long) {
|
||||
val msg = "⏳ 查询过于频繁,请稍后再试(剩余 $remaining 秒)"
|
||||
|
||||
LoggerUtil.logger.info("[$name] 发送冷却提示: $msg")
|
||||
|
||||
val request = SendGroupMsgRequest(
|
||||
|
|
@ -187,11 +178,6 @@ class RconPlayerListModule(
|
|||
ID.long(groupMessagePollingModule.targetGroupId)
|
||||
)
|
||||
client.sendUnit(request)
|
||||
|
||||
// 更新触发状态,但不更新 lastSuccessTime(避免延长冷却)
|
||||
lastTriggerState.lastTriggeredRealId = realId
|
||||
lastTriggerState.lastTriggerTime = time
|
||||
saveState(lastTriggerState)
|
||||
}
|
||||
|
||||
private val failedMessages = listOf(
|
||||
|
|
@ -479,13 +465,30 @@ class RconPlayerListModule(
|
|||
// ---------------- 持久化部分 ----------------
|
||||
|
||||
@Serializable
|
||||
data class LastTriggerState(var lastTriggeredRealId: Long, var lastTriggerTime: Long)
|
||||
data class LastTriggerState(
|
||||
var lastTriggeredRealId: Long = -1, // 上次允许处理消息ID
|
||||
var lastTriggerTime: Long = 0, // 上次允许处理时间(毫秒或秒都可以,根据你的逻辑)
|
||||
var lastCooldownRealId: Long = -1 // 上次冷却期间被拒绝的消息ID
|
||||
) {
|
||||
/** ✅ 冷却结束,更新触发状态 */
|
||||
fun updateTrigger(realId: Long, time: Long) {
|
||||
lastTriggeredRealId = realId
|
||||
lastTriggerTime = time
|
||||
// 保留 lastCooldownRealId 不变
|
||||
}
|
||||
|
||||
/** ⚠️ 冷却中,更新冷却消息ID */
|
||||
fun updateCooldownRealId(realId: Long) {
|
||||
lastCooldownRealId = realId
|
||||
// 保留 lastTriggeredRealId 和 lastTriggerTime
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveState(state: LastTriggerState) {
|
||||
try {
|
||||
// 先备份现有主文件
|
||||
if (stateFile.exists()) {
|
||||
stateFile.copyTo(File(stateFile.parent, stateFile.name + ".bak"), overwrite = true)
|
||||
stateFile.copyTo(stateBackupFile, overwrite = true)
|
||||
}
|
||||
|
||||
// 写入主文件
|
||||
|
|
@ -500,7 +503,7 @@ class RconPlayerListModule(
|
|||
return try {
|
||||
val fileToRead = when {
|
||||
stateFile.exists() -> stateFile
|
||||
File(stateFile.parent, stateFile.name + ".bak").exists() -> File(stateFile.parent, stateFile.name + ".bak")
|
||||
stateBackupFile.exists() -> stateBackupFile
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
|
@ -517,5 +520,36 @@ class RconPlayerListModule(
|
|||
LastTriggerState(-1L, 0L)
|
||||
}
|
||||
}
|
||||
// 返回模块基本信息
|
||||
override fun info(): String = buildString {
|
||||
appendLine("模块名称: $name")
|
||||
appendLine("模块类型: RconPlayerListModule")
|
||||
appendLine("目标群组: ${groupMessagePollingModule.targetGroupId}")
|
||||
appendLine("机器人昵称: $selfNickName (ID: $selfId)")
|
||||
appendLine("冷却时间: ${cooldownMillis / 1000} 秒")
|
||||
appendLine("RCON 命令路径: $rconPath")
|
||||
appendLine("RCON 配置文件路径: $rconConfigPath")
|
||||
appendLine("RCON 超时时间: $rconTimeOut ms")
|
||||
appendLine("关键词触发: ${keywords.joinToString(", ")}")
|
||||
appendLine("状态文件路径: ${stateFile.absolutePath}")
|
||||
appendLine("状态备份文件路径: ${stateBackupFile.absolutePath}")
|
||||
appendLine("上次触发消息ID: ${lastTriggerState.lastTriggeredRealId}")
|
||||
appendLine("上次触发时间: ${lastTriggerState.lastTriggerTime}")
|
||||
}
|
||||
|
||||
// 返回模块使用帮助
|
||||
override fun help(): String = buildString {
|
||||
appendLine("使用帮助 - RconPlayerListModule")
|
||||
appendLine("功能: 查询服务器 TPS 和在线玩家列表,通过关键词触发或冷却机制限制频率")
|
||||
appendLine("触发关键词: ${keywords.joinToString(", ")}")
|
||||
appendLine("示例:")
|
||||
keywords.forEach { keyword ->
|
||||
appendLine(" - 在群里发送 \"$keyword\" 将触发 RCON 查询")
|
||||
}
|
||||
appendLine("注意事项:")
|
||||
appendLine(" - 查询冷却时间为 ${cooldownMillis / 1000} 秒")
|
||||
appendLine(" - RCON 查询可能受服务器响应时间影响")
|
||||
appendLine(" - 查询结果会以转发消息形式发送到群组")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
package top.r3944realms.ltdmanager.module.util
|
||||
package top.r3944realms.ltdmanager.module.common
|
||||
|
||||
/**
|
||||
* 命令解析器
|
||||
* 严格模式:只支持命令后带空格的情况,避免误读
|
||||
*/
|
||||
class CommandParser(private val commands: List<String>) {
|
||||
|
||||
/**
|
||||
* 获取指令
|
||||
*/
|
||||
fun getCommands(): List<String> = commands
|
||||
/**
|
||||
* 解析命令
|
||||
* @param text 输入的文本
|
||||
|
|
|
|||
|
|
@ -1,30 +1,72 @@
|
|||
package top.r3944realms.ltdmanager.module.common
|
||||
package top.r3944realms.ltdmanager.module.common.cooldown
|
||||
|
||||
import top.r3944realms.ltdmanager.GlobalManager.napCatClient
|
||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||
import top.r3944realms.ltdmanager.napcat.request.group.SetGroupBanRequest
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
/**
|
||||
* 管理冷却
|
||||
* @param S 状态类型
|
||||
*/
|
||||
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 val getLastTrigger: (S, Long?) -> Pair<Long, Long>, // (lastTimeSec, lastRealId)
|
||||
private val updateTrigger: (S, Long?, Long, Long) -> S, // 更新 lastTimeSec, lastRealId
|
||||
private val updateCooldownRealId: (S, Long?, Long) -> S,
|
||||
private val groupId: Long, // 所属群组 ID,用于禁言
|
||||
private val banSeconds: Int = 60 // 重复发送禁言时间
|
||||
) {
|
||||
|
||||
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
|
||||
/**
|
||||
* 检查冷却
|
||||
* @param userId PerUser 模式必须
|
||||
* @param realId 消息 realId
|
||||
*/
|
||||
suspend fun checkAndHandle(userId: Long?, realId: Long): CooldownResult {
|
||||
require(scope != CooldownScope.PerUser || userId != null) { "userId required for per-user cooldown" }
|
||||
|
||||
val (lastTimeSec, lastCooldownRealId) = getLastTrigger(state, userId)
|
||||
val cooldownSec = cooldownMillis / 1000
|
||||
|
||||
return if (lastTime == -1L || nowSec - lastTime >= cooldownSec) {
|
||||
state = updateTrigger(state, userId, realId, msgTime)
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
val elapsed = if (lastTimeSec == -1L) Long.MAX_VALUE else now - lastTimeSec
|
||||
|
||||
return if (elapsed >= cooldownSec) {
|
||||
// ✅ 冷却结束,允许处理消息
|
||||
state = updateTrigger(state, userId, realId, now)
|
||||
stateProvider.save(state)
|
||||
CooldownResult(true)
|
||||
CooldownResult(allowed = true, remaining = 0, notify = false)
|
||||
} else {
|
||||
if (realId != lastCooldownRealId) {
|
||||
val remaining = max(0, cooldownSec - elapsed)
|
||||
val notify = realId != lastCooldownRealId // 第一次触发冷却提示
|
||||
|
||||
if (notify) {
|
||||
// 第一次冷却提示,记录消息 ID
|
||||
state = updateCooldownRealId(state, userId, realId)
|
||||
stateProvider.save(state)
|
||||
} else {
|
||||
// // ⚠️ 重复发送冷却消息 -> 禁言
|
||||
// if (userId != null) {
|
||||
// banUser(userId, groupId, banSeconds)
|
||||
// }
|
||||
}
|
||||
CooldownResult(false, cooldownSec - (nowSec - lastTime))
|
||||
|
||||
CooldownResult(allowed = false, remaining = remaining, notify = notify)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
package top.r3944realms.ltdmanager.module.common
|
||||
package top.r3944realms.ltdmanager.module.common.cooldown
|
||||
|
||||
/**
|
||||
* 冷却结果
|
||||
* @param allowed 是否允许触发
|
||||
* @param remaining 剩余秒数(如果未允许触发)
|
||||
* @param notify 是否可以发送冷却提示
|
||||
*/
|
||||
data class CooldownResult(
|
||||
val canTrigger: Boolean,
|
||||
val remainingSeconds: Long = 0
|
||||
val allowed: Boolean,
|
||||
val remaining: Long = 0L,
|
||||
val notify: Boolean = true
|
||||
)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package top.r3944realms.ltdmanager.module.common
|
||||
package top.r3944realms.ltdmanager.module.common.cooldown
|
||||
|
||||
sealed class CooldownScope {
|
||||
object Global : CooldownScope()
|
||||
object PerUser : CooldownScope()
|
||||
data object Global : CooldownScope()
|
||||
data object PerUser : CooldownScope()
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.module.common
|
||||
package top.r3944realms.ltdmanager.module.common.cooldown
|
||||
|
||||
interface CooldownStateProvider<S> {
|
||||
fun load(): S
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter
|
||||
|
||||
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
|
||||
|
||||
interface MessageFilter {
|
||||
suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
package top.r3944realms.ltdmanager.module.common
|
||||
package top.r3944realms.ltdmanager.module.common.filter
|
||||
|
||||
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>) {
|
||||
|
|
@ -16,55 +14,4 @@ class TriggerMessageFilter(private val filters: List<MessageFilter>) {
|
|||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,15 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter.type
|
||||
|
||||
class CommandFilter {
|
||||
import top.r3944realms.ltdmanager.module.common.CommandParser
|
||||
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
|
||||
import top.r3944realms.ltdmanager.napcat.data.MessageType
|
||||
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
|
||||
|
||||
/** 命令解析器匹配 */
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,19 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter.type
|
||||
|
||||
class CooldownFilter {
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
||||
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
|
||||
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
|
||||
|
||||
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.checkAndHandle(msg.userId, msg.realId)
|
||||
if (!result.allowed && result.notify) {
|
||||
sendCooldown(msg, result.remaining)
|
||||
}
|
||||
return result.allowed
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,11 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter.type
|
||||
|
||||
class IgnoreSelfFilter {
|
||||
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
|
||||
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
|
||||
|
||||
/** 忽略机器人自己的消息 */
|
||||
class IgnoreSelfFilter(private val selfId: Long) : MessageFilter {
|
||||
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
|
||||
return msg.userId != selfId
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,14 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter.type
|
||||
|
||||
class KeywordFilter {
|
||||
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
|
||||
import top.r3944realms.ltdmanager.napcat.data.MessageType
|
||||
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
|
||||
|
||||
/** 文本关键词匹配 */
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,18 @@
|
|||
package top.r3944realms.ltdmanager.module.common.filter.type
|
||||
|
||||
class NewMessageFilter {
|
||||
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
|
||||
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
|
||||
import top.r3944realms.ltdmanager.utils.Environment
|
||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||
|
||||
/** 只保留比上次触发更新的消息 */
|
||||
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)
|
||||
val result = msg.time > lastTime || (msg.time == lastTime && msg.realId > lastRealId)
|
||||
if (Environment.isDevelopment()) LoggerUtil.logger.debug("NewMessageFilter: msg.time=${msg.time}, msg.realId=${msg.realId}, lastTime=$lastTime, lastRealId=$lastRealId, result=$result")
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,120 @@
|
|||
package top.r3944realms.ltdmanager.utils
|
||||
|
||||
/**
|
||||
* 文件名非法字符过滤器
|
||||
*/
|
||||
object FileNameFilter {
|
||||
|
||||
// Windows系统非法字符
|
||||
private val WINDOWS_ILLEGAL_CHARS = setOf('\\', '/', ':', '*', '?', '"', '<', '>', '|')
|
||||
|
||||
// Unix系统非法字符(主要是/和空字符)
|
||||
private val UNIX_ILLEGAL_CHARS = setOf('/', '\u0000')
|
||||
|
||||
// 通用非法字符(控制字符)
|
||||
private val CONTROL_CHARS = (0x00..0x1F).map { it.toChar() }.toSet()
|
||||
|
||||
// Windows保留文件名
|
||||
private val WINDOWS_RESERVED_NAMES = setOf(
|
||||
"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
|
||||
)
|
||||
|
||||
/**
|
||||
* 过滤文件名中的非法字符
|
||||
* @param fileName 原始文件名(不包含路径)
|
||||
* @param systemType 目标系统类型
|
||||
* @param replaceChar 替换字符
|
||||
* @param handleReservedNames 是否处理保留名称
|
||||
* @return 过滤后的安全文件名
|
||||
*/
|
||||
fun filterFileName(
|
||||
fileName: String,
|
||||
systemType: SystemType = SystemType.CROSS_PLATFORM,
|
||||
replaceChar: Char = '_',
|
||||
handleReservedNames: Boolean = true
|
||||
): String {
|
||||
if (fileName.isEmpty()) return fileName
|
||||
|
||||
var filtered = when (systemType) {
|
||||
SystemType.WINDOWS -> filterForWindows(fileName, replaceChar)
|
||||
SystemType.UNIX -> filterForUnix(fileName, replaceChar)
|
||||
SystemType.CROSS_PLATFORM -> filterCrossPlatform(fileName, replaceChar)
|
||||
}
|
||||
|
||||
// 处理保留名称
|
||||
if (handleReservedNames && systemType != SystemType.UNIX) {
|
||||
filtered = handleReservedName(filtered, replaceChar)
|
||||
}
|
||||
|
||||
// 确保文件名不以点或空格结尾(某些系统有问题)
|
||||
filtered = filtered.trimEnd('.', ' ')
|
||||
|
||||
// 如果过滤后为空,返回默认名称
|
||||
return if (filtered.isEmpty()) "unnamed_file" else filtered
|
||||
}
|
||||
|
||||
/**
|
||||
* 为Windows系统过滤
|
||||
*/
|
||||
private fun filterForWindows(fileName: String, replaceChar: Char): String {
|
||||
val illegalChars = WINDOWS_ILLEGAL_CHARS + CONTROL_CHARS
|
||||
return fileName.map { if (it in illegalChars) replaceChar else it }.joinToString("")
|
||||
}
|
||||
|
||||
/**
|
||||
* 为Unix系统过滤
|
||||
*/
|
||||
private fun filterForUnix(fileName: String, replaceChar: Char): String {
|
||||
val illegalChars = UNIX_ILLEGAL_CHARS + CONTROL_CHARS
|
||||
return fileName.map { if (it in illegalChars) replaceChar else it }.joinToString("")
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨平台过滤
|
||||
*/
|
||||
private fun filterCrossPlatform(fileName: String, replaceChar: Char): String {
|
||||
val illegalChars = WINDOWS_ILLEGAL_CHARS + UNIX_ILLEGAL_CHARS + CONTROL_CHARS
|
||||
return fileName.map { if (it in illegalChars) replaceChar else it }.joinToString("")
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Windows保留名称
|
||||
*/
|
||||
private fun handleReservedName(fileName: String, replaceChar: Char): String {
|
||||
val nameWithoutExt = fileName.substringBeforeLast('.')
|
||||
val extension = fileName.substringAfterLast('.', "")
|
||||
|
||||
return if (WINDOWS_RESERVED_NAMES.contains(nameWithoutExt.uppercase())) {
|
||||
val newName = "${nameWithoutExt}$replaceChar"
|
||||
if (extension.isNotEmpty()) "$newName.$extension" else newName
|
||||
} else {
|
||||
fileName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量处理文件名
|
||||
*/
|
||||
fun batchFilterFileNames(
|
||||
fileNames: List<String>,
|
||||
systemType: SystemType = SystemType.CROSS_PLATFORM,
|
||||
replaceChar: Char = '_'
|
||||
): Map<String, String> {
|
||||
return fileNames.associateWith { filterFileName(it, systemType, replaceChar) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件名是否安全
|
||||
*/
|
||||
fun isFileNameSafe(
|
||||
fileName: String,
|
||||
systemType: SystemType = SystemType.CROSS_PLATFORM
|
||||
): Boolean {
|
||||
if (fileName.isEmpty()) return false
|
||||
|
||||
val filtered = filterFileName(fileName, systemType, '_', true)
|
||||
return fileName == filtered && fileName == filtered.trimEnd('.', ' ')
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
package top.r3944realms.ltdmanager.utils
|
||||
|
||||
/**
|
||||
* 系统类型枚举
|
||||
*/
|
||||
enum class SystemType {
|
||||
WINDOWS, UNIX, CROSS_PLATFORM
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
val mailModule = mailConfig.port?.let { portIt ->
|
||||
mailConfig.mailAddress?.let { mailAddressIt ->
|
||||
MailModule(
|
||||
moduleName = "WhiteListGroup",
|
||||
host = mailConfig.host.toString(),
|
||||
authToken = mailConfig.decryptedPassword.toString(),
|
||||
port = portIt,
|
||||
|
|
@ -29,7 +30,7 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
val expireHours = 24 // 有效期 24 小时
|
||||
val expireTime = LocalDateTime.now().plusHours(expireHours.toLong())
|
||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
|
||||
val bodyC = HtmlTemplateUtil.renderTemplate(template.file.toString(), mapOf(
|
||||
val bodyC = HtmlTemplateUtil.renderTemplateFromClasspath(template.file.toString(), mapOf(
|
||||
"player_name" to "小明",
|
||||
"activation_code" to "ABC123",
|
||||
"expire_time" to expireTime,
|
||||
|
|
|
|||
|
|
@ -1,2 +1,12 @@
|
|||
package top.r394realms.ltdmanagertest.misc
|
||||
|
||||
fun main() {
|
||||
val test = "/s 222"
|
||||
val startsWith = test.startsWith("/s")
|
||||
var removePrefix = "";
|
||||
if (startsWith) {
|
||||
removePrefix = test.removePrefix("/s")
|
||||
}
|
||||
println(startsWith)
|
||||
println(removePrefix)
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import top.r3944realms.ltdmanager.module.GroupRequestHandlerModule
|
|||
fun main() = GlobalManager.runBlockingMain {
|
||||
// 创建模块实例
|
||||
val groupModule = GroupRequestHandlerModule(
|
||||
moduleName = "WhiteListGroup",
|
||||
client = GlobalManager.napCatClient,
|
||||
targetGroupId = 538751386
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user