huge changes
This commit is contained in:
191
services/surgeChecker.js
Normal file
191
services/surgeChecker.js
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Surge detection — checks for critical ticket volume/staffing conditions
|
||||
* and pings ALL_STAFF_CHANNEL_ID with role mention.
|
||||
*/
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const { CONFIG } = require('../config');
|
||||
const { mongoose } = require('../db-connection');
|
||||
const { setCooldown, isOnCooldown, isStaffRecentlyActive } = require('./patternStore');
|
||||
const { getStaffAvailability, isAnyStaffAvailable } = require('./staffPresence');
|
||||
|
||||
const Ticket = mongoose.model('Ticket');
|
||||
|
||||
async function pingStaff(client, message, embedFields) {
|
||||
const channelId = CONFIG.ALL_STAFF_CHANNEL_ID;
|
||||
if (!channelId || !client) return;
|
||||
try {
|
||||
const channel = await client.channels.fetch(channelId);
|
||||
if (!channel) return;
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Staff Alert')
|
||||
.setDescription(message)
|
||||
.setColor(0xFF4400)
|
||||
.setTimestamp();
|
||||
if (embedFields.length > 0) {
|
||||
embed.addFields(embedFields.map(f => ({
|
||||
name: f.name,
|
||||
value: String(f.value).slice(0, 1024),
|
||||
inline: f.inline ?? true
|
||||
})));
|
||||
}
|
||||
const content = CONFIG.SURGE_ROLE_ID ? `<@&${CONFIG.SURGE_ROLE_ID}>` : undefined;
|
||||
await channel.send({ content, embeds: [embed] });
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function checkTicketSurge(client) {
|
||||
if (isOnCooldown('surge:tickets', CONFIG.SURGE_COOLDOWN_MINUTES)) return;
|
||||
const since = new Date(Date.now() - CONFIG.SURGE_TICKET_WINDOW_MINUTES * 60000);
|
||||
const count = await Ticket.countDocuments({ createdAt: { $gte: since } });
|
||||
if (count >= CONFIG.SURGE_TICKET_COUNT) {
|
||||
setCooldown('surge:tickets');
|
||||
await pingStaff(client,
|
||||
`${count} tickets created in the past ${CONFIG.SURGE_TICKET_WINDOW_MINUTES} minutes.`,
|
||||
[{ name: 'Action needed', value: 'Check open tickets and claim.', inline: false }]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkGameSurge(client) {
|
||||
if (isOnCooldown('surge:game', CONFIG.SURGE_COOLDOWN_MINUTES)) return;
|
||||
const since = new Date(Date.now() - CONFIG.SURGE_GAME_TICKET_WINDOW_MINUTES * 60000);
|
||||
const gameCounts = await Ticket.aggregate([
|
||||
{ $match: { createdAt: { $gte: since }, game: { $ne: null } } },
|
||||
{ $group: { _id: '$game', count: { $sum: 1 } } },
|
||||
{ $match: { count: { $gte: CONFIG.SURGE_GAME_TICKET_COUNT } } },
|
||||
{ $sort: { count: -1 } }
|
||||
]);
|
||||
if (gameCounts.length > 0) {
|
||||
setCooldown('surge:game');
|
||||
const fields = gameCounts.map(g => ({
|
||||
name: g._id,
|
||||
value: `${g.count} tickets in ${CONFIG.SURGE_GAME_TICKET_WINDOW_MINUTES} min`,
|
||||
inline: true
|
||||
}));
|
||||
await pingStaff(client, 'Game ticket surge detected.', fields);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkStaleSurge(client) {
|
||||
if (isOnCooldown('surge:stale', CONFIG.SURGE_COOLDOWN_MINUTES)) return;
|
||||
const cutoff = new Date(Date.now() - CONFIG.SURGE_STALE_HOURS * 3600000);
|
||||
const count = await Ticket.countDocuments({
|
||||
status: 'open',
|
||||
lastActivity: { $lte: cutoff }
|
||||
});
|
||||
if (count >= CONFIG.SURGE_STALE_COUNT) {
|
||||
setCooldown('surge:stale');
|
||||
await pingStaff(client,
|
||||
`${count} tickets have had no activity in the past ${CONFIG.SURGE_STALE_HOURS} hours.`,
|
||||
[{ name: 'Action needed', value: 'Review and respond to stale tickets.', inline: false }]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNeedsResponseSurge(client) {
|
||||
if (isOnCooldown('surge:needs_response', CONFIG.SURGE_COOLDOWN_MINUTES)) return;
|
||||
const cutoff = new Date(Date.now() - CONFIG.SURGE_NEEDS_RESPONSE_HOURS * 3600000);
|
||||
const count = await Ticket.countDocuments({
|
||||
status: 'open',
|
||||
lastMessageAuthorIsStaff: false,
|
||||
lastActivity: { $lte: cutoff }
|
||||
});
|
||||
if (count >= CONFIG.SURGE_NEEDS_RESPONSE_COUNT) {
|
||||
setCooldown('surge:needs_response');
|
||||
await pingStaff(client,
|
||||
`${count} tickets are waiting on a staff response for over ${CONFIG.SURGE_NEEDS_RESPONSE_HOURS} hour(s).`,
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUnclaimedSurge(client) {
|
||||
if (isOnCooldown('surge:unclaimed', CONFIG.SURGE_COOLDOWN_MINUTES)) return;
|
||||
const cutoff = new Date(Date.now() - CONFIG.SURGE_UNCLAIMED_MINUTES * 60000);
|
||||
const count = await Ticket.countDocuments({
|
||||
status: 'open',
|
||||
claimedBy: null,
|
||||
createdAt: { $lte: cutoff }
|
||||
});
|
||||
if (count >= CONFIG.SURGE_UNCLAIMED_COUNT) {
|
||||
setCooldown('surge:unclaimed');
|
||||
await pingStaff(client,
|
||||
`${count} tickets have been unclaimed for over ${CONFIG.SURGE_UNCLAIMED_MINUTES} minutes.`,
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkTier3UnclaimedSurge(client) {
|
||||
if (isOnCooldown('surge:tier3_unclaimed', 30)) return;
|
||||
const cutoff = new Date(Date.now() - CONFIG.SURGE_TIER3_UNCLAIMED_MINUTES * 60000);
|
||||
const tickets = await Ticket.find({
|
||||
status: 'open',
|
||||
escalationTier: 2,
|
||||
claimedBy: null,
|
||||
createdAt: { $lte: cutoff }
|
||||
}).lean();
|
||||
if (tickets.length > 0) {
|
||||
setCooldown('surge:tier3_unclaimed');
|
||||
await pingStaff(client,
|
||||
`${tickets.length} Tier 3 ticket(s) unclaimed for over ${CONFIG.SURGE_TIER3_UNCLAIMED_MINUTES} minutes.`,
|
||||
tickets.map(t => ({ name: t.subject || 'No subject', value: `<#${t.discordThreadId}>`, inline: true }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkZeroStaffSurge(client) {
|
||||
if (isOnCooldown('surge:no_staff', CONFIG.SURGE_NO_STAFF_COOLDOWN_MINUTES)) return;
|
||||
if (!CONFIG.STAFF_IDS.length) return;
|
||||
|
||||
const openCount = await Ticket.countDocuments({ status: 'open' });
|
||||
if (openCount < CONFIG.SURGE_NO_STAFF_OPEN_TICKET_THRESHOLD) return;
|
||||
|
||||
const guild = client.guilds.cache.get(CONFIG.DISCORD_GUILD_ID);
|
||||
if (!guild) return;
|
||||
|
||||
const { available, source } = isAnyStaffAvailable(guild);
|
||||
|
||||
let noStaff = false;
|
||||
let detailLine = '';
|
||||
const { online, dnd, offline } = getStaffAvailability(guild);
|
||||
|
||||
if (source === 'unknown') {
|
||||
const recentlyActive = CONFIG.STAFF_IDS.filter(id => isStaffRecentlyActive(id, 60));
|
||||
if (recentlyActive.length === 0) {
|
||||
noStaff = true;
|
||||
detailLine = 'No staff active in the last 60 minutes (presence intent unavailable, using message activity fallback).';
|
||||
}
|
||||
} else if (!available) {
|
||||
noStaff = true;
|
||||
const dndNote = dnd.length > 0 ? ` (${dnd.length} on DND)` : '';
|
||||
detailLine = `${offline.length} staff offline/invisible${dndNote}. ${online.length} online.`;
|
||||
}
|
||||
|
||||
if (!noStaff) return;
|
||||
|
||||
setCooldown('surge:no_staff');
|
||||
|
||||
const fields = [
|
||||
{ name: 'Open tickets', value: String(openCount), inline: true },
|
||||
{ name: 'Detection method', value: source === 'unknown' ? 'Message activity' : 'Presence', inline: true },
|
||||
{ name: source === 'unknown' ? 'Note' : 'Staff status', value: detailLine, inline: false }
|
||||
];
|
||||
|
||||
await pingStaff(client,
|
||||
`${openCount} open ticket(s) with no staff available to respond.`,
|
||||
fields
|
||||
);
|
||||
}
|
||||
|
||||
async function runSurgeChecks(client) {
|
||||
try { await checkTicketSurge(client); } catch (e) { console.error('checkTicketSurge:', e); }
|
||||
try { await checkGameSurge(client); } catch (e) { console.error('checkGameSurge:', e); }
|
||||
try { await checkStaleSurge(client); } catch (e) { console.error('checkStaleSurge:', e); }
|
||||
try { await checkNeedsResponseSurge(client); } catch (e) { console.error('checkNeedsResponseSurge:', e); }
|
||||
try { await checkUnclaimedSurge(client); } catch (e) { console.error('checkUnclaimedSurge:', e); }
|
||||
try { await checkTier3UnclaimedSurge(client); } catch (e) { console.error('checkTier3UnclaimedSurge:', e); }
|
||||
try { await checkZeroStaffSurge(client); } catch (e) { console.error('checkZeroStaffSurge:', e); }
|
||||
}
|
||||
|
||||
module.exports = { runSurgeChecks };
|
||||
Reference in New Issue
Block a user