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

372 lines
14 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.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
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class McServerStatusModule(
private val groupMessagePollingModule: GroupMessagePollingModule,
private val selfId: Long,
private val selfNickName: String,
private val cooldownSeconds: Long = 60,
private val commands: List<String> = listOf("/mcs", "/s"),
private val presetServer: Map<Set<String>, String> = mapOf(
setOf("hp", "hypixel") to "mc.hypixel.net",
setOf("pm", "mineplex") to "play.mineplex.com"
)
) : BaseModule(), PersistentState<McServerStatusModule.CooldownState> {
private val presetServerByAlias: Map<String, String> by lazy {
presetServer.flatMap { (aliases, ip) ->
aliases.map { it.lowercase() to ip }
}.toMap()
}
fun getServerIp(alias: String): String? = presetServerByAlias[alias.lowercase()]
override val name: String = "McServerStatusModule"
private var scope: CoroutineScope? = null
private val stateFile = getStateFile("mc_server_status_state.json")
private val stateBackupFile = getStateFile("mc_server_status_state.json.bak")
private val fileLock = ReentrantLock()
private var cooldownState = loadState()
override fun getStateFile(): File = stateFile
override fun 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<GetFriendMsgHistoryEvent.SpecificMsg>) {
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<GetFriendMsgHistoryEvent.SpecificMsg>)
: List<GetFriendMsgHistoryEvent.SpecificMsg> {
val filtered = messages.asSequence()
.filter { msg ->
// 忽略自己消息
msg.userId != selfId &&
// 新消息判断
(msg.time > cooldownState.getLastTriggerTime(msg.userId) ||
(msg.time == cooldownState.getLastTriggerTime(msg.userId) &&
msg.realId > cooldownState.getLastTriggerRealId(msg.userId)))
}
.filter { msg ->
// 检查命令
msg.message.any { seg ->
seg.type == MessageType.Text &&
(
seg.data.text?.let { text -> commands.any { cmd -> text.startsWith(cmd) } } == true
)
}
}
.filter { runBlocking { handleCooldown(it) } } // 这里处理冷却
.toList()
return filtered
}
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 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(
MessageElement.reply(ID.long(realId), text),
ID.long(groupMessagePollingModule.targetGroupId)
)
client.sendUnit(request)
}
private suspend fun processCommand(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
// 找出文本内容
val text = msg.message
.firstOrNull { it.type == MessageType.Text }
?.data?.text
?.trim()
?: return
// 解析命令
val matchedCommand = commands.firstOrNull { text.startsWith(it) } ?: return
var address = text.removePrefix(matchedCommand).trim()
// 使用预设别名替换
presetServerByAlias[address.lowercase()]?.let { presetIp ->
address = presetIp
}
if (address.isEmpty()) {
sendFailedMessage(
napCatClient,
msg.userId,
msg.realId,
msg.time,
"❌ 请输入服务器地址,例如 /mcs n2.akiracloud.net:10599"
)
return
}
try {
val status = mcSrvStatusClient.getServerStatus(address) // 返回 McServerStatus
// 检查是否查询失败
if (!status.online) {
sendFailedMessage(
napCatClient, msg.userId, msg.realId, msg.time,
"❌ 查询失败,请检查服务器地址或服务器是否在线"
)
return
}
// 查询成功,发送状态消息
sendStatusForwardMessage(napCatClient, msg, address, 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: GetFriendMsgHistoryEvent.SpecificMsg,
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
fun getLastTriggerRealId(qq: Long): Long = map[qq]?.realId ?: -1
fun updateLastTrigger(qq: Long, realId: Long, time: Long = -1): CooldownState {
val old = map[qq]
val newTime = if (time != -1L) time else old?.time ?: -1
val newMap = map.toMutableMap().apply {
put(qq, TriggerDetail(realId, newTime, old?.lastCooldownRealId ?: -1))
}
return copy(map = newMap)
}
fun updateLastCooldownRealId(qq: Long, realId: Long): CooldownState {
val old = map[qq]
val newMap = map.toMutableMap().apply {
put(qq, TriggerDetail(
realId = old?.realId ?: -1,
time = old?.time ?: -1,
lastCooldownRealId = realId
))
}
return copy(map = newMap)
}
}
@Serializable
data class TriggerDetail(
val realId: Long,
val time: Long,
val lastCooldownRealId: Long = -1L
)
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)
}
}
}
}