419 lines
17 KiB
Kotlin
419 lines
17 KiB
Kotlin
package top.r3944realms.ltdmanager.module
|
||
|
||
import kotlinx.coroutines.*
|
||
import kotlinx.serialization.Serializable
|
||
import kotlinx.serialization.json.Json
|
||
import top.r3944realms.ltdmanager.mcserver.McServerStatus
|
||
import top.r3944realms.ltdmanager.module.common.CommandParser
|
||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownStateProvider
|
||
import top.r3944realms.ltdmanager.module.common.filter.TriggerMessageFilter
|
||
import top.r3944realms.ltdmanager.module.common.filter.type.CommandFilter
|
||
import top.r3944realms.ltdmanager.module.common.filter.type.CooldownFilter
|
||
import top.r3944realms.ltdmanager.module.common.filter.type.IgnoreSelfFilter
|
||
import top.r3944realms.ltdmanager.module.common.filter.type.NewMessageFilter
|
||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||
import top.r3944realms.ltdmanager.napcat.data.ID
|
||
import top.r3944realms.ltdmanager.napcat.data.MessageElement
|
||
import top.r3944realms.ltdmanager.napcat.data.MessageType
|
||
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
|
||
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
|
||
import java.util.concurrent.locks.ReentrantLock
|
||
import kotlin.concurrent.withLock
|
||
|
||
class McServerStatusModule(
|
||
moduleName: String,
|
||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||
private val selfId: Long,
|
||
private val selfNickName: String,
|
||
private val cooldownMillis: Long = 60_000L,
|
||
private val commands: List<String> = listOf("/mcs", "/s"),
|
||
private val presetServer: Map<Set<String>, String> = mapOf(
|
||
setOf("hp", "hypixel") to "mc.hypixel.net",
|
||
setOf("pm", "mineplex") to "play.mineplex.com"
|
||
)
|
||
) : BaseModule("McServerStatusModule", moduleName), PersistentState<McServerStatusModule.CooldownState> {
|
||
private val stateFile:File = getStateFileInternal("mc_server_status_state.json", name)
|
||
private val stateBackupFile:File = getStateFileInternal("mc_server_status_state.json.bak", name)
|
||
private val commandParser: CommandParser = CommandParser(commands)
|
||
|
||
private val cooldownManager by lazy {
|
||
CooldownManager(
|
||
cooldownMillis = cooldownMillis,
|
||
scope = CooldownScope.PerUser,
|
||
stateProvider = object : CooldownStateProvider<CooldownState> {
|
||
override fun load() = loadState()
|
||
override fun save(state: CooldownState) = saveState(state)
|
||
},
|
||
getLastTrigger = { state, qq ->
|
||
val detail = state.map[qq]
|
||
(detail?.time ?: -1L) to (detail?.lastCooldownRealId ?: -1L)
|
||
},
|
||
updateTrigger = { state, qq, realId, time ->
|
||
val id = requireNotNull(qq) { "userId required for per-user cooldown" }
|
||
state.updateLastTrigger(id, realId, time) }
|
||
,
|
||
updateCooldownRealId = { state, qq, realId ->
|
||
val id = requireNotNull(qq) { "userId required for per-user cooldown" }
|
||
state.updateLastCooldownRealId(id, realId)
|
||
},
|
||
groupId = groupMessagePollingModule.targetGroupId
|
||
)
|
||
}
|
||
private val triggerFilter = TriggerMessageFilter(
|
||
listOf(
|
||
IgnoreSelfFilter(selfId),
|
||
NewMessageFilter { qq ->
|
||
cooldownState.getLastTriggerTime(qq) to cooldownState.getLastTriggerRealId(qq)
|
||
},
|
||
CommandFilter(commandParser),
|
||
CooldownFilter(
|
||
cooldownManager = cooldownManager,
|
||
sendCooldown = { msg, remaining ->
|
||
sendCooldownMessage(napCatClient, msg.realId, "⏳ 查询过于频繁, $remaining 秒后执行查询,切勿重复发送")
|
||
}
|
||
)
|
||
)
|
||
)
|
||
|
||
private val presetServerByAlias: Map<String, String> by lazy {
|
||
presetServer.flatMap { (aliases, ip) ->
|
||
aliases.map { it.lowercase() to ip }
|
||
}.toMap()
|
||
}
|
||
fun getServerIp(alias: String): String? = presetServerByAlias[alias.lowercase()]
|
||
private var scope: CoroutineScope? = null
|
||
|
||
private val fileLock = ReentrantLock()
|
||
private var cooldownState = loadState()
|
||
|
||
override fun getStateFileInternal(): File = stateFile
|
||
override fun getState(): CooldownState = cooldownState
|
||
|
||
override fun onLoad() {
|
||
LoggerUtil.logger.info("[$name] 模块已装载,目标群组: ${groupMessagePollingModule.targetGroupId}")
|
||
|
||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||
scope!!.launch {
|
||
LoggerUtil.logger.info("[$name] 轮询协程启动")
|
||
groupMessagePollingModule.messagesFlow.collect { messages ->
|
||
if (loaded) handleMessages(messages)
|
||
}
|
||
}
|
||
}
|
||
|
||
override suspend fun onUnload() {
|
||
saveState(cooldownState)
|
||
scope?.cancel()
|
||
LoggerUtil.logger.info("[$name] 模块已卸载完成")
|
||
}
|
||
|
||
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
|
||
if (messages.isEmpty()) return
|
||
val triggerMsgs = filterTriggerMessages(messages)
|
||
if (triggerMsgs.isEmpty()) return
|
||
|
||
try {
|
||
triggerMsgs.forEach {
|
||
processCommand(it)
|
||
}
|
||
} catch (e: Exception) {
|
||
sendFailedMessage(napCatClient, text = "系统错误,请联系管理员: $e")
|
||
} finally {
|
||
saveState(cooldownState)
|
||
}
|
||
}
|
||
|
||
private suspend fun filterTriggerMessages(
|
||
messages: List<MsgHistorySpecificMsg>
|
||
): List<MsgHistorySpecificMsg> = triggerFilter.filter(messages)
|
||
|
||
private suspend fun sendFailedMessage(
|
||
client: NapCatClient,
|
||
qq: Long? = null,
|
||
realId: Long? = null,
|
||
time: Long? = null,
|
||
text: String = "失败消息"
|
||
) {
|
||
LoggerUtil.logger.info("[$name] 发送失败消息: realId=$realId, text=$text")
|
||
if (realId != null && qq != null && time != null) {
|
||
val request = SendGroupMsgRequest(
|
||
MessageElement.reply(ID.long(realId), text),
|
||
ID.long(groupMessagePollingModule.targetGroupId)
|
||
)
|
||
client.sendUnit(request)
|
||
LoggerUtil.logger.info("[$name] 已发送 失败消息")
|
||
|
||
// 更新触发的最大 realId
|
||
cooldownState = cooldownState.updateLastTrigger(qq, realId, time)
|
||
} else {
|
||
val request = SendGroupMsgRequest(
|
||
listOf(MessageElement.text(text)),
|
||
ID.long(groupMessagePollingModule.targetGroupId)
|
||
)
|
||
client.sendUnit(request)
|
||
LoggerUtil.logger.info("[$name] 已发送 失败消息[无指定对象]")
|
||
}
|
||
}
|
||
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, text: String) {
|
||
val request = SendGroupMsgRequest(
|
||
MessageElement.reply(ID.long(realId), text),
|
||
ID.long(groupMessagePollingModule.targetGroupId)
|
||
)
|
||
client.sendUnit(request)
|
||
}
|
||
|
||
|
||
|
||
private suspend fun processCommand(msg: MsgHistorySpecificMsg) {
|
||
// 找出文本内容
|
||
val text = msg.message
|
||
.firstOrNull { it.type == MessageType.Text }
|
||
?.data?.text
|
||
?.trim()
|
||
?: return
|
||
|
||
// 使用命令解析器解析命令
|
||
val parsedCommand = commandParser.parseCommand(text) ?: return
|
||
val (_, address) = parsedCommand
|
||
|
||
// 使用预设别名替换
|
||
val finalAddress = if (address.isNotEmpty()) {
|
||
presetServerByAlias[address.lowercase()] ?: address
|
||
} else {
|
||
""
|
||
}
|
||
|
||
if (finalAddress.isEmpty()) {
|
||
sendFailedMessage(
|
||
napCatClient,
|
||
msg.userId,
|
||
msg.realId,
|
||
msg.time,
|
||
"❌ 请输入服务器地址,例如 /mcs n2.akiracloud.net:10599"
|
||
)
|
||
return
|
||
}
|
||
|
||
try {
|
||
val status = mcSrvStatusClient.getServerStatus(finalAddress)
|
||
|
||
if (!status.online) {
|
||
sendFailedMessage(
|
||
napCatClient, msg.userId, msg.realId, msg.time,
|
||
"❌ 查询失败,请检查服务器地址或服务器是否在线"
|
||
)
|
||
return
|
||
}
|
||
|
||
sendStatusForwardMessage(napCatClient, msg, finalAddress, status, msg.realId, msg.time)
|
||
} catch (e: Exception) {
|
||
LoggerUtil.logger.error("查询服务器状态失败: $address", e)
|
||
sendFailedMessage(
|
||
napCatClient,
|
||
msg.userId,
|
||
msg.realId,
|
||
msg.time,
|
||
"❌ 查询失败,请检查服务器地址或服务器是否在线"
|
||
)
|
||
}
|
||
}
|
||
|
||
// ---------------- 转发消息封装 ----------------
|
||
private suspend fun sendStatusForwardMessage(
|
||
client: NapCatClient,
|
||
msg: MsgHistorySpecificMsg,
|
||
address: String,
|
||
status: McServerStatus,
|
||
realId: Long,
|
||
time: Long
|
||
) {
|
||
LoggerUtil.logger.info("[$name] 发送服务器状态转发消息: realId=$realId, address=$address, online=${status.online}")
|
||
|
||
val messages = mutableListOf<SendForwardMsgRequest.Message>()
|
||
|
||
// ① 服务器基本信息 + MOTD
|
||
val motdText = status.motd?.clean?.joinToString("\n") ?: "无 MOTD"
|
||
val basicInfo = buildString {
|
||
appendLine("🌐 服务器: $address")
|
||
appendLine("─".repeat(25))
|
||
appendLine("MOTD:\n$motdText")
|
||
}
|
||
messages.add(SendForwardMsgRequest.Message(SendForwardMsgRequest.PurpleData(basicInfo), MessageType.Text))
|
||
|
||
// ② 玩家列表
|
||
val playerList = status.players?.list?.joinToString("\n") { it.name } ?: "无"
|
||
val playersInfo = buildString {
|
||
appendLine("📊 在线: ${status.players?.online ?: 0}/${status.players?.max ?: 0}")
|
||
appendLine("👥 玩家:\n$playerList")
|
||
}
|
||
messages.add(SendForwardMsgRequest.Message(SendForwardMsgRequest.PurpleData(playersInfo), MessageType.Text))
|
||
|
||
// ③ 版本 + 状态
|
||
val versionStatus = buildString {
|
||
appendLine("🎮 版本: ${status.version ?: "未知"}")
|
||
appendLine("✅ 状态: ${if (status.online) "在线" else "离线"}")
|
||
status.software?.let { appendLine("💻 软件: $it") }
|
||
}
|
||
messages.add(SendForwardMsgRequest.Message(SendForwardMsgRequest.PurpleData(versionStatus), MessageType.Text))
|
||
|
||
// ④ 摘要信息
|
||
val summaryText = buildString {
|
||
appendLine("📌 查询摘要")
|
||
appendLine("─".repeat(20))
|
||
appendLine("服务器: $address")
|
||
appendLine("在线玩家: ${status.players?.online ?: 0}/${status.players?.max ?: 0}")
|
||
appendLine("状态: ${if (status.online) "在线" else "离线"}")
|
||
appendLine("🕐 ${getCurrentTime()}")
|
||
appendLine("🤖 由 $selfNickName 提供")
|
||
}
|
||
messages.add(SendForwardMsgRequest.Message(SendForwardMsgRequest.PurpleData(summaryText), MessageType.Text))
|
||
|
||
// 封装 Forward 消息
|
||
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("点击查看服务器状态与玩家列表"),
|
||
SendForwardMsgRequest.ForwardModelNews("在线 ${status.players?.online ?: 0} / ${status.players?.max ?: 0}"),
|
||
SendForwardMsgRequest.ForwardModelNews("更新时间: ${getCurrentTime()}")
|
||
),
|
||
prompt = "服务器状态查询结果",
|
||
source = "🎮 服务器状态",
|
||
summary = "在线 ${status.players?.online ?: 0} / ${status.players?.max ?: 0} 人"
|
||
)
|
||
|
||
client.sendUnit(request)
|
||
LoggerUtil.logger.info("[$name] 已发送服务器状态转发消息")
|
||
|
||
// 更新冷却状态
|
||
cooldownState = cooldownState.updateLastTrigger(msg.userId, realId, time)
|
||
}
|
||
|
||
|
||
|
||
// 时间格式化
|
||
private fun getCurrentTime(): String {
|
||
return java.time.LocalDateTime.now()
|
||
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
||
}
|
||
|
||
// ---------------- 状态持久化 ----------------
|
||
@Serializable
|
||
data class CooldownState(
|
||
val map: Map<Long, TriggerDetail> = emptyMap()
|
||
) {
|
||
// 获取上次处理时间
|
||
fun getLastTriggerTime(qq: Long): Long = map[qq]?.time ?: -1
|
||
|
||
// 获取上次处理消息ID
|
||
fun getLastTriggerRealId(qq: Long): Long = map[qq]?.realId ?: -1
|
||
|
||
// 获取上次冷却消息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 newMap = map.toMutableMap().apply {
|
||
put(qq, TriggerDetail(
|
||
realId = realId, // 当前允许处理消息ID
|
||
time = time, // 当前允许处理消息时间
|
||
lastCooldownRealId = old?.lastCooldownRealId ?: -1 // 保留冷却中记录的消息ID
|
||
))
|
||
}
|
||
return copy(map = newMap)
|
||
}
|
||
|
||
// 冷却中消息 → 只更新 lastCooldownRealId,保留 time 和 realId
|
||
fun updateLastCooldownRealId(qq: Long, realId: Long): CooldownState {
|
||
val old = map[qq]
|
||
val newMap = map.toMutableMap().apply {
|
||
put(qq, TriggerDetail(
|
||
realId = old?.realId ?: -1, // 保持上次允许处理的消息ID
|
||
time = old?.time ?: -1, // 保持上次允许处理的时间
|
||
lastCooldownRealId = realId // 更新当前冷却拒绝的消息ID
|
||
))
|
||
}
|
||
return copy(map = newMap)
|
||
}
|
||
}
|
||
|
||
@Serializable
|
||
data class TriggerDetail(
|
||
val realId: Long, // 上次允许处理消息ID
|
||
val time: Long, // 上次允许处理消息时间(秒)
|
||
val lastCooldownRealId: Long = -1 // 上次被冷却拒绝的消息ID
|
||
)
|
||
|
||
override fun loadState(): CooldownState {
|
||
return try {
|
||
val fileToRead = when {
|
||
stateFile.exists() -> stateFile
|
||
stateBackupFile.exists() -> stateBackupFile
|
||
else -> null
|
||
}
|
||
if (fileToRead == null) return CooldownState()
|
||
val content = fileToRead.readText()
|
||
Json.decodeFromString(CooldownState.serializer(), content)
|
||
} catch (e: Exception) {
|
||
LoggerUtil.logger.warn("[$name] 状态恢复失败,使用默认值", e)
|
||
CooldownState()
|
||
}
|
||
}
|
||
|
||
override fun saveState(state: CooldownState) {
|
||
fileLock.withLock {
|
||
try {
|
||
val json = Json.encodeToString(CooldownState.serializer(), state)
|
||
if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true)
|
||
stateFile.writeText(json)
|
||
} catch (e: Exception) {
|
||
LoggerUtil.logger.error("[$name] 保存状态失败", e)
|
||
}
|
||
}
|
||
}
|
||
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(" - 查询结果会以转发消息形式发送到群组")
|
||
}
|
||
}
|