simplify: rename CONFIG channels, dedup hasStaffRole, drop enforceEmbedLimit
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,8 @@ const CONFIG = {
|
|||||||
DISCORD_TICKET_CATEGORY_ID: process.env.DISCORD_TICKET_CATEGORY_ID || process.env.TICKET_CATEGORY_ID,
|
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_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,
|
ROLE_TO_PING_ID: process.env.ROLE_ID_TO_PING || process.env.ROLE_TO_PING_ID,
|
||||||
TRANSCRIPT_CHAN: process.env.TRANSCRIPT_CHANNEL_ID,
|
TRANSCRIPT_CHANNEL_ID: process.env.TRANSCRIPT_CHANNEL_ID,
|
||||||
LOG_CHAN: process.env.LOGGING_CHANNEL_ID,
|
LOGGING_CHANNEL_ID: process.env.LOGGING_CHANNEL_ID,
|
||||||
DEBUGGING_CHANNEL_ID: process.env.DEBUGGING_CHANNEL_ID || null,
|
DEBUGGING_CHANNEL_ID: process.env.DEBUGGING_CHANNEL_ID || null,
|
||||||
CLIENT_ID: process.env.DISCORD_APPLICATION_ID,
|
CLIENT_ID: process.env.DISCORD_APPLICATION_ID,
|
||||||
REFRESH_TOKEN: process.env.REFRESH_TOKEN,
|
REFRESH_TOKEN: process.env.REFRESH_TOKEN,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const {
|
|||||||
stripEmailQuotes,
|
stripEmailQuotes,
|
||||||
stripMobileFooter,
|
stripMobileFooter,
|
||||||
detectGame,
|
detectGame,
|
||||||
enforceEmbedLimit,
|
|
||||||
sanitizeEmbedText
|
sanitizeEmbedText
|
||||||
} = require('./utils');
|
} = require('./utils');
|
||||||
const { getGmailClient } = require('./services/gmail');
|
const { getGmailClient } = require('./services/gmail');
|
||||||
@@ -225,7 +224,6 @@ async function poll(client) {
|
|||||||
{ name: 'Subject', value: `\`\`\`\n${sanitizeEmbedText(subject) || 'No subject'}\n\`\`\``, inline: false }
|
{ name: 'Subject', value: `\`\`\`\n${sanitizeEmbedText(subject) || 'No subject'}\n\`\`\``, inline: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
enforceEmbedLimit([ticketInfoEmbed]);
|
|
||||||
const welcomeMsg = await enqueueSend(ticketChan, {
|
const welcomeMsg = await enqueueSend(ticketChan, {
|
||||||
content: `<@&${CONFIG.ROLE_ID_TO_PING}>`,
|
content: `<@&${CONFIG.ROLE_ID_TO_PING}>`,
|
||||||
embeds: [ticketInfoEmbed],
|
embeds: [ticketInfoEmbed],
|
||||||
@@ -251,7 +249,7 @@ async function poll(client) {
|
|||||||
|
|
||||||
if (transcriptRows.length > 0) {
|
if (transcriptRows.length > 0) {
|
||||||
const transcriptChan = await client.channels
|
const transcriptChan = await client.channels
|
||||||
.fetch(CONFIG.TRANSCRIPT_CHAN)
|
.fetch(CONFIG.TRANSCRIPT_CHANNEL_ID)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
|
||||||
if (transcriptChan) {
|
if (transcriptChan) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const { CONFIG } = require('../config');
|
|||||||
const { makeTicketName, resolveCreatorNickname, getOrCreateTicketCategory, cleanupEmptyOverflowCategory, checkTicketCreationRateLimit, toDiscordSafeName } = require('../services/tickets');
|
const { makeTicketName, resolveCreatorNickname, getOrCreateTicketCategory, cleanupEmptyOverflowCategory, checkTicketCreationRateLimit, toDiscordSafeName } = require('../services/tickets');
|
||||||
const { sendTicketClosedEmail } = require('../services/gmail');
|
const { sendTicketClosedEmail } = require('../services/gmail');
|
||||||
const { getTicketActionRow } = require('../utils/ticketComponents');
|
const { getTicketActionRow } = require('../utils/ticketComponents');
|
||||||
const { sanitizeEmbedText, truncateEmbedDescription, enforceEmbedLimit } = require('../utils');
|
const { sanitizeEmbedText, truncateEmbedDescription } = require('../utils');
|
||||||
const { enqueueRename, enqueueSend } = require('../services/channelQueue');
|
const { enqueueRename, enqueueSend } = require('../services/channelQueue');
|
||||||
const { runEscalation, runDeescalation } = require('./commands');
|
const { runEscalation, runDeescalation } = require('./commands');
|
||||||
const { pendingCloses } = require('./pendingCloses');
|
const { pendingCloses } = require('./pendingCloses');
|
||||||
@@ -470,7 +470,7 @@ async function handleConfirmClose(interaction, ticket, sendEmail = true) {
|
|||||||
await enqueueSend(interaction.channel, discordCloseContent);
|
await enqueueSend(interaction.channel, discordCloseContent);
|
||||||
|
|
||||||
const transcriptChan = await interaction.client.channels
|
const transcriptChan = await interaction.client.channels
|
||||||
.fetch(CONFIG.TRANSCRIPT_CHAN)
|
.fetch(CONFIG.TRANSCRIPT_CHANNEL_ID)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
let transcriptMsg = null;
|
let transcriptMsg = null;
|
||||||
|
|
||||||
@@ -516,7 +516,7 @@ async function handleConfirmClose(interaction, ticket, sendEmail = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logChan = await interaction.client.channels
|
const logChan = await interaction.client.channels
|
||||||
.fetch(CONFIG.LOG_CHAN)
|
.fetch(CONFIG.LOGGING_CHANNEL_ID)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
if (logChan) {
|
if (logChan) {
|
||||||
const closerMention = interaction.user.toString();
|
const closerMention = interaction.user.toString();
|
||||||
@@ -682,7 +682,6 @@ async function handleTicketModal(interaction) {
|
|||||||
|
|
||||||
const actionRow = getTicketActionRow({ escalationTier: 0 });
|
const actionRow = getTicketActionRow({ escalationTier: 0 });
|
||||||
|
|
||||||
enforceEmbedLimit([welcomeEmbed, infoEmbed, resourcesEmbed]);
|
|
||||||
let welcomeMsg;
|
let welcomeMsg;
|
||||||
try {
|
try {
|
||||||
welcomeMsg = await enqueueSend(channel, {
|
welcomeMsg = await enqueueSend(channel, {
|
||||||
@@ -709,7 +708,7 @@ async function handleTicketModal(interaction) {
|
|||||||
|
|
||||||
await interaction.deleteReply().catch(() => {});
|
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) {
|
if (logChan) {
|
||||||
await enqueueSend(logChan,
|
await enqueueSend(logChan,
|
||||||
`📝 ${channel.name} created by ${interaction.user.tag}`
|
`📝 ${channel.name} created by ${interaction.user.tag}`
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const {
|
|||||||
} = require('discord.js');
|
} = require('discord.js');
|
||||||
const { mongoose } = require('../db-connection');
|
const { mongoose } = require('../db-connection');
|
||||||
const { CONFIG } = require('../config');
|
const { CONFIG } = require('../config');
|
||||||
const { getPriorityEmoji, replaceVariables } = require('../utils');
|
const { getPriorityEmoji, replaceVariables, isStaff } = require('../utils');
|
||||||
const { makeTicketName, resolveCreatorNickname, getOrCreateTicketCategory, checkTicketCreationRateLimit } = require('../services/tickets');
|
const { makeTicketName, resolveCreatorNickname, getOrCreateTicketCategory, checkTicketCreationRateLimit } = require('../services/tickets');
|
||||||
const { sendTicketNotificationEmail } = require('../services/gmail');
|
const { sendTicketNotificationEmail } = require('../services/gmail');
|
||||||
const { getTicketActionRow } = require('../utils/ticketComponents');
|
const { getTicketActionRow } = require('../utils/ticketComponents');
|
||||||
@@ -23,19 +23,6 @@ const { pendingCloses } = require('./pendingCloses');
|
|||||||
const Ticket = mongoose.model('Ticket');
|
const Ticket = mongoose.model('Ticket');
|
||||||
const Tag = mongoose.model('Tag');
|
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).
|
* 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
|
* @param {import('discord.js').CommandInteraction|import('discord.js').ContextMenuCommandInteraction} interaction
|
||||||
@@ -44,7 +31,7 @@ function hasStaffRole(member) {
|
|||||||
async function requireStaffRole(interaction) {
|
async function requireStaffRole(interaction) {
|
||||||
if (!interaction.guild) return false;
|
if (!interaction.guild) return false;
|
||||||
if (!CONFIG.ROLE_ID_TO_PING && (!CONFIG.ADDITIONAL_STAFF_ROLES || CONFIG.ADDITIONAL_STAFF_ROLES.length === 0)) 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';
|
const roleMention = CONFIG.ROLE_ID_TO_PING ? `<@&${CONFIG.ROLE_ID_TO_PING}>` : 'support';
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: `This command is only available to the support team (${roleMention}).`,
|
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
|
const logChan = await interaction.client.channels
|
||||||
.fetch(CONFIG.LOG_CHAN)
|
.fetch(CONFIG.LOGGING_CHANNEL_ID)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
if (logChan) {
|
if (logChan) {
|
||||||
const ticketType = isDiscordTicket ? 'Discord' : 'Email';
|
const ticketType = isDiscordTicket ? 'Discord' : 'Email';
|
||||||
@@ -203,7 +190,7 @@ async function runDeescalation(interaction, ticket) {
|
|||||||
.setFooter({ text: interaction.member?.displayName || interaction.user.username });
|
.setFooter({ text: interaction.member?.displayName || interaction.user.username });
|
||||||
await interaction.editReply({ embeds: [deescalateEmbed] });
|
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) {
|
if (logChan) {
|
||||||
const ticketType = isDiscordTicket ? 'Discord' : 'Email';
|
const ticketType = isDiscordTicket ? 'Discord' : 'Email';
|
||||||
await enqueueSend(logChan,
|
await enqueueSend(logChan,
|
||||||
@@ -373,7 +360,7 @@ async function handleCommand(interaction) {
|
|||||||
allowedMentions: { parse: ['users'] }
|
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) {
|
if (logChan) {
|
||||||
await enqueueSend(logChan, {
|
await enqueueSend(logChan, {
|
||||||
content: `Ticket ${interaction.channel} transferred from ${interaction.user.tag} to ${member.tag}.\nReason: ${reason}`,
|
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.channel.setParent(category.id, { lockPermissions: true });
|
||||||
await interaction.reply(`Moved ticket to **${category.name}**.`);
|
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) {
|
if (logChan) {
|
||||||
await enqueueSend(logChan,
|
await enqueueSend(logChan,
|
||||||
`Ticket ${interaction.channel} moved to category **${category.name}** by ${interaction.user.tag}`
|
`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
|
const transcriptChan = await clientRef.channels
|
||||||
.fetch(CONFIG.TRANSCRIPT_CHAN)
|
.fetch(CONFIG.TRANSCRIPT_CHANNEL_ID)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
|
||||||
if (transcriptChan) {
|
if (transcriptChan) {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ async function logTicketEvent(action, fields, interaction = null) {
|
|||||||
if (interaction?.user?.tag) {
|
if (interaction?.user?.tag) {
|
||||||
embed.setFooter({ text: 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 = {
|
module.exports = {
|
||||||
|
|||||||
74
utils.js
74
utils.js
@@ -264,83 +264,9 @@ function truncateEmbedDescription(str, max = 4096) {
|
|||||||
return s.length > max ? s.slice(0, max - 3) + '...' : s;
|
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 = {
|
module.exports = {
|
||||||
sanitizeEmbedText,
|
sanitizeEmbedText,
|
||||||
truncateEmbedDescription,
|
truncateEmbedDescription,
|
||||||
enforceEmbedLimit,
|
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
safeEqual,
|
safeEqual,
|
||||||
isStaff,
|
isStaff,
|
||||||
|
|||||||
Reference in New Issue
Block a user