/** * Chat monitoring — tracks unresponded messages in configured channels * and alerts staff when thresholds are crossed. */ const { EmbedBuilder } = require('discord.js'); const { CONFIG, parseThresholdString } = require('../config'); const { shouldFireCooldownEscalating, clearEscalating } = require('./patternStore'); // channelId → { lastStaffMessageAt, unrespondedCount, lastAlertAt } const chatState = new Map(); const chatMessageThresholdsMs = (CONFIG.NOTIFICATION_THRESHOLDS?.chat_messages || []) .map(parseThresholdString) .filter(n => Number.isFinite(n) && n > 0); const chatTimeThresholdsMs = (CONFIG.NOTIFICATION_THRESHOLDS?.chat_time || []) .map(parseThresholdString) .filter(n => Number.isFinite(n) && n > 0); function initChatMonitoring(client) { for (const channelId of CONFIG.CHAT_ALERT_CHANNEL_IDS) { chatState.set(channelId, { lastStaffMessageAt: new Date(), unrespondedCount: 0, lastAlertAt: null }); } } function isStaff(member) { if (!member?.roles?.cache) return false; if (CONFIG.ROLE_ID_TO_PING && member.roles.cache.has(CONFIG.ROLE_ID_TO_PING)) return true; const additional = CONFIG.ADDITIONAL_STAFF_ROLES || []; return additional.some(roleId => member.roles.cache.has(roleId)); } async function handleChatMessage(msg, client) { if (msg.author.bot) return; if (!chatState.has(msg.channel.id)) return; const state = chatState.get(msg.channel.id); if (isStaff(msg.member)) { state.lastStaffMessageAt = new Date(); state.unrespondedCount = 0; clearEscalating(`chat:messages:${msg.channel.id}`); clearEscalating(`chat:time:${msg.channel.id}`); } else { state.unrespondedCount++; } } async function runChatAlertChecks(client) { const alertChannelId = CONFIG.ALL_STAFF_CHAT_ALERT_CHANNEL_ID; if (!alertChannelId || !client) return; for (const [channelId, state] of chatState) { // Message count threshold if (state.unrespondedCount >= CONFIG.CHAT_ALERT_MESSAGE_COUNT) { const cooldownKey = `chat:messages:${channelId}`; if (shouldFireCooldownEscalating(cooldownKey, chatMessageThresholdsMs) !== null) { const embed = new EmbedBuilder() .setTitle('Chat needs attention') .setDescription(`<#${channelId}> has ${state.unrespondedCount} unresponded messages.`) .setColor(0xFF8800) .setTimestamp(); try { const alertChan = await client.channels.fetch(alertChannelId); const content = CONFIG.SURGE_ROLE_ID ? `<@&${CONFIG.SURGE_ROLE_ID}>` : undefined; if (alertChan) await alertChan.send({ content, embeds: [embed] }); } catch (_) {} } } // Time threshold const hoursSinceStaff = (Date.now() - state.lastStaffMessageAt.getTime()) / 3600000; if (hoursSinceStaff >= CONFIG.CHAT_ALERT_HOURS_WITHOUT_RESPONSE && state.unrespondedCount > 0) { const cooldownKey = `chat:time:${channelId}`; if (shouldFireCooldownEscalating(cooldownKey, chatTimeThresholdsMs) !== null) { const embed = new EmbedBuilder() .setTitle('Chat without staff response') .setDescription(`<#${channelId}> has had no staff response for ${Math.floor(hoursSinceStaff)} hour(s) with ${state.unrespondedCount} pending message(s).`) .setColor(0xFF8800) .setTimestamp(); try { const alertChan = await client.channels.fetch(alertChannelId); const content = CONFIG.SURGE_ROLE_ID ? `<@&${CONFIG.SURGE_ROLE_ID}>` : undefined; if (alertChan) await alertChan.send({ content, embeds: [embed] }); } catch (_) {} } } } } module.exports = { initChatMonitoring, handleChatMessage, runChatAlertChecks };