huge changes

This commit is contained in:
indifferentketchup
2026-04-07 01:43:06 -05:00
parent ca63ecbcfd
commit 69c247ed1b
37 changed files with 3468 additions and 169 deletions

View File

@@ -19,10 +19,13 @@ const { CONFIG } = require('../config');
const { canRename, makeTicketName, resolveCreatorNickname, minutesFromMs, getOrCreateTicketCategory, cleanupEmptyOverflowCategory, createDiscordTicketAsThread, checkTicketCreationRateLimit, getSenderLocal, toDiscordSafeName } = require('../services/tickets');
const { sendTicketClosedEmail } = require('../services/gmail');
const { getTicketActionRow } = require('../utils/ticketComponents');
const { sanitizeEmbedText, truncateEmbedDescription, truncateEmbedField, enforceEmbedLimit } = require('../utils');
const { setEmailRouting } = require('../services/guildSettings');
const { enqueueRename } = require('../services/channelQueue');
const { runEscalation, runDeescalation } = require('./commands');
const { trackInteraction, trackError } = require('./analytics');
const { pendingCloses } = require('./pendingCloses');
const { increment } = require('../services/patternStore');
const Ticket = mongoose.model('Ticket');
const Transcript = mongoose.model('Transcript');
@@ -131,7 +134,25 @@ async function handleButton(interaction) {
}
if (interaction.customId === 'confirm_close') {
return handleConfirmClose(interaction, ticket);
const timerSeconds = CONFIG.FORCE_CLOSE_TIMER;
if (pendingCloses.has(interaction.channel.id)) {
return interaction.reply({ content: 'A close is already pending for this ticket.', ephemeral: true });
}
await interaction.update({ content: `Closing ticket in ${timerSeconds} seconds. Use \`/cancel-close\` to abort.`, components: [] });
const timerId = setTimeout(async () => {
pendingCloses.delete(interaction.channel.id);
const freshTicket = await Ticket.findOne({ discordThreadId: interaction.channel.id }).lean();
if (!freshTicket || freshTicket.status === 'closed') return;
const { logTicketEvent } = require('../services/debugLog');
logTicketEvent('Force-close timer fired', [
{ name: 'Ticket', value: interaction.channel.name || interaction.channel.id },
{ name: 'Set by', value: interaction.user.tag },
{ name: 'Duration', value: `${timerSeconds}s` }
]).catch(() => {});
await handleConfirmClose(interaction, freshTicket);
}, timerSeconds * 1000);
pendingCloses.set(interaction.channel.id, { timeout: timerId, userId: interaction.user.id, username: interaction.user.tag });
return;
}
if (interaction.customId === 'cancel_close') {
@@ -294,6 +315,8 @@ async function handleClaim(interaction, ticket) {
}
if (isClaimed && !isClaimedByMe && !CONFIG.ALLOW_CLAIM_OVERWRITE) {
const { logSecurity } = require('../services/debugLog');
logSecurity('Unauthorized button attempt', interaction.user, interaction.customId).catch(() => {});
return interaction.reply({
content: `This ticket is already claimed by **${freshTicket.claimedBy}**.`,
ephemeral: true
@@ -307,6 +330,8 @@ async function handleClaim(interaction, ticket) {
);
freshTicket.claimedBy = claimerLabel;
freshTicket.claimerId = interaction.user.id;
increment('staff_claims', interaction.user.id, 'today');
increment('staff_claims', interaction.user.id, 'week');
// Resolve claimerEmoji from STAFF_EMOJIS map (fallback to CLAIMER_EMOJI_FALLBACK)
const claimerEmoji = CONFIG.STAFF_EMOJIS.get(interaction.user.id) || CONFIG.CLAIMER_EMOJI_FALLBACK;
@@ -358,6 +383,8 @@ async function handleClaim(interaction, ticket) {
.setColor(CONFIG.EMBED_COLOR_CLAIMED)
.setFooter({ text: `Claimed by ${claimerLabel}` });
await interaction.followUp({ embeds: [claimEmbed] });
const { addMemberToStaffThread } = require('../services/staffThread');
await addMemberToStaffThread(interaction.channel, interaction.user.id).catch(() => {});
} else {
// Unclaim
await Ticket.updateOne(
@@ -415,6 +442,10 @@ async function handleClaim(interaction, ticket) {
// --- CONFIRM CLOSE ---
async function handleConfirmClose(interaction, ticket) {
const closedAt = new Date();
increment('staff_closes', interaction.user.id, 'today');
if (!ticket.ticketTag) {
increment('untagged_closes', 'total', 'today');
}
try {
await interaction.update({ content: 'Archiving and closing...', components: [] });
} catch {
@@ -669,35 +700,64 @@ async function handleTicketModal(interaction) {
const displayName = interaction.member?.displayName || interaction.user.username;
// Welcome embed (dark grey #1e2124)
const welcomeEmbed = new EmbedBuilder()
.setDescription(CONFIG.TICKET_WELCOME_MESSAGE)
.setColor(CONFIG.EMBED_COLOR_INFO)
.setFooter({ text: 'Indifferent Broccoli Tickets' });
// Ticket details embed (dark) short labels, trimmed description
const descTrimmed = description.length > 500 ? description.slice(0, 497) + '…' : description;
const welcomeEmbed = new EmbedBuilder()
.setTitle("We got your ticket.")
.setDescription("We'll be with you as soon as possible.")
.setColor(5763719)
.setThumbnail("https://indifferentbroccoli.com/img/broccoli_shadow_square.png")
.setFooter({ text: "indifferent broccoli tickets (:|)", iconURL: "https://i.ibb.co/sJdytfFM/Untitled-design-6.png" });
const infoEmbed = new EmbedBuilder()
.setColor(CONFIG.EMBED_COLOR_INFO)
.setColor(5763719)
.setDescription(truncateEmbedDescription(
`**Account Email:**\n\`\`\`\n${sanitizeEmbedText(email)}\n\`\`\`\n` +
`**Game:**\n\`\`\`\n${sanitizeEmbedText(game) || "Not specified"}\n\`\`\`\n` +
`**What do you need help with?**\n\`\`\`\n${sanitizeEmbedText(descTrimmed)}\n\`\`\``
));
const resourcesEmbed = new EmbedBuilder()
.setTitle("We're ~~happy~~ indifferent to help. :indifferentbroccoli:")
.setDescription("Please feel free to add any additional information to the ticket, including recent changes to the server, if any.")
.setColor(5763719)
.addFields(
{ name: 'Email', value: email, inline: true },
{ name: 'Game', value: game || 'Not specified', inline: true },
{ name: 'Description', value: descTrimmed, inline: false }
{ name: "Check out our wiki for guides:", value: "[Indifferent Broccolipedia](https://wiki.indifferentbroccoli.com)", inline: false }
)
.setTimestamp();
.setFooter({ text: "indifferent broccoli tickets (:|)", iconURL: "https://i.ibb.co/sJdytfFM/Untitled-design-6.png" });
const actionRow = getTicketActionRow({ escalationTier: 0 });
const welcomeMsg = await channel.send({
content: `Hey There ${interaction.user} 🥦`,
embeds: [welcomeEmbed, infoEmbed],
components: [actionRow]
});
enforceEmbedLimit([welcomeEmbed, infoEmbed, resourcesEmbed]);
try {
const welcomeMsg = await channel.send({
content: `Hey There ${interaction.user} 🥦`,
embeds: [welcomeEmbed, infoEmbed, resourcesEmbed],
components: [actionRow]
});
await Ticket.updateOne(
{ discordThreadId: channel.id },
{ $set: { welcomeMessageId: welcomeMsg.id } }
);
await Ticket.updateOne(
{ discordThreadId: channel.id },
{ $set: { welcomeMessageId: welcomeMsg.id } }
);
} catch (err) {
console.error('welcomeMessageId-save', err);
}
const { createStaffThread } = require('../services/staffThread');
await createStaffThread(channel, interaction.client).catch(() => {});
if (CONFIG.PIN_INITIAL_MESSAGE_ENABLED && welcomeMsg) {
const { pinMessage } = require('../services/pinMessage');
await pinMessage(welcomeMsg, interaction.client).catch(() => {});
}
increment('user_tickets', interaction.user.id, 'today');
increment('user_tickets', interaction.user.id, 'week');
if (game) {
increment('game_tickets', game, 'today');
increment('game_tickets', game, 'week');
}
await interaction.deleteReply().catch(() => {});