Files
broccolini-bot/services/chatAlertChecker.js
2026-04-18 11:10:41 +00:00

94 lines
3.7 KiB
JavaScript

/**
* 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');
const { enqueueSend } = require('./channelQueue');
// 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 enqueueSend(alertChan, { 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 enqueueSend(alertChan, { content, embeds: [embed] });
} catch (_) {}
}
}
}
}
module.exports = { initChatMonitoring, handleChatMessage, runChatAlertChecks };