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:
@@ -24,23 +24,22 @@ const ALLOWED_CONFIG_KEYS = new Set([
|
||||
'EMAIL_ESCALATED2_CHANNEL_ID', 'DISCORD_ESCALATED2_CHANNEL_ID',
|
||||
'EMAIL_ESCALATED3_CHANNEL_ID', 'DISCORD_ESCALATED3_CHANNEL_ID',
|
||||
// Roles and staff
|
||||
'ROLE_ID_TO_PING', 'ROLE_TO_PING_ID', 'ADDITIONAL_STAFF_ROLES', 'BLACKLISTED_ROLES',
|
||||
'ROLE_ID_TO_PING', 'ADDITIONAL_STAFF_ROLES', 'BLACKLISTED_ROLES',
|
||||
'ADMIN_ID',
|
||||
// Channel IDs
|
||||
'TRANSCRIPT_CHANNEL_ID', 'LOGGING_CHANNEL_ID', 'DEBUGGING_CHANNEL_ID',
|
||||
'RENAME_LOG_CHANNEL_ID',
|
||||
// Messages and labels
|
||||
'ESCALATION_MESSAGE', 'TICKET_CLOSE_SUBJECT_PREFIX', 'TICKET_CLOSE_MESSAGE', 'TICKET_CLOSE_SIGNATURE',
|
||||
'DISCORD_CLOSE_MESSAGE', 'DISCORD_TRANSCRIPT_MESSAGE', 'DISCORD_AUTO_CLOSE_MESSAGE',
|
||||
'TICKET_WELCOME_MESSAGE', 'TICKET_CLAIMED_MESSAGE', 'TICKET_UNCLAIMED_MESSAGE',
|
||||
'REMINDER_MESSAGE', 'BUTTON_LABEL_CLOSE', 'BUTTON_LABEL_CLAIM', 'BUTTON_LABEL_UNCLAIM',
|
||||
'BUTTON_LABEL_CLOSE', 'BUTTON_LABEL_CLAIM', 'BUTTON_LABEL_UNCLAIM',
|
||||
'BUTTON_EMOJI_CLOSE', 'BUTTON_EMOJI_CLAIM', 'BUTTON_EMOJI_UNCLAIM',
|
||||
// Branding
|
||||
'LOGO_URL', 'SUPPORT_NAME', 'EMAIL_SIGNATURE', 'GAME_LIST',
|
||||
// Toggles
|
||||
'AUTO_CLOSE_ENABLED', 'AUTO_CLOSE_AFTER_HOURS', 'AUTO_UNCLAIM_ENABLED', 'AUTO_UNCLAIM_AFTER_HOURS',
|
||||
'ALLOW_CLAIM_OVERWRITE',
|
||||
'REMINDER_ENABLED', 'REMINDER_AFTER_HOURS', 'PRIORITY_ENABLED', 'DEFAULT_PRIORITY',
|
||||
'PRIORITY_ENABLED', 'DEFAULT_PRIORITY',
|
||||
'STAFF_THREAD_ENABLED', 'STAFF_THREAD_NAME', 'STAFF_THREAD_AUTO_ADD_ROLE', 'STAFF_THREAD_ROLE_ID',
|
||||
'PIN_INITIAL_MESSAGE_ENABLED', 'PIN_ESCALATION_MESSAGE_ENABLED', 'PIN_SUPPRESS_SYSTEM_MESSAGE',
|
||||
// Limits and thresholds
|
||||
@@ -140,26 +139,6 @@ const VALIDATORS = {
|
||||
return { ok: true, coerced: parts.join(',') };
|
||||
}
|
||||
},
|
||||
json: {
|
||||
type: 'json',
|
||||
validate(value) {
|
||||
if (isEmptyInput(value)) return { ok: true, coerced: '' };
|
||||
const str = String(value);
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return { ok: true, coerced: str };
|
||||
} catch (_) {
|
||||
return { ok: false, error: 'must be valid JSON' };
|
||||
}
|
||||
}
|
||||
},
|
||||
string_or_json: {
|
||||
type: 'string_or_json',
|
||||
validate(value) {
|
||||
if (value === null || value === undefined) return { ok: false, error: 'cannot be null' };
|
||||
return { ok: true, coerced: String(value) };
|
||||
}
|
||||
},
|
||||
// Fallback. Preserves legacy coercion so CONFIG.* values keep their types
|
||||
// for consumers that compare with === true / === 5 (see old applyConfigUpdates).
|
||||
string: {
|
||||
|
||||
@@ -182,18 +182,17 @@ async function sendTicketClosedEmail(ticket, closerName, userId = null) {
|
||||
/**
|
||||
* Send a notification email in the ticket thread (e.g. escalation, high-priority).
|
||||
* @param {Object} ticket - Ticket with gmailThreadId, senderEmail, subject
|
||||
* @param {string} subjectLine - Fallback subject if the thread can't be queried
|
||||
* @param {string} messageBody - Plain or HTML message body
|
||||
* @param {string} [userId] - Discord user ID for signature (optional)
|
||||
*/
|
||||
async function sendTicketNotificationEmail(ticket, subjectLine, messageBody, userId = null) {
|
||||
async function sendTicketNotificationEmail(ticket, messageBody, userId = null) {
|
||||
try {
|
||||
const recipient = resolveCustomerRecipient(ticket, 'sendTicketNotificationEmail');
|
||||
if (!recipient) return;
|
||||
|
||||
const gmail = getGmailClient();
|
||||
const { subject, msgId } = await fetchThreadSubjectAndMsgId(gmail, ticket.gmailThreadId);
|
||||
const encodedSubject = encodeReplySubject(subject || subjectLine || ticket.subject || 'Support');
|
||||
const encodedSubject = encodeReplySubject(subject || ticket.subject || 'Support');
|
||||
|
||||
await sendThreadedEmail(gmail, {
|
||||
threadId: ticket.gmailThreadId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Ticket database helpers – counters, rename, limits, auto-close,
|
||||
* reminders, auto-unclaim, channel creation.
|
||||
* auto-unclaim, channel creation.
|
||||
*/
|
||||
const { ChannelType } = require('discord.js');
|
||||
const { mongoose, withRetry } = require('../db-connection');
|
||||
@@ -269,15 +269,6 @@ async function checkTicketLimits(senderEmail) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// --- ACTIVITY ---
|
||||
|
||||
async function updateTicketActivity(gmailThreadId) {
|
||||
await Ticket.updateOne(
|
||||
{ gmailThreadId },
|
||||
{ $set: { lastActivity: new Date() } }
|
||||
);
|
||||
}
|
||||
|
||||
// --- SCHEDULED CHECKS ---
|
||||
// These accept `client` and optionally `sendTicketClosedEmail` to avoid circular deps.
|
||||
|
||||
@@ -291,11 +282,11 @@ async function checkAutoClose(client, sendTicketClosedEmail) {
|
||||
lastActivity: { $lt: cutoffTime, $ne: null }
|
||||
}).sort({ createdAt: 1 }).limit(500).lean());
|
||||
|
||||
const guild = client.guilds.cache.first();
|
||||
if (!guild) return;
|
||||
|
||||
for (const ticket of staleTickets) {
|
||||
try {
|
||||
const guild = client.guilds.cache.first();
|
||||
if (!guild) continue;
|
||||
|
||||
const channel = await guild.channels.fetch(ticket.discordThreadId).catch(() => null);
|
||||
if (channel) {
|
||||
await enqueueSend(channel, CONFIG.DISCORD_AUTO_CLOSE_MESSAGE);
|
||||
@@ -337,11 +328,11 @@ async function checkAutoUnclaim(client) {
|
||||
lastActivity: { $lt: unclaimTime, $ne: null }
|
||||
}).lean());
|
||||
|
||||
const guild = client.guilds.cache.first();
|
||||
if (!guild) return;
|
||||
|
||||
for (const ticket of staleClaimedTickets) {
|
||||
try {
|
||||
const guild = client.guilds.cache.first();
|
||||
if (!guild) continue;
|
||||
|
||||
const channel = await guild.channels.fetch(ticket.discordThreadId).catch(() => null);
|
||||
if (channel) {
|
||||
await withRetry(() => Ticket.updateOne(
|
||||
@@ -426,7 +417,6 @@ module.exports = {
|
||||
makeTicketName,
|
||||
checkTicketCreationRateLimit,
|
||||
checkTicketLimits,
|
||||
updateTicketActivity,
|
||||
checkAutoClose,
|
||||
checkAutoUnclaim,
|
||||
reconcileDeletedTicketChannels,
|
||||
|
||||
37
services/transcript.js
Normal file
37
services/transcript.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Shared transcript rendering for the two close paths
|
||||
* (handlers/buttons.js runFinalClose and handlers/commands/close.js postTranscript).
|
||||
* Pure formatting only — each caller owns its own posting / DB / email side effects.
|
||||
*/
|
||||
const { CONFIG } = require('../config');
|
||||
|
||||
/** 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');
|
||||
}
|
||||
|
||||
/** Format a date for the transcript header (US locale, 12h, with time zone). */
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
/** Build the transcript-channel message body from DISCORD_TRANSCRIPT_MESSAGE. */
|
||||
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}`;
|
||||
}
|
||||
|
||||
module.exports = { buildTranscriptText, formatDateForTranscript, renderTranscriptHeader };
|
||||
Reference in New Issue
Block a user