strip: remove /backup /export /search /stats /fix-stale-tickets + analytics module

- delete handlers/analytics.js
- remove trackInteraction calls; replace trackError with logError().catch(() => {})
- remove 5 slash commands from register.js
- remove BACKUP_EXPORT_CHANNEL_ID from config + schema + .env.example
This commit is contained in:
2026-04-21 16:44:01 +00:00
parent fa7d4af132
commit 34dc55c20b
8 changed files with 13 additions and 409 deletions

View File

@@ -12,14 +12,13 @@ const {
} = require('discord.js');
const { mongoose } = require('../db-connection');
const { CONFIG } = require('../config');
const { getPriorityEmoji, replaceVariables, escapeRegex } = require('../utils');
const { getPriorityEmoji, replaceVariables } = require('../utils');
const { makeTicketName, resolveCreatorNickname, getOrCreateTicketCategory, createDiscordTicketAsThread, checkTicketCreationRateLimit } = require('../services/tickets');
const { sendTicketNotificationEmail } = require('../services/gmail');
const { getTicketActionRow } = require('../utils/ticketComponents');
const { getEmailRouting } = require('../services/guildSettings');
const { enqueueRename, enqueueMove, enqueueSend } = require('../services/channelQueue');
const { setNotifyDm } = require('../services/staffSettings');
const { trackInteraction, trackError, getAnalyticsSummary } = require('./analytics');
const { logTicketEvent, logSecurity, logError } = require('../services/debugLog');
const { handleSetupCommand } = require('./setup');
const { pendingCloses } = require('./pendingCloses');
@@ -249,7 +248,7 @@ async function handleCommand(interaction) {
components: [row]
});
} catch (err) {
trackError('email-routing-command', err, interaction);
logError('email-routing-command', err, interaction).catch(() => {});
await interaction.editReply('Failed to load routing options.').catch(() => {});
}
return;
@@ -631,7 +630,6 @@ async function handleCommand(interaction) {
// /response saved response tags (send, create, edit, delete, list)
if (interaction.commandName === 'response') {
trackInteraction('commands', 'response', interaction.user.tag);
const subcommand = interaction.options.getSubcommand();
try {
@@ -671,7 +669,7 @@ async function handleCommand(interaction) {
if (err.code === 11000 || err.message?.includes('duplicate')) {
await interaction.reply({ content: `❌ Tag "${name}" already exists.`, ephemeral: true });
} else {
trackError('tag-create', err, interaction);
logError('tag-create', err, interaction).catch(() => {});
await interaction.reply({ content: '❌ Failed to create tag.', ephemeral: true });
}
}
@@ -690,7 +688,7 @@ async function handleCommand(interaction) {
await interaction.reply({ content: `✅ Tag "${name}" updated successfully.`, ephemeral: true });
}
} catch (err) {
trackError('tag-edit', err, interaction);
logError('tag-edit', err, interaction).catch(() => {});
await interaction.reply({ content: '❌ Failed to edit tag.', ephemeral: true });
}
}
@@ -737,7 +735,7 @@ async function handleCommand(interaction) {
await interaction.editReply({ embeds: [embed] });
}
} catch (err) {
trackError('response-command', err, interaction);
logError('response-command', err, interaction).catch(() => {});
const errorMsg = '❌ An error occurred while processing the response command.';
if (interaction.deferred) {
await interaction.editReply(errorMsg);
@@ -898,201 +896,6 @@ async function handleCommand(interaction) {
}
}
// /backup export full ticket list to BACKUP_EXPORT_CHANNEL_ID
if (interaction.commandName === 'backup') {
trackInteraction('commands', 'backup', interaction.user.tag);
await interaction.deferReply({ ephemeral: true });
if (!CONFIG.BACKUP_EXPORT_CHANNEL_ID) {
return interaction.editReply('BACKUP_EXPORT_CHANNEL_ID is not set in .env.');
}
try {
// Stream every ticket through a Mongoose cursor to a tmp file so peak RSS
// stays bounded regardless of collection size; attach the file, then unlink.
const fs = require('fs');
const os = require('os');
const path = require('path');
const tmpName = `ticket-backup-${Date.now()}-${process.pid}.txt`;
const tmpPath = path.join(os.tmpdir(), tmpName);
const ws = fs.createWriteStream(tmpPath, { encoding: 'utf8' });
ws.write('# Ticket backup ' + new Date().toISOString() + '\n');
ws.write('ticketNumber\tstatus\tsenderEmail\tsubject\tcreatedAt\tclaimedBy\tpriority\tescalationTier\n');
let count = 0;
const cursor = Ticket.find().sort({ ticketNumber: 1 }).lean().cursor();
for await (const t of cursor) {
const created = t.createdAt ? new Date(t.createdAt).toISOString() : '';
ws.write([
t.ticketNumber,
t.status || '',
(t.senderEmail || '').replace(/\t/g, ' '),
(t.subject || '').replace(/\t/g, ' ').slice(0, 200),
created,
(t.claimedBy || '').replace(/\t/g, ' '),
t.priority || '',
t.escalationTier ?? ''
].join('\t') + '\n');
count++;
}
await new Promise((resolve, reject) => ws.end(err => err ? reject(err) : resolve()));
try {
const channel = await interaction.client.channels.fetch(CONFIG.BACKUP_EXPORT_CHANNEL_ID);
await enqueueSend(channel, {
content: `Ticket backup by ${interaction.user.tag} (${count} tickets)`,
files: [new AttachmentBuilder(tmpPath, { name: tmpName })]
});
await interaction.editReply(`Backup complete. ${count} tickets sent to the backup channel.`);
} finally {
fs.promises.unlink(tmpPath).catch(() => {});
}
} catch (err) {
trackError('backup-command', err, interaction);
await interaction.editReply('Failed to create backup: ' + (err.message || err));
}
}
// /export export tickets with optional status and limit to BACKUP_EXPORT_CHANNEL_ID
if (interaction.commandName === 'export') {
trackInteraction('commands', 'export', interaction.user.tag);
await interaction.deferReply({ ephemeral: true });
if (!CONFIG.BACKUP_EXPORT_CHANNEL_ID) {
return interaction.editReply('BACKUP_EXPORT_CHANNEL_ID is not set in .env.');
}
try {
const status = interaction.options.getString('status') || null;
const limit = interaction.options.getInteger('limit') || 500;
const filter = status ? { status } : {};
const tickets = await Ticket.find(filter).sort({ ticketNumber: -1 }).limit(limit).lean();
const lines = ['# Ticket export ' + new Date().toISOString() + (status ? ` (status=${status})` : '') + ` limit=${limit}`, 'ticketNumber\tstatus\tsenderEmail\tsubject\tcreatedAt\tclaimedBy\tpriority\tescalationTier'];
for (const t of tickets) {
const created = t.createdAt ? new Date(t.createdAt).toISOString() : '';
lines.push([t.ticketNumber, t.status || '', (t.senderEmail || '').replace(/\t/g, ' '), (t.subject || '').replace(/\t/g, ' ').slice(0, 200), created, (t.claimedBy || '').replace(/\t/g, ' '), t.priority || '', t.escalationTier ?? ''].join('\t'));
}
const buf = Buffer.from(lines.join('\n'), 'utf8');
const channel = await interaction.client.channels.fetch(CONFIG.BACKUP_EXPORT_CHANNEL_ID);
await enqueueSend(channel, {
content: `Ticket export by ${interaction.user.tag} (${tickets.length} tickets${status ? ` status=${status}` : ''})`,
files: [new AttachmentBuilder(buf, { name: `ticket-export-${Date.now()}.txt` })]
});
await interaction.editReply(`Export complete. ${tickets.length} tickets sent to the backup channel.`);
} catch (err) {
trackError('export-command', err, interaction);
await interaction.editReply('Failed to export: ' + (err.message || err));
}
}
// /search
if (interaction.commandName === 'search') {
trackInteraction('commands', 'search', interaction.user.tag);
await interaction.deferReply({ ephemeral: true });
try {
const query = interaction.options.getString('query');
const status = interaction.options.getString('status') || 'all';
const regex = new RegExp(escapeRegex(query), 'i');
const filter = {
$or: [
{ senderEmail: regex },
{ subject: regex }
]
};
const ticketNum = parseInt(query, 10);
if (!Number.isNaN(ticketNum) && String(ticketNum) === query.trim()) {
filter.$or.push({ ticketNumber: ticketNum });
}
if (status !== 'all') filter.status = status;
const results = await Ticket.find(filter).sort({ createdAt: -1 }).limit(10).lean();
if (!results || results.length === 0) {
return interaction.editReply('🔍 No tickets found matching your query.');
}
const embed = new EmbedBuilder()
.setTitle(`🔍 Search Results for "${query}"`)
.setDescription(`Found ${results.length} ticket(s)`)
.setColor(CONFIG.EMBED_COLOR_INFO);
for (const ticket of results.slice(0, 5)) {
const priorityEmoji = getPriorityEmoji(ticket.priority || 'normal');
const statusEmoji = ticket.status === 'open' ? '🟢' : '🔴';
embed.addFields({
name: `${priorityEmoji} Ticket #${ticket.ticketNumber} ${statusEmoji}`,
value: `**Subject:** ${ticket.subject || 'No subject'}\n**From:** ${ticket.senderEmail}\n**Status:** ${ticket.status}\n**Claimed:** ${ticket.claimedBy || 'Unclaimed'}`,
inline: false
});
}
if (results.length > 5) {
embed.setFooter({ text: `Showing 5 of ${results.length} results` });
}
await interaction.editReply({ embeds: [embed] });
} catch (err) {
trackError('search-command', err, interaction);
await interaction.editReply('❌ An error occurred while searching.');
}
}
// /fix-stale-tickets
if (interaction.commandName === 'fix-stale-tickets') {
if (interaction.user.id !== CONFIG.ADMIN_ID) {
return interaction.reply({ content: 'You do not have permission to run this command.', ephemeral: true });
}
await interaction.deferReply({ ephemeral: true });
try {
const result = await Ticket.updateMany(
{ status: 'open', lastActivity: null },
[{ $set: { lastActivity: '$createdAt' } }]
);
await interaction.editReply(`Fixed ${result.modifiedCount} ticket(s).`);
} catch (err) {
console.error('fix-stale-tickets:', err);
await interaction.editReply('❌ Failed to backfill tickets.').catch(() => {});
}
}
// /stats
if (interaction.commandName === 'stats') {
trackInteraction('commands', 'stats', interaction.user.tag);
await interaction.deferReply({ ephemeral: true });
try {
const summary = getAnalyticsSummary();
const ticketStats = await Ticket.aggregate([
{ $group: { _id: '$status', count: { $sum: 1 } } }
]);
const openCount = ticketStats.find(s => s._id === 'open')?.count || 0;
const closedCount = ticketStats.find(s => s._id === 'closed')?.count || 0;
const claimedCount = await Ticket.countDocuments({ status: 'open', claimedBy: { $ne: null } });
const embed = new EmbedBuilder()
.setTitle('📊 Bot Statistics & Analytics')
.setColor(CONFIG.EMBED_COLOR_INFO)
.addFields([
{ name: '⏱️ Uptime', value: summary.uptime, inline: true },
{ name: '💬 Total Interactions', value: summary.totalInteractions.toString(), inline: true },
{ name: '📈 Commands Used', value: summary.commandsUsed.toString(), inline: true },
{ name: '🎫 Open Tickets', value: openCount.toString(), inline: true },
{ name: '✅ Closed Tickets', value: closedCount.toString(), inline: true },
{ name: '📌 Claimed Tickets', value: (claimedCount || 0).toString(), inline: true },
{ name: '🔥 Most Used Command', value: summary.mostUsedCommand, inline: true },
{ name: '❌ Errors (Last Hour)', value: summary.errorsLastHour.toString(), inline: true },
{ name: '📉 Error Rate', value: summary.errorRate, inline: true },
{ name: '📋 Top Commands', value: summary.topCommands.join('\n') || 'None', inline: false }
])
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (err) {
trackError('stats-command', err, interaction);
await interaction.editReply('❌ An error occurred while fetching statistics.');
}
}
}
/**
@@ -1104,7 +907,6 @@ async function handleContextMenu(interaction) {
// Create Ticket From Message
if (interaction.isMessageContextMenuCommand() && interaction.commandName === 'Create Ticket From Message') {
trackInteraction('contextMenus', 'create-ticket-from-message', interaction.user.tag);
await interaction.deferReply({ ephemeral: true });
const rateLimit = checkTicketCreationRateLimit(interaction.user.id);
@@ -1215,14 +1017,13 @@ async function handleContextMenu(interaction) {
await interaction.editReply(`✅ Ticket created: ${channel}`);
} catch (err) {
trackError('create-ticket-from-message', err, interaction);
logError('create-ticket-from-message', err, interaction).catch(() => {});
await interaction.editReply('❌ Failed to create ticket from message.');
}
}
// View User Tickets
if (interaction.isUserContextMenuCommand() && interaction.commandName === 'View User Tickets') {
trackInteraction('contextMenus', 'view-user-tickets', interaction.user.tag);
await interaction.deferReply({ ephemeral: true });
try {
@@ -1258,7 +1059,7 @@ async function handleContextMenu(interaction) {
await interaction.editReply({ embeds: [embed] });
} catch (err) {
trackError('view-user-tickets', err, interaction);
logError('view-user-tickets', err, interaction).catch(() => {});
await interaction.editReply('❌ Failed to fetch user tickets.');
}
}