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.parallel=true
|
||||||
org.gradle.degree_of_parallelism=16
|
org.gradle.degree_of_parallelism=16
|
||||||
project_group=top.r3944realms.ltdmanager
|
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 selfNickName = "闲趣老土豆"
|
||||||
// 创建模块实例
|
// 创建模块实例
|
||||||
val groupModule = GroupRequestHandlerModule(
|
val groupModule = GroupRequestHandlerModule(
|
||||||
|
moduleName = "WhiteListGroup",
|
||||||
client = GlobalManager.napCatClient,
|
client = GlobalManager.napCatClient,
|
||||||
targetGroupId = groupId
|
targetGroupId = groupId
|
||||||
)
|
)
|
||||||
val groupMsgPollingModule = GroupMessagePollingModule(
|
val groupMsgPollingModule = GroupMessagePollingModule(
|
||||||
|
moduleName = "WhiteListGroup",
|
||||||
targetGroupId = groupId,
|
targetGroupId = groupId,
|
||||||
pollIntervalMillis = 5_000L,
|
pollIntervalMillis = 5_000L,
|
||||||
msgHistoryCheck = 15
|
msgHistoryCheck = 15
|
||||||
)
|
)
|
||||||
|
val helpModule = HelpModule(
|
||||||
|
moduleName = "WhiteListGroup",
|
||||||
|
groupMessagePollingModule = groupMsgPollingModule,
|
||||||
|
selfId = selfQQId,
|
||||||
|
selfNickName = selfNickName,
|
||||||
|
)
|
||||||
val toolConfig = YamlConfigLoader.loadToolConfig()
|
val toolConfig = YamlConfigLoader.loadToolConfig()
|
||||||
val rconModule = RconPlayerListModule(
|
val rconModule = RconPlayerListModule(
|
||||||
|
moduleName = "WhiteListGroup",
|
||||||
groupMessagePollingModule = groupMsgPollingModule,
|
groupMessagePollingModule = groupMsgPollingModule,
|
||||||
rconTimeOut = 2_000L,
|
rconTimeOut = 2_000L,
|
||||||
cooldownMillis = 10_000L,
|
cooldownMillis = 10_000L,
|
||||||
|
|
@ -38,6 +47,7 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
)
|
)
|
||||||
val mailConfig = YamlConfigLoader.loadMailConfig()
|
val mailConfig = YamlConfigLoader.loadMailConfig()
|
||||||
val mailModule = MailModule(
|
val mailModule = MailModule(
|
||||||
|
moduleName = "WhiteListGroup",
|
||||||
host = mailConfig.host.toString(),
|
host = mailConfig.host.toString(),
|
||||||
authToken = mailConfig.decryptedPassword.toString(),
|
authToken = mailConfig.decryptedPassword.toString(),
|
||||||
port = mailConfig.port!!,
|
port = mailConfig.port!!,
|
||||||
|
|
@ -45,6 +55,7 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
)
|
)
|
||||||
val blessingSkinConfig = YamlConfigLoader.loadBlessingSkinServerConfig()
|
val blessingSkinConfig = YamlConfigLoader.loadBlessingSkinServerConfig()
|
||||||
val invitationCodesModule = InvitationCodesModule(
|
val invitationCodesModule = InvitationCodesModule(
|
||||||
|
moduleName = "WhiteListGroup",
|
||||||
groupMessagePollingModule = groupMsgPollingModule,
|
groupMessagePollingModule = groupMsgPollingModule,
|
||||||
mailModule = mailModule,
|
mailModule = mailModule,
|
||||||
apiToken = blessingSkinConfig.invitationApi?.decryptedToken!!,
|
apiToken = blessingSkinConfig.invitationApi?.decryptedToken!!,
|
||||||
|
|
@ -57,9 +68,10 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val mcServerStatusModule = McServerStatusModule(
|
val mcServerStatusModule = McServerStatusModule(
|
||||||
|
moduleName = "WhiteListGroup",
|
||||||
groupMessagePollingModule = groupMsgPollingModule,
|
groupMessagePollingModule = groupMsgPollingModule,
|
||||||
selfId = selfQQId,
|
selfId = selfQQId,
|
||||||
cooldownSeconds = 20,
|
cooldownMillis = 20_000L,
|
||||||
selfNickName = selfNickName,
|
selfNickName = selfNickName,
|
||||||
commands = listOf("/m", "/mcs", "seek", "s"),
|
commands = listOf("/m", "/mcs", "seek", "s"),
|
||||||
presetServer = mapOf(
|
presetServer = mapOf(
|
||||||
|
|
@ -67,6 +79,19 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
setOf("土豆", "老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
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)
|
GlobalManager.moduleManager.registerModule(groupModule)
|
||||||
|
|
@ -75,6 +100,9 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
GlobalManager.moduleManager.registerModule(rconModule)
|
GlobalManager.moduleManager.registerModule(rconModule)
|
||||||
GlobalManager.moduleManager.registerModule(mailModule)
|
GlobalManager.moduleManager.registerModule(mailModule)
|
||||||
GlobalManager.moduleManager.registerModule(invitationCodesModule)
|
GlobalManager.moduleManager.registerModule(invitationCodesModule)
|
||||||
|
GlobalManager.moduleManager.registerModule(helpModule)
|
||||||
|
GlobalManager.moduleManager.registerModule(banModule)
|
||||||
|
GlobalManager.moduleManager.registerModule(modGroupHandlerModule)
|
||||||
|
|
||||||
// 加载模块
|
// 加载模块
|
||||||
GlobalManager.moduleManager.loadModule(groupModule.name)
|
GlobalManager.moduleManager.loadModule(groupModule.name)
|
||||||
|
|
@ -83,4 +111,7 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
GlobalManager.moduleManager.loadModule(rconModule.name)
|
GlobalManager.moduleManager.loadModule(rconModule.name)
|
||||||
GlobalManager.moduleManager.loadModule(mailModule.name)
|
GlobalManager.moduleManager.loadModule(mailModule.name)
|
||||||
GlobalManager.moduleManager.loadModule(invitationCodesModule.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,
|
moduleName: String,
|
||||||
private val groupMessagePollingModule : GroupMessagePollingModule,
|
private val groupMessagePollingModule : GroupMessagePollingModule,
|
||||||
private val selfId: Long,
|
private val selfId: Long,
|
||||||
commandPrefixList: List<String> = listOf("/mute"), // 默认命令前缀
|
commandPrefixList: List<String> = listOf("/mute"), // 默认命令前缀
|
||||||
private val minBanMinutes: Int = 1,
|
private val minBanMinutes: Int = 1,
|
||||||
private val maxBanMinutes: Int = 15
|
private val maxBanMinutes: Int = 15
|
||||||
) : BaseModule("CommandBanModule", moduleName), PersistentState<CommandBanModule.BanState> {
|
) : BaseModule("BanModule", moduleName), PersistentState<BanModule.BanState> {
|
||||||
|
|
||||||
private val commandParser = CommandParser(commandPrefixList)
|
private val commandParser = CommandParser(commandPrefixList)
|
||||||
private val commandFilter = CommandFilter(commandParser)
|
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
|
override fun getState(): BanState = banState
|
||||||
|
|
||||||
private val triggerFilter by lazy {
|
private val triggerFilter by lazy {
|
||||||
TriggerMessageFilter(
|
TriggerMessageFilter(
|
||||||
listOf(
|
listOf(
|
||||||
IgnoreSelfFilter(selfId),
|
IgnoreSelfFilter(selfId),
|
||||||
NewMessageFilter { _ -> banState.lastTriggerTime to banState.lastTriggerRealId },
|
NewMessageFilter { userId ->
|
||||||
|
banState.getLastTriggerTime(userId) to banState.getLastTriggerRealId(userId)
|
||||||
|
},
|
||||||
commandFilter
|
commandFilter
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var scope: CoroutineScope? = null
|
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 getStateFileInternal(): File = stateFile
|
||||||
|
|
||||||
|
|
@ -105,8 +108,8 @@ class CommandBanModule(
|
||||||
sendGroupMessage("✅ 你已被禁言 $durationMinutes 分钟", msg.realId)
|
sendGroupMessage("✅ 你已被禁言 $durationMinutes 分钟", msg.realId)
|
||||||
|
|
||||||
// 更新状态(保证状态保存正确)
|
// 更新状态(保证状态保存正确)
|
||||||
banState.lastTriggerRealId = msg.realId
|
// 禁言成功后更新状态
|
||||||
banState.lastTriggerTime = msg.time
|
banState = banState.updateLastTrigger(targetUserId, msg.realId, msg.time)
|
||||||
saveState(banState)
|
saveState(banState)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -152,11 +155,28 @@ class CommandBanModule(
|
||||||
|
|
||||||
// ---------------- 持久化 ----------------
|
// ---------------- 持久化 ----------------
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BanState(
|
data class UserBanDetail(
|
||||||
var lastTriggerRealId: Long = -1,
|
val realId: Long,
|
||||||
var lastTriggerTime: Long = 0
|
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) {
|
override fun saveState(state: BanState) {
|
||||||
try {
|
try {
|
||||||
if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true)
|
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) {}
|
} catch (_: CancellationException) {}
|
||||||
LoggerUtil.syncInfo("[$name] 模块已安全停止")
|
LoggerUtil.syncInfo("[$name] 模块已安全停止")
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 模块说明 / 帮助信息
|
||||||
|
* 默认返回空字符串,子类可重写提供具体帮助文本
|
||||||
|
*/
|
||||||
|
open fun help(): String = ""
|
||||||
|
/** 模块基础信息,用于 HelpModule 显示 */
|
||||||
|
open fun info(): String = "模块 $name 未提供详细信息"
|
||||||
/**
|
/**
|
||||||
* 提供访问全局 NapCatClient 的快捷方式
|
* 提供访问全局 NapCatClient 的快捷方式
|
||||||
*/
|
*/
|
||||||
|
|
@ -85,9 +92,14 @@ abstract class BaseModule {
|
||||||
* 提供访问全局 mcSrvStatusClient 的快捷方式
|
* 提供访问全局 mcSrvStatusClient 的快捷方式
|
||||||
*/
|
*/
|
||||||
protected val mcSrvStatusClient get() = GlobalManager.mcSrvStatusClient
|
protected val mcSrvStatusClient get() = GlobalManager.mcSrvStatusClient
|
||||||
|
/**
|
||||||
|
* 提供访问全局 加载模块 的快捷方式
|
||||||
|
*/
|
||||||
|
protected val moduleMap get() = GlobalManager.moduleManager.getModules()
|
||||||
/**
|
/**
|
||||||
* 获取数据库连接
|
* 获取数据库连接
|
||||||
* 使用 try-with-resources 时会自动关闭
|
* 使用 try-with-resources 时会自动关闭
|
||||||
*/
|
*/
|
||||||
protected fun getConnection() = GlobalManager.getConnection()
|
protected fun getConnection() = GlobalManager.getConnection()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -11,12 +11,11 @@ import top.r3944realms.ltdmanager.napcat.request.message.GetGroupMsgHistoryReque
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
|
||||||
class GroupMessagePollingModule(
|
class GroupMessagePollingModule(
|
||||||
|
moduleName: String,
|
||||||
val targetGroupId: Long,
|
val targetGroupId: Long,
|
||||||
private val pollIntervalMillis: Long = 5_000L,
|
private val pollIntervalMillis: Long = 5_000L,
|
||||||
private val msgHistoryCheck: Int = 15
|
private val msgHistoryCheck: Int = 15,
|
||||||
) : BaseModule() {
|
) : BaseModule("MessagePollingModule", moduleName) {
|
||||||
|
|
||||||
override val name: String = "MessagePollingModule"
|
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
|
|
||||||
// 用 Flow 存消息,其他模块可以订阅
|
// 用 Flow 存消息,其他模块可以订阅
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,11 @@ import top.r3944realms.ltdmanager.napcat.request.group.SetGroupAddRequestRequest
|
||||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||||
|
|
||||||
class GroupRequestHandlerModule(
|
class GroupRequestHandlerModule(
|
||||||
|
moduleName: String,
|
||||||
private val client: NapCatClient,
|
private val client: NapCatClient,
|
||||||
private val targetGroupId: Long,
|
private val targetGroupId: Long,
|
||||||
private val pollIntervalMillis: Long = 30_000L,
|
private val pollIntervalMillis: Long = 30_000L,
|
||||||
) : BaseModule() {
|
) : BaseModule("GroupRequestHandlerModule", moduleName) {
|
||||||
|
|
||||||
override val name: String = "GroupRequestHandlerModule"
|
|
||||||
|
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
|
|
||||||
|
|
@ -176,4 +175,7 @@ class GroupRequestHandlerModule(
|
||||||
return data.invitedRequest + data.joinRequests
|
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
|
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.ResponseResult
|
||||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||||
import top.r3944realms.ltdmanager.core.mail.mail
|
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.module.exception.InvitationCodeException
|
||||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
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.event.message.GetFriendMsgHistoryEvent
|
||||||
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
|
||||||
import top.r3944realms.ltdmanager.utils.HtmlTemplateUtil
|
import top.r3944realms.ltdmanager.utils.HtmlTemplateUtil
|
||||||
|
|
@ -63,24 +70,67 @@ api格式 https://skins.r3944realms.top/api/invitation-codes/generate?token=XXXX
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class InvitationCodesModule(
|
class InvitationCodesModule(
|
||||||
|
moduleName: String,
|
||||||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||||
private val mailModule: MailModule,
|
private val mailModule: MailModule,
|
||||||
private val apiToken: String,
|
private val apiToken: String,
|
||||||
private val selfId: Long,
|
selfId: Long,
|
||||||
private val cooldownMillis: Long = 120_000,
|
private val cooldownMillis: Long = 120_000,
|
||||||
private val keywords: Set<String> = setOf("申请邀请码")
|
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 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 val fileLock = ReentrantLock()
|
||||||
|
|
||||||
private var lastTriggerMapState = loadState()
|
private var lastTriggerMapState = loadState()
|
||||||
override fun getStateFile(): File = stateFile
|
override fun getStateFileInternal(): File = stateFile
|
||||||
override fun getState(): LastTriggerMapState = lastTriggerMapState
|
override fun getState(): LastTriggerMapState = lastTriggerMapState
|
||||||
override fun onLoad() {
|
override fun onLoad() {
|
||||||
LoggerUtil.logger.info("[$name] 模块已装载,目标群组: ${groupMessagePollingModule.targetGroupId}")
|
LoggerUtil.logger.info("[$name] 模块已装载,目标群组: ${groupMessagePollingModule.targetGroupId}")
|
||||||
|
|
@ -135,29 +185,17 @@ class InvitationCodesModule(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 过滤出符合条件的触发消息 */
|
/** 过滤出符合条件的触发消息 */
|
||||||
private fun filterTriggerMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>)
|
private suspend fun filterTriggerMessages(
|
||||||
: List<GetFriendMsgHistoryEvent.SpecificMsg> {
|
messages: List<GetFriendMsgHistoryEvent.SpecificMsg>
|
||||||
|
): List<GetFriendMsgHistoryEvent.SpecificMsg> {
|
||||||
|
|
||||||
val filtered = messages.asSequence()
|
// 先应用通用过滤器
|
||||||
.filter { msg ->
|
val filtered = triggerFilter.filter(messages)
|
||||||
msg.userId != selfId &&
|
|
||||||
(msg.time > lastTriggerMapState.getLastTriggerTime(msg.userId) ||
|
// 再做 groupBy -> 只保留每个用户最新一条
|
||||||
(msg.time == lastTriggerMapState.getLastTriggerTime(msg.userId)
|
return filtered
|
||||||
&& 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.groupBy { it.userId }
|
.groupBy { it.userId }
|
||||||
.mapNotNull { (_, msgs) -> msgs.maxByOrNull { it.time } }
|
.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>,
|
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) {
|
private suspend fun sendCooldownMessage(client: NapCatClient, qq: Long, realId: Long, msg: String) {
|
||||||
val request = SendGroupMsgRequest(
|
val request = SendGroupMsgRequest(
|
||||||
MessageElement.reply(ID.long(realId), msg),
|
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
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
class MailModule(
|
class MailModule(
|
||||||
|
moduleName: String,
|
||||||
private val protocol: String = "SMTP",
|
private val protocol: String = "SMTP",
|
||||||
private val host: String,
|
private val host: String,
|
||||||
private val port: Int,
|
private val port: Int,
|
||||||
|
|
@ -18,9 +19,7 @@ class MailModule(
|
||||||
private val enableAuth: Boolean = true,
|
private val enableAuth: Boolean = true,
|
||||||
private val enableTLS: Boolean = true,
|
private val enableTLS: Boolean = true,
|
||||||
private val intervalMillis: Long = 2000L // 每封邮件之间的间隔(默认 2s)
|
private val intervalMillis: Long = 2000L // 每封邮件之间的间隔(默认 2s)
|
||||||
) : BaseModule() {
|
) : BaseModule("MailModule", moduleName) {
|
||||||
|
|
||||||
override val name: String = "MailModule"
|
|
||||||
|
|
||||||
private lateinit var session: Session
|
private lateinit var session: Session
|
||||||
private val queue = LinkedBlockingQueue<Mail>() // 邮件队列
|
private val queue = LinkedBlockingQueue<Mail>() // 邮件队列
|
||||||
|
|
@ -142,4 +141,36 @@ class MailModule(
|
||||||
|
|
||||||
Transport.send(message)
|
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.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import top.r3944realms.ltdmanager.mcserver.McServerStatus
|
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.NapCatClient
|
||||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||||||
|
|
@ -17,30 +26,72 @@ import java.util.concurrent.locks.ReentrantLock
|
||||||
import kotlin.concurrent.withLock
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
class McServerStatusModule(
|
class McServerStatusModule(
|
||||||
|
moduleName: String,
|
||||||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||||
private val selfId: Long,
|
private val selfId: Long,
|
||||||
private val selfNickName: String,
|
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 commands: List<String> = listOf("/mcs", "/s"),
|
||||||
private val presetServer: Map<Set<String>, String> = mapOf(
|
private val presetServer: Map<Set<String>, String> = mapOf(
|
||||||
setOf("hp", "hypixel") to "mc.hypixel.net",
|
setOf("hp", "hypixel") to "mc.hypixel.net",
|
||||||
setOf("pm", "mineplex") to "play.mineplex.com"
|
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 {
|
private val presetServerByAlias: Map<String, String> by lazy {
|
||||||
presetServer.flatMap { (aliases, ip) ->
|
presetServer.flatMap { (aliases, ip) ->
|
||||||
aliases.map { it.lowercase() to ip }
|
aliases.map { it.lowercase() to ip }
|
||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
fun getServerIp(alias: String): String? = presetServerByAlias[alias.lowercase()]
|
fun getServerIp(alias: String): String? = presetServerByAlias[alias.lowercase()]
|
||||||
override val name: String = "McServerStatusModule"
|
|
||||||
private var scope: CoroutineScope? = null
|
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 val fileLock = ReentrantLock()
|
||||||
private var cooldownState = loadState()
|
private var cooldownState = loadState()
|
||||||
|
|
||||||
override fun getStateFile(): File = stateFile
|
override fun getStateFileInternal(): File = stateFile
|
||||||
override fun getState(): CooldownState = cooldownState
|
override fun getState(): CooldownState = cooldownState
|
||||||
|
|
||||||
override fun onLoad() {
|
override fun onLoad() {
|
||||||
|
|
@ -76,32 +127,11 @@ class McServerStatusModule(
|
||||||
saveState(cooldownState)
|
saveState(cooldownState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private suspend fun filterTriggerMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>)
|
|
||||||
: List<GetFriendMsgHistoryEvent.SpecificMsg> {
|
|
||||||
|
|
||||||
val filtered = messages.asSequence()
|
private suspend fun filterTriggerMessages(
|
||||||
.filter { msg ->
|
messages: List<GetFriendMsgHistoryEvent.SpecificMsg>
|
||||||
// 忽略自己消息
|
): List<GetFriendMsgHistoryEvent.SpecificMsg> = triggerFilter.filter(messages)
|
||||||
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()
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
private suspend fun sendFailedMessage(
|
private suspend fun sendFailedMessage(
|
||||||
client: NapCatClient,
|
client: NapCatClient,
|
||||||
qq: Long? = null,
|
qq: Long? = null,
|
||||||
|
|
@ -129,31 +159,7 @@ class McServerStatusModule(
|
||||||
LoggerUtil.logger.info("[$name] 已发送 失败消息[无指定对象]")
|
LoggerUtil.logger.info("[$name] 已发送 失败消息[无指定对象]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** 冷却提示消息 */
|
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, text: String) {
|
||||||
|
|
||||||
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) {
|
|
||||||
val request = SendGroupMsgRequest(
|
val request = SendGroupMsgRequest(
|
||||||
MessageElement.reply(ID.long(realId), text),
|
MessageElement.reply(ID.long(realId), text),
|
||||||
ID.long(groupMessagePollingModule.targetGroupId)
|
ID.long(groupMessagePollingModule.targetGroupId)
|
||||||
|
|
@ -171,16 +177,18 @@ class McServerStatusModule(
|
||||||
?.trim()
|
?.trim()
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
// 解析命令
|
// 使用命令解析器解析命令
|
||||||
val matchedCommand = commands.firstOrNull { text.startsWith(it) } ?: return
|
val parsedCommand = commandParser.parseCommand(text) ?: return
|
||||||
var address = text.removePrefix(matchedCommand).trim()
|
val (_, address) = parsedCommand
|
||||||
|
|
||||||
// 使用预设别名替换
|
// 使用预设别名替换
|
||||||
presetServerByAlias[address.lowercase()]?.let { presetIp ->
|
val finalAddress = if (address.isNotEmpty()) {
|
||||||
address = presetIp
|
presetServerByAlias[address.lowercase()] ?: address
|
||||||
|
} else {
|
||||||
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
if (address.isEmpty()) {
|
if (finalAddress.isEmpty()) {
|
||||||
sendFailedMessage(
|
sendFailedMessage(
|
||||||
napCatClient,
|
napCatClient,
|
||||||
msg.userId,
|
msg.userId,
|
||||||
|
|
@ -192,9 +200,8 @@ class McServerStatusModule(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val status = mcSrvStatusClient.getServerStatus(address) // 返回 McServerStatus
|
val status = mcSrvStatusClient.getServerStatus(finalAddress)
|
||||||
|
|
||||||
// 检查是否查询失败
|
|
||||||
if (!status.online) {
|
if (!status.online) {
|
||||||
sendFailedMessage(
|
sendFailedMessage(
|
||||||
napCatClient, msg.userId, msg.realId, msg.time,
|
napCatClient, msg.userId, msg.realId, msg.time,
|
||||||
|
|
@ -203,9 +210,7 @@ class McServerStatusModule(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询成功,发送状态消息
|
sendStatusForwardMessage(napCatClient, msg, finalAddress, status, msg.realId, msg.time)
|
||||||
sendStatusForwardMessage(napCatClient, msg, address, status, msg.realId, msg.time)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LoggerUtil.logger.error("查询服务器状态失败: $address", e)
|
LoggerUtil.logger.error("查询服务器状态失败: $address", e)
|
||||||
sendFailedMessage(
|
sendFailedMessage(
|
||||||
|
|
@ -311,23 +316,36 @@ class McServerStatusModule(
|
||||||
data class CooldownState(
|
data class CooldownState(
|
||||||
val map: Map<Long, TriggerDetail> = emptyMap()
|
val map: Map<Long, TriggerDetail> = emptyMap()
|
||||||
) {
|
) {
|
||||||
|
// 获取上次处理时间
|
||||||
fun getLastTriggerTime(qq: Long): Long = map[qq]?.time ?: -1
|
fun getLastTriggerTime(qq: Long): Long = map[qq]?.time ?: -1
|
||||||
|
|
||||||
|
// 获取上次处理消息ID
|
||||||
fun getLastTriggerRealId(qq: Long): Long = map[qq]?.realId ?: -1
|
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 old = map[qq]
|
||||||
val newTime = if (time != -1L) time else old?.time ?: -1
|
|
||||||
val newMap = map.toMutableMap().apply {
|
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)
|
return copy(map = newMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 冷却中消息 → 只更新 lastCooldownRealId,保留 time 和 realId
|
||||||
fun updateLastCooldownRealId(qq: Long, realId: Long): CooldownState {
|
fun updateLastCooldownRealId(qq: Long, realId: Long): CooldownState {
|
||||||
val old = map[qq]
|
val old = map[qq]
|
||||||
val newMap = map.toMutableMap().apply {
|
val newMap = map.toMutableMap().apply {
|
||||||
put(qq, TriggerDetail(
|
put(qq, TriggerDetail(
|
||||||
realId = old?.realId ?: -1,
|
realId = old?.realId ?: -1, // 保持上次允许处理的消息ID
|
||||||
time = old?.time ?: -1,
|
time = old?.time ?: -1, // 保持上次允许处理的时间
|
||||||
lastCooldownRealId = realId
|
lastCooldownRealId = realId // 更新当前冷却拒绝的消息ID
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
return copy(map = newMap)
|
return copy(map = newMap)
|
||||||
|
|
@ -336,9 +354,9 @@ class McServerStatusModule(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TriggerDetail(
|
data class TriggerDetail(
|
||||||
val realId: Long,
|
val realId: Long, // 上次允许处理消息ID
|
||||||
val time: Long,
|
val time: Long, // 上次允许处理消息时间(秒)
|
||||||
val lastCooldownRealId: Long = -1L
|
val lastCooldownRealId: Long = -1 // 上次被冷却拒绝的消息ID
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun loadState(): CooldownState {
|
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
|
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>()
|
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
|
package top.r3944realms.ltdmanager.module
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.utils.FileNameFilter
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
interface PersistentState<T> {
|
interface PersistentState<T> {
|
||||||
fun getStateFile(): File
|
fun getStateFileInternal(): File
|
||||||
fun getState(): T
|
fun getState(): T
|
||||||
fun saveState(state: T)
|
fun saveState(state: T)
|
||||||
fun loadState(): T
|
fun loadState(): T
|
||||||
// 默认实现:统一管理 data 目录下的文件
|
// 默认实现:统一管理 data 目录下的文件
|
||||||
fun getStateFile(name: String): File {
|
fun getStateFileInternal(name: String, moduleName: String): File {
|
||||||
val dataDir = File("data")
|
val dataDir = File("data", FileNameFilter.filterFileName(moduleName))
|
||||||
if (!dataDir.exists()) dataDir.mkdirs()
|
if (!dataDir.exists()) dataDir.mkdirs()
|
||||||
return File(dataDir, name)
|
return File(dataDir, name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,14 @@ import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import top.r3944realms.ltdmanager.module.RconPlayerListModule.LastTriggerState
|
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.NapCatClient
|
||||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||||||
|
|
@ -18,26 +26,61 @@ import java.io.File
|
||||||
import java.util.concurrent.TimeoutException
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
class RconPlayerListModule(
|
class RconPlayerListModule(
|
||||||
|
moduleName: String,
|
||||||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||||
private val rconTimeOut: Long = 2_000L,
|
private val rconTimeOut: Long = 2_000L,
|
||||||
private val cooldownMillis: Long = 30_000L,
|
private val cooldownMillis: Long = 30_000L,
|
||||||
private var lastSuccessTime: Long = 0L,
|
|
||||||
private val selfId: Long,
|
private val selfId: Long,
|
||||||
private val selfNickName: String,
|
private val selfNickName: String,
|
||||||
private val rconPath: String,
|
private val rconPath: String,
|
||||||
private val rconConfigPath: String,
|
private val rconConfigPath: String,
|
||||||
private val keywords: Set<String> = setOf("查看玩家列表", "玩家列表", "在线玩家")
|
private val keywords: Set<String> = setOf("查看玩家列表", "玩家列表", "在线玩家")
|
||||||
) : BaseModule(), PersistentState<LastTriggerState> {
|
) : BaseModule("RconPlayerListModule", moduleName), PersistentState<LastTriggerState> {
|
||||||
|
private val cooldownManager by lazy {
|
||||||
override val name: String = "RconPlayerListModule"
|
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 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
|
// 保存最新触发过的消息 realId 和 time
|
||||||
private var lastTriggerState: LastTriggerState = loadState()
|
private var lastTriggerState: LastTriggerState = loadState()
|
||||||
|
|
@ -68,103 +111,54 @@ class RconPlayerListModule(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
|
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
|
||||||
val triggerMessages = messages
|
val filtered = triggerFilter.filter(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()
|
|
||||||
|
|
||||||
if (triggerMessages.isNotEmpty()) {
|
// RCON 模块只取最新的一条消息
|
||||||
val triggerMsg = triggerMessages.maxBy { it.time }
|
val triggerMsg = filtered.maxByOrNull { it.time }
|
||||||
LoggerUtil.logger.info("[$name] 找到触发消息 realId=${triggerMsg.realId}, time=${triggerMsg.time}, userId=${triggerMsg.userId}")
|
if (triggerMsg != null) {
|
||||||
|
try {
|
||||||
processTrigger(triggerMsg)
|
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) {
|
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")
|
val commands = listOf("forge tps", "list")
|
||||||
LoggerUtil.logger.info("[$name] 执行 RCON 命令: $commands")
|
LoggerUtil.logger.info("[$name] 执行 RCON 命令: $commands")
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
val tpsOutput = runCatching {
|
val tpsOutput = CmdUtil.runExeCommand(
|
||||||
CmdUtil.runExeCommand(
|
rconPath, "-c", rconConfigPath,
|
||||||
rconPath,
|
"-T", (rconTimeOut / 1000).toString() + "s", "forge tps"
|
||||||
"-c", rconConfigPath,
|
|
||||||
"-T", (rconTimeOut / 1000).toString() + "s",
|
|
||||||
"forge tps"
|
|
||||||
)
|
)
|
||||||
}.getOrElse { ex ->
|
val listOutput = CmdUtil.runExeCommand(
|
||||||
LoggerUtil.logger.warn("[$name] 执行 forge tps 失败: ${ex.message}")
|
rconPath, "-c", rconConfigPath,
|
||||||
throw ex
|
"-T", (rconTimeOut / 1000).toString() + "s", "list"
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tpsOutput.contains("i/o timeout") || listOutput.contains("i/o timeout")) {
|
if (tpsOutput.contains("i/o timeout") || listOutput.contains("i/o timeout")) {
|
||||||
throw TimeoutException()
|
throw TimeoutException()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并输出,后续一起解析
|
|
||||||
buildString {
|
buildString {
|
||||||
appendLine(tpsOutput.trim())
|
appendLine(tpsOutput.trim())
|
||||||
appendLine("--------")
|
appendLine("--------")
|
||||||
appendLine(listOutput.trim())
|
appendLine(listOutput.trim())
|
||||||
}
|
}
|
||||||
}.onFailure { ex ->
|
}.onFailure { ex ->
|
||||||
lastSuccessTime = now // ✅ 成功/失败都要刷新冷却开始时间
|
LoggerUtil.logger.error("[$name] RCON 查询失败", ex)
|
||||||
|
|
||||||
if (ex is TimeoutException) {
|
if (ex is TimeoutException) {
|
||||||
LoggerUtil.logger.warn("[$name] RCON 连接超时: ${ex.message}")
|
sendFailedMessage(napCatClient, msg.realId, msg.time, "⏳ RCON 连接超时")
|
||||||
sendFailedMessage(napCatClient, msg.realId, msg.time)
|
|
||||||
} else {
|
|
||||||
LoggerUtil.logger.error("[$name] RCON 命令执行失败", ex)
|
|
||||||
sendFailedMessage(
|
|
||||||
napCatClient,
|
|
||||||
msg.realId,
|
|
||||||
msg.time,
|
|
||||||
"系统内部错误请联系管理员:${ex.message}"
|
|
||||||
)
|
|
||||||
throw ex
|
|
||||||
}
|
}
|
||||||
|
throw ex
|
||||||
}.onSuccess { output ->
|
}.onSuccess { output ->
|
||||||
lastSuccessTime = now
|
|
||||||
LoggerUtil.logger.info("[$name] RCON 命令执行成功,输出长度: ${output.length}")
|
|
||||||
LoggerUtil.logger.debug("[$name] RCON 输出内容: $output")
|
|
||||||
|
|
||||||
val tpsInfo = parseTPS(output)
|
val tpsInfo = parseTPS(output)
|
||||||
val playerListInfo = parsePlayerList(output)
|
val playerListInfo = parsePlayerList(output)
|
||||||
|
|
||||||
LoggerUtil.logger.info(
|
|
||||||
"[$name] 解析成功: TPS=${tpsInfo.overall.meanTPS}, 在线 ${playerListInfo.onlineCount} 人"
|
|
||||||
)
|
|
||||||
|
|
||||||
sendForwardMessage(napCatClient, tpsInfo, playerListInfo, msg.realId, msg.time)
|
sendForwardMessage(napCatClient, tpsInfo, playerListInfo, msg.realId, msg.time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,11 +169,8 @@ class RconPlayerListModule(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, time: Long) {
|
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, remaining: Long) {
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val remaining = ((cooldownMillis - (now - lastSuccessTime)) / 1000).coerceAtLeast(1) // 至少显示 1 秒
|
|
||||||
val msg = "⏳ 查询过于频繁,请稍后再试(剩余 $remaining 秒)"
|
val msg = "⏳ 查询过于频繁,请稍后再试(剩余 $remaining 秒)"
|
||||||
|
|
||||||
LoggerUtil.logger.info("[$name] 发送冷却提示: $msg")
|
LoggerUtil.logger.info("[$name] 发送冷却提示: $msg")
|
||||||
|
|
||||||
val request = SendGroupMsgRequest(
|
val request = SendGroupMsgRequest(
|
||||||
|
|
@ -187,11 +178,6 @@ class RconPlayerListModule(
|
||||||
ID.long(groupMessagePollingModule.targetGroupId)
|
ID.long(groupMessagePollingModule.targetGroupId)
|
||||||
)
|
)
|
||||||
client.sendUnit(request)
|
client.sendUnit(request)
|
||||||
|
|
||||||
// 更新触发状态,但不更新 lastSuccessTime(避免延长冷却)
|
|
||||||
lastTriggerState.lastTriggeredRealId = realId
|
|
||||||
lastTriggerState.lastTriggerTime = time
|
|
||||||
saveState(lastTriggerState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val failedMessages = listOf(
|
private val failedMessages = listOf(
|
||||||
|
|
@ -479,13 +465,30 @@ class RconPlayerListModule(
|
||||||
// ---------------- 持久化部分 ----------------
|
// ---------------- 持久化部分 ----------------
|
||||||
|
|
||||||
@Serializable
|
@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) {
|
override fun saveState(state: LastTriggerState) {
|
||||||
try {
|
try {
|
||||||
// 先备份现有主文件
|
// 先备份现有主文件
|
||||||
if (stateFile.exists()) {
|
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 {
|
return try {
|
||||||
val fileToRead = when {
|
val fileToRead = when {
|
||||||
stateFile.exists() -> stateFile
|
stateFile.exists() -> stateFile
|
||||||
File(stateFile.parent, stateFile.name + ".bak").exists() -> File(stateFile.parent, stateFile.name + ".bak")
|
stateBackupFile.exists() -> stateBackupFile
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -517,5 +520,36 @@ class RconPlayerListModule(
|
||||||
LastTriggerState(-1L, 0L)
|
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>) {
|
class CommandParser(private val commands: List<String>) {
|
||||||
|
/**
|
||||||
|
* 获取指令
|
||||||
|
*/
|
||||||
|
fun getCommands(): List<String> = commands
|
||||||
/**
|
/**
|
||||||
* 解析命令
|
* 解析命令
|
||||||
* @param text 输入的文本
|
* @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>(
|
class CooldownManager<S>(
|
||||||
private val cooldownMillis: Long,
|
private val cooldownMillis: Long,
|
||||||
private val scope: CooldownScope,
|
private val scope: CooldownScope,
|
||||||
private val stateProvider: CooldownStateProvider<S>,
|
private val stateProvider: CooldownStateProvider<S>,
|
||||||
private val getLastTrigger: (S, Long?) -> Pair<Long, Long>, // (time, realId)
|
private val getLastTrigger: (S, Long?) -> Pair<Long, Long>, // (lastTimeSec, lastRealId)
|
||||||
private val updateTrigger: (S, Long?, Long, Long) -> S, // (qq, realId, time)
|
private val updateTrigger: (S, Long?, Long, Long) -> S, // 更新 lastTimeSec, lastRealId
|
||||||
private val updateCooldownRealId: (S, Long?, Long) -> S // (qq, realId)
|
private val updateCooldownRealId: (S, Long?, Long) -> S,
|
||||||
|
private val groupId: Long, // 所属群组 ID,用于禁言
|
||||||
|
private val banSeconds: Int = 60 // 重复发送禁言时间
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var state: S = stateProvider.load()
|
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
|
val cooldownSec = cooldownMillis / 1000
|
||||||
|
|
||||||
return if (lastTime == -1L || nowSec - lastTime >= cooldownSec) {
|
val now = System.currentTimeMillis() / 1000
|
||||||
state = updateTrigger(state, userId, realId, msgTime)
|
val elapsed = if (lastTimeSec == -1L) Long.MAX_VALUE else now - lastTimeSec
|
||||||
|
|
||||||
|
return if (elapsed >= cooldownSec) {
|
||||||
|
// ✅ 冷却结束,允许处理消息
|
||||||
|
state = updateTrigger(state, userId, realId, now)
|
||||||
stateProvider.save(state)
|
stateProvider.save(state)
|
||||||
CooldownResult(true)
|
CooldownResult(allowed = true, remaining = 0, notify = false)
|
||||||
} else {
|
} else {
|
||||||
if (realId != lastCooldownRealId) {
|
val remaining = max(0, cooldownSec - elapsed)
|
||||||
|
val notify = realId != lastCooldownRealId // 第一次触发冷却提示
|
||||||
|
|
||||||
|
if (notify) {
|
||||||
|
// 第一次冷却提示,记录消息 ID
|
||||||
state = updateCooldownRealId(state, userId, realId)
|
state = updateCooldownRealId(state, userId, realId)
|
||||||
stateProvider.save(state)
|
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(
|
data class CooldownResult(
|
||||||
val canTrigger: Boolean,
|
val allowed: Boolean,
|
||||||
val remainingSeconds: Long = 0
|
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 {
|
sealed class CooldownScope {
|
||||||
object Global : CooldownScope()
|
data object Global : CooldownScope()
|
||||||
object PerUser : 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> {
|
interface CooldownStateProvider<S> {
|
||||||
fun load(): S
|
fun load(): S
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
package top.r3944realms.ltdmanager.module.common.filter
|
package top.r3944realms.ltdmanager.module.common.filter
|
||||||
|
|
||||||
|
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
|
||||||
|
|
||||||
interface MessageFilter {
|
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
|
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
|
||||||
|
|
||||||
class TriggerMessageFilter(private val filters: List<MessageFilter>) {
|
class TriggerMessageFilter(private val filters: List<MessageFilter>) {
|
||||||
|
|
@ -17,54 +15,3 @@ class TriggerMessageFilter(private val filters: List<MessageFilter>) {
|
||||||
return result
|
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
|
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
|
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
|
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
|
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
|
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
|
package top.r3944realms.ltdmanager.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件名非法字符过滤器
|
||||||
|
*/
|
||||||
object FileNameFilter {
|
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
|
package top.r3944realms.ltdmanager.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统类型枚举
|
||||||
|
*/
|
||||||
enum class SystemType {
|
enum class SystemType {
|
||||||
|
WINDOWS, UNIX, CROSS_PLATFORM
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
val mailModule = mailConfig.port?.let { portIt ->
|
val mailModule = mailConfig.port?.let { portIt ->
|
||||||
mailConfig.mailAddress?.let { mailAddressIt ->
|
mailConfig.mailAddress?.let { mailAddressIt ->
|
||||||
MailModule(
|
MailModule(
|
||||||
|
moduleName = "WhiteListGroup",
|
||||||
host = mailConfig.host.toString(),
|
host = mailConfig.host.toString(),
|
||||||
authToken = mailConfig.decryptedPassword.toString(),
|
authToken = mailConfig.decryptedPassword.toString(),
|
||||||
port = portIt,
|
port = portIt,
|
||||||
|
|
@ -29,7 +30,7 @@ fun main() = GlobalManager.runBlockingMain {
|
||||||
val expireHours = 24 // 有效期 24 小时
|
val expireHours = 24 // 有效期 24 小时
|
||||||
val expireTime = LocalDateTime.now().plusHours(expireHours.toLong())
|
val expireTime = LocalDateTime.now().plusHours(expireHours.toLong())
|
||||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
|
.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 "小明",
|
"player_name" to "小明",
|
||||||
"activation_code" to "ABC123",
|
"activation_code" to "ABC123",
|
||||||
"expire_time" to expireTime,
|
"expire_time" to expireTime,
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,12 @@
|
||||||
package top.r394realms.ltdmanagertest.misc
|
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 {
|
fun main() = GlobalManager.runBlockingMain {
|
||||||
// 创建模块实例
|
// 创建模块实例
|
||||||
val groupModule = GroupRequestHandlerModule(
|
val groupModule = GroupRequestHandlerModule(
|
||||||
|
moduleName = "WhiteListGroup",
|
||||||
client = GlobalManager.napCatClient,
|
client = GlobalManager.napCatClient,
|
||||||
targetGroupId = 538751386
|
targetGroupId = 538751386
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user