Remove dead/stale code, dedup close+escalation paths
Dead/stale removals (grep-confirmed no consumers):
- config: drop 9 unread CONFIG keys (ROLE_TO_PING_ID, SIGNATURE,
REMINDER_*, RENAME_LOG_CHANNEL_ID, SETTINGS_*); remove their
ALLOWED_CONFIG_KEYS entries and the orphaned settings-site UI fields
- configSchema: delete unreachable json/string_or_json validators
- models: drop unused ticketTag field
- gmail-poll: remove unused isPollSuspended export
- utils: remove dead htmlToTextWithBlocks/decodeHtmlEntities/BLOCK_TAG_REGEX
- internalApi: remove router._allowedKeys (test it served is gone)
- discord client: drop unused GuildPresences privileged intent
- broccolini-discord: remove dormant /api 503 gate (no /api routes)
Fixes:
- context-menu ticket create now uses makeTicketName('unclaimed', ...)
instead of the contract-violating ticket-<n> name
- drop write-only pending.userId from both close paths
Dedup / simplify:
- new services/transcript.js shares the transcript text/date/header
builders between the button and force-close paths (had drifted)
- resolveEscalationCategoryId() replaces 3 copies of the category logic
- ticketChannelOverwrites() shares the create-permission array between
the two interactive ticket-create paths
- finalizeBody() shares the email-cleanup tail in parseGmailMessage
- getTicketActionRow drops its never-passed options arg;
sendTicketNotificationEmail drops its always-null subjectLine arg
- hoist invariant guild lookup out of the auto-close/unclaim loops
- drop redundant lastActivity write (and now-dead updateTicketActivity)
- /help lists all current commands and the right-click apps
This commit is contained in:
@@ -16,7 +16,6 @@ const {
|
||||
AttachmentBuilder,
|
||||
EmbedBuilder,
|
||||
MessageFlags,
|
||||
PermissionFlagsBits,
|
||||
ModalBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle
|
||||
@@ -25,10 +24,11 @@ const { mongoose } = require('../db-connection');
|
||||
const { CONFIG } = require('../config');
|
||||
const { makeTicketName, resolveCreatorNickname, getOrCreateTicketCategory, cleanupEmptyOverflowCategory, checkTicketCreationRateLimit, toDiscordSafeName } = require('../services/tickets');
|
||||
const { sendTicketClosedEmail } = require('../services/gmail');
|
||||
const { getTicketActionRow } = require('../utils/ticketComponents');
|
||||
const { getTicketActionRow, ticketChannelOverwrites } = require('../utils/ticketComponents');
|
||||
const { buildTranscriptText, formatDateForTranscript, renderTranscriptHeader } = require('../services/transcript');
|
||||
const { sanitizeEmbedText, truncateEmbedDescription } = require('../utils');
|
||||
const { enqueueRename, enqueueSend } = require('../services/channelQueue');
|
||||
const { runEscalation, runDeescalation } = require('./commands');
|
||||
const { runEscalation, runDeescalation, resolveEscalationCategoryId } = require('./commands');
|
||||
const { pendingCloses } = require('./pendingCloses');
|
||||
const { addMemberToStaffThread, createStaffThread } = require('../services/staffThread');
|
||||
const { pinMessage } = require('../services/pinMessage');
|
||||
@@ -309,7 +309,7 @@ async function handleConfirmCloseRequest(interaction, ticket) {
|
||||
await runFinalClose(interaction, freshTicket, effectiveSendEmail);
|
||||
}, timerSeconds * 1000));
|
||||
|
||||
pendingCloses.set(channelId, { timeout: timerId, userId: interaction.user.id, username: userTag, sendEmail });
|
||||
pendingCloses.set(channelId, { timeout: timerId, username: userTag, sendEmail });
|
||||
}
|
||||
|
||||
async function handleCancelCloseRequest(interaction) {
|
||||
@@ -358,10 +358,7 @@ async function handleEscalateButton(interaction, ticket) {
|
||||
return interaction.reply({ content: `This ticket is already at tier ${tier + 1}.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
const isDiscordTicket = ticket.gmailThreadId.startsWith('discord-');
|
||||
const categoryId = tier === 1
|
||||
? (isDiscordTicket ? CONFIG.DISCORD_ESCALATED2_CHANNEL_ID : CONFIG.EMAIL_ESCALATED2_CHANNEL_ID)
|
||||
: (isDiscordTicket ? CONFIG.DISCORD_ESCALATED3_CHANNEL_ID : CONFIG.EMAIL_ESCALATED3_CHANNEL_ID);
|
||||
const categoryId = resolveEscalationCategoryId(ticket, tier);
|
||||
|
||||
if (!categoryId && !interaction.channel.isThread()) {
|
||||
return interaction.reply({
|
||||
@@ -474,33 +471,6 @@ async function runFinalClose(interaction, ticket, sendEmail = true) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Render the last 100 messages of a channel as a plaintext transcript. */
|
||||
async function buildTranscriptText(channel, ticket) {
|
||||
const messages = await channel.messages.fetch({ limit: 100 });
|
||||
return `TRANSCRIPT: ${ticket.subject}\nUser: ${ticket.senderEmail}\n---\n` +
|
||||
messages
|
||||
.reverse()
|
||||
.map(m => `[${m.createdAt.toLocaleString()}] ${m.author.tag}: ${m.cleanContent}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function formatDateForTranscript(d) {
|
||||
return new Date(d).toLocaleString('en-US', {
|
||||
month: '2-digit', day: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hour12: true, timeZoneName: 'short'
|
||||
});
|
||||
}
|
||||
|
||||
function renderTranscriptHeader(channelName, senderEmail, openedStr, closedStr) {
|
||||
return CONFIG.DISCORD_TRANSCRIPT_MESSAGE
|
||||
.replace(/\{channel_name\}/g, channelName)
|
||||
.replace(/\{email\}/g, senderEmail || '')
|
||||
.replace(/\{date_opened\}/g, openedStr)
|
||||
.replace(/\{date_closed\}/g, closedStr)
|
||||
+ `\n\nDate Opened: ${openedStr}\nDate Closed: ${closedStr}`;
|
||||
}
|
||||
|
||||
async function dmTranscriptToCreator(client, ticket, channelName, transcriptText, openedStr, closedStr) {
|
||||
// Prefer ticket.creatorId (stored on creation). Fall back to legacy parsing for
|
||||
// pre-creatorId modal tickets only — split-pop returns the wrong value for
|
||||
@@ -601,17 +571,7 @@ async function handleTicketModal(interaction) {
|
||||
name: unclaimedName,
|
||||
type: ChannelType.GuildText,
|
||||
parent: parentCategoryIdForTicket,
|
||||
permissionOverwrites: [
|
||||
{ id: guild.id, deny: [PermissionFlagsBits.ViewChannel] },
|
||||
{
|
||||
id: interaction.user.id,
|
||||
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory]
|
||||
},
|
||||
{
|
||||
id: CONFIG.ROLE_ID_TO_PING,
|
||||
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory]
|
||||
}
|
||||
]
|
||||
permissionOverwrites: ticketChannelOverwrites(guild, interaction.user.id)
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('guild.channels.create (ticket modal):', err);
|
||||
|
||||
@@ -14,6 +14,7 @@ const { enqueueSend } = require('../../services/channelQueue');
|
||||
const { logTicketEvent } = require('../../services/debugLog');
|
||||
const { pendingCloses } = require('../pendingCloses');
|
||||
const { findTicketForChannel } = require('../sharedHelpers');
|
||||
const { buildTranscriptText, formatDateForTranscript, renderTranscriptHeader } = require('../../services/transcript');
|
||||
|
||||
const Ticket = mongoose.model('Ticket');
|
||||
|
||||
@@ -56,7 +57,7 @@ async function handleForceClose(interaction) {
|
||||
const channelRef = interaction.channel;
|
||||
const clientRef = interaction.client;
|
||||
const timerId = setTimeout(() => finalizeForceClose(channelRef, clientRef), timerSeconds * 1000);
|
||||
pendingCloses.set(channelRef.id, { timeout: timerId, userId: interaction.user.id, username: interaction.user.tag });
|
||||
pendingCloses.set(channelRef.id, { timeout: timerId, username: interaction.user.tag });
|
||||
}
|
||||
|
||||
/** Performs the actual force-close work after the countdown elapses. */
|
||||
@@ -92,14 +93,7 @@ async function finalizeForceClose(channelRef, clientRef) {
|
||||
async function postTranscript(channelRef, clientRef, freshTicket) {
|
||||
await enqueueSend(channelRef, CONFIG.DISCORD_CLOSE_MESSAGE);
|
||||
|
||||
const messages = await channelRef.messages.fetch({ limit: 100 });
|
||||
const log =
|
||||
`TRANSCRIPT: ${freshTicket.subject}\nUser: ${freshTicket.senderEmail}\n---\n` +
|
||||
messages
|
||||
.reverse()
|
||||
.map(m => `[${m.createdAt.toLocaleString()}] ${m.author.tag}: ${m.cleanContent}`)
|
||||
.join('\n');
|
||||
|
||||
const log = await buildTranscriptText(channelRef, freshTicket);
|
||||
const file = new AttachmentBuilder(Buffer.from(log), {
|
||||
name: `transcript-${channelRef.name}.txt`
|
||||
});
|
||||
@@ -109,19 +103,9 @@ async function postTranscript(channelRef, clientRef, freshTicket) {
|
||||
.catch(() => null);
|
||||
if (!transcriptChan) return;
|
||||
|
||||
const fmt = (d) => new Date(d).toLocaleString('en-US', {
|
||||
month: '2-digit', day: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hour12: true, timeZoneName: 'short'
|
||||
});
|
||||
const openedStr = fmt(freshTicket.createdAt);
|
||||
const closedStr = fmt(new Date());
|
||||
const transcriptContent = CONFIG.DISCORD_TRANSCRIPT_MESSAGE
|
||||
.replace(/\{channel_name\}/g, channelRef.name)
|
||||
.replace(/\{email\}/g, freshTicket.senderEmail || '')
|
||||
.replace(/\{date_opened\}/g, openedStr)
|
||||
.replace(/\{date_closed\}/g, closedStr)
|
||||
+ `\n\nDate Opened: ${openedStr}\nDate Closed: ${closedStr}`;
|
||||
const openedStr = formatDateForTranscript(freshTicket.createdAt);
|
||||
const closedStr = formatDateForTranscript(new Date());
|
||||
const transcriptContent = renderTranscriptHeader(channelRef.name, freshTicket.senderEmail, openedStr, closedStr);
|
||||
await enqueueSend(transcriptChan, { content: transcriptContent, files: [file] });
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,13 @@
|
||||
const {
|
||||
ChannelType,
|
||||
EmbedBuilder,
|
||||
MessageFlags,
|
||||
PermissionFlagsBits
|
||||
MessageFlags
|
||||
} = require('discord.js');
|
||||
const { mongoose } = require('../../db-connection');
|
||||
const { CONFIG } = require('../../config');
|
||||
const { getPriorityEmoji } = require('../../utils');
|
||||
const { checkTicketCreationRateLimit, getOrCreateTicketCategory } = require('../../services/tickets');
|
||||
const { getTicketActionRow } = require('../../utils/ticketComponents');
|
||||
const { checkTicketCreationRateLimit, getOrCreateTicketCategory, makeTicketName } = require('../../services/tickets');
|
||||
const { getTicketActionRow, ticketChannelOverwrites } = require('../../utils/ticketComponents');
|
||||
const { enqueueSend } = require('../../services/channelQueue');
|
||||
const { logError } = require('../../services/debugLog');
|
||||
|
||||
@@ -36,6 +35,8 @@ async function handleCreateTicketFromMessage(interaction) {
|
||||
const guild = interaction.guild;
|
||||
const lastTicket = await Ticket.findOne().sort({ ticketNumber: -1 }).select('ticketNumber').lean();
|
||||
const ticketNumber = (lastTicket?.ticketNumber || 0) + 1;
|
||||
const creatorNickname = message.member?.displayName || message.author.username;
|
||||
const unclaimedName = makeTicketName('unclaimed', { ticketNumber }, creatorNickname);
|
||||
|
||||
let parentCategoryIdForTicket;
|
||||
try {
|
||||
@@ -52,20 +53,10 @@ async function handleCreateTicketFromMessage(interaction) {
|
||||
let channel;
|
||||
try {
|
||||
channel = await guild.channels.create({
|
||||
name: `ticket-${ticketNumber}`,
|
||||
name: unclaimedName,
|
||||
type: ChannelType.GuildText,
|
||||
parent: parentCategoryIdForTicket,
|
||||
permissionOverwrites: [
|
||||
{ id: guild.id, deny: [PermissionFlagsBits.ViewChannel] },
|
||||
{
|
||||
id: message.author.id,
|
||||
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory]
|
||||
},
|
||||
{
|
||||
id: CONFIG.ROLE_ID_TO_PING,
|
||||
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory]
|
||||
}
|
||||
]
|
||||
permissionOverwrites: ticketChannelOverwrites(guild, message.author.id)
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('guild.channels.create (context menu ticket):', err);
|
||||
|
||||
@@ -19,15 +19,26 @@ const { fetchLoggingChannel } = require('./helpers');
|
||||
|
||||
const Ticket = mongoose.model('Ticket');
|
||||
|
||||
/**
|
||||
* Resolve the destination category for an escalation target tier
|
||||
* (nextTier 1 = tier 2, 2 = tier 3), picking the Discord vs email category set
|
||||
* by ticket origin. Returns null/undefined when the relevant category is unset.
|
||||
*/
|
||||
function resolveEscalationCategoryId(ticket, nextTier) {
|
||||
const isDiscordTicket = ticket.gmailThreadId.startsWith('discord-');
|
||||
if (nextTier === 1) {
|
||||
return isDiscordTicket ? CONFIG.DISCORD_ESCALATED2_CHANNEL_ID : CONFIG.EMAIL_ESCALATED2_CHANNEL_ID;
|
||||
}
|
||||
return isDiscordTicket ? CONFIG.DISCORD_ESCALATED3_CHANNEL_ID : CONFIG.EMAIL_ESCALATED3_CHANNEL_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run escalation to a target tier (1 = tier 2, 2 = tier 3). Caller must
|
||||
* validate ticket and currentTier < nextTier, and have already deferred.
|
||||
*/
|
||||
async function runEscalation(interaction, ticket, nextTier) {
|
||||
const isDiscordTicket = ticket.gmailThreadId.startsWith('discord-');
|
||||
const categoryId = nextTier === 1
|
||||
? (isDiscordTicket ? CONFIG.DISCORD_ESCALATED2_CHANNEL_ID : CONFIG.EMAIL_ESCALATED2_CHANNEL_ID)
|
||||
: (isDiscordTicket ? CONFIG.DISCORD_ESCALATED3_CHANNEL_ID : CONFIG.EMAIL_ESCALATED3_CHANNEL_ID);
|
||||
const categoryId = resolveEscalationCategoryId(ticket, nextTier);
|
||||
|
||||
// Clear claim on escalation
|
||||
await Ticket.updateOne(
|
||||
@@ -88,7 +99,7 @@ async function runEscalation(interaction, ticket, nextTier) {
|
||||
const escalatorName = interaction.member?.displayName || interaction.user.username;
|
||||
const tierLabel = nextTier === 1 ? 'tier 2' : 'tier 3';
|
||||
const emailBody = `${escalatorName} escalated this ticket to ${tierLabel}.`;
|
||||
await sendTicketNotificationEmail(ticket, null, emailBody, interaction.user.id);
|
||||
await sendTicketNotificationEmail(ticket, emailBody, interaction.user.id);
|
||||
} catch (emailErr) {
|
||||
console.error('Escalation email failed (non-fatal):', emailErr.message);
|
||||
}
|
||||
@@ -179,9 +190,7 @@ async function handleEscalate(interaction) {
|
||||
}
|
||||
|
||||
const isDiscordTicket = ticket.gmailThreadId.startsWith('discord-');
|
||||
const categoryId = nextTier === 1
|
||||
? (isDiscordTicket ? CONFIG.DISCORD_ESCALATED2_CHANNEL_ID : CONFIG.EMAIL_ESCALATED2_CHANNEL_ID)
|
||||
: (isDiscordTicket ? CONFIG.DISCORD_ESCALATED3_CHANNEL_ID : CONFIG.EMAIL_ESCALATED3_CHANNEL_ID);
|
||||
const categoryId = resolveEscalationCategoryId(ticket, nextTier);
|
||||
const configKey = nextTier === 1 ? 'ESCALATED2' : 'ESCALATED3';
|
||||
if (!categoryId && !interaction.channel.isThread()) {
|
||||
return interaction.reply({
|
||||
@@ -210,4 +219,4 @@ async function handleDeescalate(interaction) {
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { runEscalation, runDeescalation, handleEscalate, handleDeescalate };
|
||||
module.exports = { runEscalation, runDeescalation, handleEscalate, handleDeescalate, resolveEscalationCategoryId };
|
||||
|
||||
@@ -25,7 +25,7 @@ const { logError, logTicketEvent } = require('../../services/debugLog');
|
||||
const { findTicketForChannel } = require('../sharedHelpers');
|
||||
|
||||
const { requireStaffRole, fetchLoggingChannel } = require('./helpers');
|
||||
const { runEscalation, runDeescalation, handleEscalate, handleDeescalate } = require('./escalation');
|
||||
const { runEscalation, runDeescalation, handleEscalate, handleDeescalate, resolveEscalationCategoryId } = require('./escalation');
|
||||
const { handleCloseTimer, handleCancelClose, handleForceClose } = require('./close');
|
||||
const { handleResponse, handleAutocomplete } = require('./response');
|
||||
const { handlePanel, handleSignature } = require('./panel');
|
||||
@@ -266,7 +266,7 @@ async function handleHelp(interaction) {
|
||||
},
|
||||
{
|
||||
name: 'Ticket Management',
|
||||
value: '`/transfer @staff` - Transfer ticket to another staff member\n`/move #category` - Move ticket to another category\n`/force-close` - Force close ticket without confirmation\n`/topic <text>` - Set ticket topic/description'
|
||||
value: '`/transfer @staff [reason]` - Transfer ticket to another staff member\n`/move #category` - Move ticket to another category\n`/force-close` - Force close ticket without confirmation\n`/cancel-close` - Abort a pending force-close countdown\n`/topic <text>` - Set ticket topic/description'
|
||||
},
|
||||
{
|
||||
name: 'Saved Responses',
|
||||
@@ -282,10 +282,18 @@ async function handleHelp(interaction) {
|
||||
},
|
||||
{
|
||||
name: 'Escalation',
|
||||
value: '`/escalate [reason] [tier]` - Escalate ticket (tier 2 or 3, or one step)\n`/deescalate` - De-escalate ticket (tier 3→2 or tier 2→normal)'
|
||||
value: '`/escalate <level>` - Escalate ticket (tier 2 or 3, or one step)\n`/deescalate` - De-escalate ticket (tier 3→2 or tier 2→normal)'
|
||||
},
|
||||
{
|
||||
name: 'Staff Configuration',
|
||||
value: '`/notifydm` - Toggle DM notifications for your claimed tickets\n`/signature` - Set your email signature\n`/closetimer <seconds>` - Set the force-close countdown\n`/staffthread` - Toggle/configure per-ticket staff threads\n`/pinmessages` - Toggle auto-pinning of ticket messages\n`/gmailpoll <interval>` - Set the Gmail poll interval'
|
||||
},
|
||||
{
|
||||
name: 'Right-click (Apps menu)',
|
||||
value: '`Create Ticket From Message` - Turn a message into a ticket\n`View User Tickets` - Show a user\'s recent tickets'
|
||||
}
|
||||
])
|
||||
.setFooter({ text: 'Click buttons on ticket messages to claim/close' });
|
||||
.setFooter({ text: 'Click buttons on ticket messages to claim/close. Config changes via slash commands apply until the next restart.' });
|
||||
|
||||
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
@@ -342,5 +350,6 @@ module.exports = {
|
||||
handleContextMenu,
|
||||
handleAutocomplete,
|
||||
runEscalation,
|
||||
runDeescalation
|
||||
runDeescalation,
|
||||
resolveEscalationCategoryId
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ const { mongoose } = require('../db-connection');
|
||||
const { CONFIG } = require('../config');
|
||||
const { extractRawEmail, isStaff } = require('../utils');
|
||||
const { getGmailClient, sendGmailReply } = require('../services/gmail');
|
||||
const { updateTicketActivity } = require('../services/tickets');
|
||||
const { getNotifyDm } = require('../services/staffSettings');
|
||||
const { logError } = require('../services/debugLog');
|
||||
|
||||
@@ -93,8 +92,6 @@ async function handleDiscordReply(m) {
|
||||
msgId,
|
||||
m.author.id
|
||||
);
|
||||
|
||||
await updateTicketActivity(ticket.gmailThreadId);
|
||||
} catch (e) {
|
||||
console.error('REPLY ERROR:', e);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Shared pending-close timer map.
|
||||
* Keyed by channel.id → { timeout, userId, username }.
|
||||
* Used by buttons.js (sets timers) and commands.js (cancel-close clears them).
|
||||
* Keyed by channel.id → { timeout, username, sendEmail }.
|
||||
* Used by buttons.js (sets timers) and commands/ (cancel-close clears them).
|
||||
*/
|
||||
const pendingCloses = new Map();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user