From 83b6b4ae0cba4643db40c7db1b84a370fa45bf80 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Thu, 7 May 2026 18:45:18 +0000 Subject: [PATCH] simplify: rename CONFIG channels, dedup hasStaffRole, drop enforceEmbedLimit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename CONFIG.TRANSCRIPT_CHAN -> CONFIG.TRANSCRIPT_CHANNEL_ID and CONFIG.LOG_CHAN -> CONFIG.LOGGING_CHANNEL_ID across 9 callsites so CONFIG keys match their .env names — no more "grep .env, find nothing" for new readers - Replace handlers/commands.js#hasStaffRole with utils.js#isStaff (was a verbatim copy) - Delete utils.js#enforceEmbedLimit and its 2 callsites; both inputs are bounded well under the 6000-char Discord embed cap, so the trim was defensive code that never fired Co-Authored-By: Claude Opus 4.7 (1M context) --- config.js | 4 +-- gmail-poll.js | 4 +-- handlers/buttons.js | 9 +++--- handlers/commands.js | 27 +++++----------- services/debugLog.js | 2 +- utils.js | 74 -------------------------------------------- 6 files changed, 15 insertions(+), 105 deletions(-) diff --git a/config.js b/config.js index 6608657..d5120b1 100644 --- a/config.js +++ b/config.js @@ -18,8 +18,8 @@ const CONFIG = { DISCORD_TICKET_CATEGORY_ID: process.env.DISCORD_TICKET_CATEGORY_ID || process.env.TICKET_CATEGORY_ID, ROLE_ID_TO_PING: process.env.ROLE_ID_TO_PING, ROLE_TO_PING_ID: process.env.ROLE_ID_TO_PING || process.env.ROLE_TO_PING_ID, - TRANSCRIPT_CHAN: process.env.TRANSCRIPT_CHANNEL_ID, - LOG_CHAN: process.env.LOGGING_CHANNEL_ID, + TRANSCRIPT_CHANNEL_ID: process.env.TRANSCRIPT_CHANNEL_ID, + LOGGING_CHANNEL_ID: process.env.LOGGING_CHANNEL_ID, DEBUGGING_CHANNEL_ID: process.env.DEBUGGING_CHANNEL_ID || null, CLIENT_ID: process.env.DISCORD_APPLICATION_ID, REFRESH_TOKEN: process.env.REFRESH_TOKEN, diff --git a/gmail-poll.js b/gmail-poll.js index 77f11a6..a45ab8d 100644 --- a/gmail-poll.js +++ b/gmail-poll.js @@ -14,7 +14,6 @@ const { stripEmailQuotes, stripMobileFooter, detectGame, - enforceEmbedLimit, sanitizeEmbedText } = require('./utils'); const { getGmailClient } = require('./services/gmail'); @@ -225,7 +224,6 @@ async function poll(client) { { name: 'Subject', value: `\`\`\`\n${sanitizeEmbedText(subject) || 'No subject'}\n\`\`\``, inline: false } ); - enforceEmbedLimit([ticketInfoEmbed]); const welcomeMsg = await enqueueSend(ticketChan, { content: `<@&${CONFIG.ROLE_ID_TO_PING}>`, embeds: [ticketInfoEmbed], @@ -251,7 +249,7 @@ async function poll(client) { if (transcriptRows.length > 0) { const transcriptChan = await client.channels - .fetch(CONFIG.TRANSCRIPT_CHAN) + .fetch(CONFIG.TRANSCRIPT_CHANNEL_ID) .catch(() => null); if (transcriptChan) { diff --git a/handlers/buttons.js b/handlers/buttons.js index 3b3ab78..432cc58 100644 --- a/handlers/buttons.js +++ b/handlers/buttons.js @@ -19,7 +19,7 @@ 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 { sanitizeEmbedText, truncateEmbedDescription, enforceEmbedLimit } = require('../utils'); +const { sanitizeEmbedText, truncateEmbedDescription } = require('../utils'); const { enqueueRename, enqueueSend } = require('../services/channelQueue'); const { runEscalation, runDeescalation } = require('./commands'); const { pendingCloses } = require('./pendingCloses'); @@ -470,7 +470,7 @@ async function handleConfirmClose(interaction, ticket, sendEmail = true) { await enqueueSend(interaction.channel, discordCloseContent); const transcriptChan = await interaction.client.channels - .fetch(CONFIG.TRANSCRIPT_CHAN) + .fetch(CONFIG.TRANSCRIPT_CHANNEL_ID) .catch(() => null); let transcriptMsg = null; @@ -516,7 +516,7 @@ async function handleConfirmClose(interaction, ticket, sendEmail = true) { } const logChan = await interaction.client.channels - .fetch(CONFIG.LOG_CHAN) + .fetch(CONFIG.LOGGING_CHANNEL_ID) .catch(() => null); if (logChan) { const closerMention = interaction.user.toString(); @@ -682,7 +682,6 @@ async function handleTicketModal(interaction) { const actionRow = getTicketActionRow({ escalationTier: 0 }); - enforceEmbedLimit([welcomeEmbed, infoEmbed, resourcesEmbed]); let welcomeMsg; try { welcomeMsg = await enqueueSend(channel, { @@ -709,7 +708,7 @@ async function handleTicketModal(interaction) { await interaction.deleteReply().catch(() => {}); - const logChan = await interaction.client.channels.fetch(CONFIG.LOG_CHAN).catch(() => null); + const logChan = await interaction.client.channels.fetch(CONFIG.LOGGING_CHANNEL_ID).catch(() => null); if (logChan) { await enqueueSend(logChan, `📝 ${channel.name} created by ${interaction.user.tag}` diff --git a/handlers/commands.js b/handlers/commands.js index 2c73d99..1295e66 100644 --- a/handlers/commands.js +++ b/handlers/commands.js @@ -11,7 +11,7 @@ const { } = require('discord.js'); const { mongoose } = require('../db-connection'); const { CONFIG } = require('../config'); -const { getPriorityEmoji, replaceVariables } = require('../utils'); +const { getPriorityEmoji, replaceVariables, isStaff } = require('../utils'); const { makeTicketName, resolveCreatorNickname, getOrCreateTicketCategory, checkTicketCreationRateLimit } = require('../services/tickets'); const { sendTicketNotificationEmail } = require('../services/gmail'); const { getTicketActionRow } = require('../utils/ticketComponents'); @@ -23,19 +23,6 @@ const { pendingCloses } = require('./pendingCloses'); const Ticket = mongoose.model('Ticket'); const Tag = mongoose.model('Tag'); -/** - * True if member has the support role (ROLE_ID_TO_PING) or any ADDITIONAL_STAFF_ROLES. - * Used to restrict commands to staff only; customers cannot use bot commands. - * @param {import('discord.js').GuildMember|null} member - * @returns {boolean} - */ -function hasStaffRole(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)); -} - /** * Reply ephemeral and return true if the interaction is in a guild and the user is not staff (so caller should return). * @param {import('discord.js').CommandInteraction|import('discord.js').ContextMenuCommandInteraction} interaction @@ -44,7 +31,7 @@ function hasStaffRole(member) { async function requireStaffRole(interaction) { if (!interaction.guild) return false; if (!CONFIG.ROLE_ID_TO_PING && (!CONFIG.ADDITIONAL_STAFF_ROLES || CONFIG.ADDITIONAL_STAFF_ROLES.length === 0)) return false; - if (hasStaffRole(interaction.member)) return false; + if (isStaff(interaction.member)) return false; const roleMention = CONFIG.ROLE_ID_TO_PING ? `<@&${CONFIG.ROLE_ID_TO_PING}>` : 'support'; await interaction.reply({ content: `This command is only available to the support team (${roleMention}).`, @@ -149,7 +136,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) { } const logChan = await interaction.client.channels - .fetch(CONFIG.LOG_CHAN) + .fetch(CONFIG.LOGGING_CHANNEL_ID) .catch(() => null); if (logChan) { const ticketType = isDiscordTicket ? 'Discord' : 'Email'; @@ -203,7 +190,7 @@ async function runDeescalation(interaction, ticket) { .setFooter({ text: interaction.member?.displayName || interaction.user.username }); await interaction.editReply({ embeds: [deescalateEmbed] }); - const logChan = await interaction.client.channels.fetch(CONFIG.LOG_CHAN).catch(() => null); + const logChan = await interaction.client.channels.fetch(CONFIG.LOGGING_CHANNEL_ID).catch(() => null); if (logChan) { const ticketType = isDiscordTicket ? 'Discord' : 'Email'; await enqueueSend(logChan, @@ -373,7 +360,7 @@ async function handleCommand(interaction) { allowedMentions: { parse: ['users'] } }); - const logChan = await interaction.client.channels.fetch(CONFIG.LOG_CHAN).catch(() => null); + const logChan = await interaction.client.channels.fetch(CONFIG.LOGGING_CHANNEL_ID).catch(() => null); if (logChan) { await enqueueSend(logChan, { content: `Ticket ${interaction.channel} transferred from ${interaction.user.tag} to ${member.tag}.\nReason: ${reason}`, @@ -400,7 +387,7 @@ async function handleCommand(interaction) { await interaction.channel.setParent(category.id, { lockPermissions: true }); await interaction.reply(`Moved ticket to **${category.name}**.`); - const logChan = await interaction.client.channels.fetch(CONFIG.LOG_CHAN).catch(() => null); + const logChan = await interaction.client.channels.fetch(CONFIG.LOGGING_CHANNEL_ID).catch(() => null); if (logChan) { await enqueueSend(logChan, `Ticket ${interaction.channel} moved to category **${category.name}** by ${interaction.user.tag}` @@ -530,7 +517,7 @@ async function handleCommand(interaction) { }); const transcriptChan = await clientRef.channels - .fetch(CONFIG.TRANSCRIPT_CHAN) + .fetch(CONFIG.TRANSCRIPT_CHANNEL_ID) .catch(() => null); if (transcriptChan) { diff --git a/services/debugLog.js b/services/debugLog.js index 2b9a465..fa75f38 100644 --- a/services/debugLog.js +++ b/services/debugLog.js @@ -69,7 +69,7 @@ async function logTicketEvent(action, fields, interaction = null) { if (interaction?.user?.tag) { embed.setFooter({ text: interaction.user.tag }); } - await sendToChannel(CONFIG.LOG_CHAN, embed, interaction?.client); + await sendToChannel(CONFIG.LOGGING_CHANNEL_ID, embed, interaction?.client); } module.exports = { diff --git a/utils.js b/utils.js index 12467c4..8cce297 100644 --- a/utils.js +++ b/utils.js @@ -264,83 +264,9 @@ function truncateEmbedDescription(str, max = 4096) { return s.length > max ? s.slice(0, max - 3) + '...' : s; } -/** - * Enforce the 6 000 char total embed limit across an array of EmbedBuilder - * instances. Mutates in place: trims the largest description first, then - * largest field values, until the total is under 6 000 chars. - * Returns the same array for chaining. - */ -function enforceEmbedLimit(embeds) { - const charCount = (e) => { - const d = e.data || {}; - let total = 0; - if (d.title) total += d.title.length; - if (d.description) total += d.description.length; - if (d.footer?.text) total += d.footer.text.length; - if (d.author?.name) total += d.author.name.length; - if (d.fields) { - for (const f of d.fields) { - if (f.name) total += f.name.length; - if (f.value) total += f.value.length; - } - } - return total; - }; - - const LIMIT = 6000; - - const totalChars = () => embeds.reduce((sum, e) => sum + charCount(e), 0); - - // Trim largest descriptions first - while (totalChars() > LIMIT) { - let largestIdx = -1; - let largestLen = 0; - for (let i = 0; i < embeds.length; i++) { - const desc = embeds[i].data?.description; - if (desc && desc.length > largestLen) { - largestLen = desc.length; - largestIdx = i; - } - } - if (largestIdx === -1 || largestLen <= 4) break; - const excess = totalChars() - LIMIT; - const newLen = Math.max(1, largestLen - excess - 3); - embeds[largestIdx].setDescription( - embeds[largestIdx].data.description.slice(0, newLen) + '...' - ); - if (totalChars() <= LIMIT) break; - // If still over, loop will pick next largest - } - - // Trim largest field values - while (totalChars() > LIMIT) { - let targetEmbed = null; - let targetFieldIdx = -1; - let targetLen = 0; - for (const e of embeds) { - const fields = e.data?.fields || []; - for (let fi = 0; fi < fields.length; fi++) { - if (fields[fi].value && fields[fi].value.length > targetLen) { - targetLen = fields[fi].value.length; - targetEmbed = e; - targetFieldIdx = fi; - } - } - } - if (!targetEmbed || targetLen <= 4) break; - const excess = totalChars() - LIMIT; - const newLen = Math.max(1, targetLen - excess - 3); - targetEmbed.data.fields[targetFieldIdx].value = - targetEmbed.data.fields[targetFieldIdx].value.slice(0, newLen) + '...'; - } - - return embeds; -} - module.exports = { sanitizeEmbedText, truncateEmbedDescription, - enforceEmbedLimit, escapeHtml, safeEqual, isStaff,