diff --git a/handlers/commands/index.js b/handlers/commands/index.js index 5546932..de7fe2e 100644 --- a/handlers/commands/index.js +++ b/handlers/commands/index.js @@ -18,8 +18,9 @@ const { EmbedBuilder, MessageFlags } = require('discord.js'); const { mongoose } = require('../../db-connection'); const { CONFIG } = require('../../config'); const { setNotifyDm } = require('../../services/staffSettings'); -const { enqueueMove, enqueueOverwrite, enqueueTopic, enqueueSend } = require('../../services/channelQueue'); -const { logTicketEvent } = require('../../services/debugLog'); +const { makeTicketName, resolveCreatorNickname } = require('../../services/tickets'); +const { enqueueRename, enqueueMove, enqueueOverwrite, enqueueTopic, enqueueSend } = require('../../services/channelQueue'); +const { logError, logTicketEvent } = require('../../services/debugLog'); const { findTicketForChannel } = require('../sharedHelpers'); const { requireStaffRole, fetchLoggingChannel } = require('./helpers'); @@ -54,16 +55,20 @@ async function handleAdd(interaction) { const ticket = await findTicketForChannel(interaction); if (!ticket) return; + // Defer up front: enqueueOverwrite serializes behind any pending rename/move + // on this channel and can exceed Discord's 3s interaction-token window. + await interaction.deferReply(); + try { await enqueueOverwrite(interaction.channel, user.id, { ViewChannel: true, SendMessages: true, ReadMessageHistory: true }); - await interaction.reply({ content: `Added ${user} to this ticket.`, allowedMentions: { parse: ['users'] } }); + await interaction.editReply({ content: `Added ${user} to this ticket.`, allowedMentions: { parse: ['users'] } }); } catch (err) { console.error('Add user error:', err); - await interaction.reply({ content: 'Failed to add user.', flags: MessageFlags.Ephemeral }); + await interaction.editReply({ content: 'Failed to add user.' }).catch(() => {}); } } @@ -72,12 +77,15 @@ async function handleRemove(interaction) { const ticket = await findTicketForChannel(interaction); if (!ticket) return; + // Defer up front — same reason as handleAdd. + await interaction.deferReply(); + try { await enqueueOverwrite(interaction.channel, user.id, null, 'delete'); - await interaction.reply({ content: `Removed ${user} from this ticket.`, allowedMentions: { parse: ['users'] } }); + await interaction.editReply({ content: `Removed ${user} from this ticket.`, allowedMentions: { parse: ['users'] } }); } catch (err) { console.error('Remove user error:', err); - await interaction.reply({ content: 'Failed to remove user.', flags: MessageFlags.Ephemeral }); + await interaction.editReply({ content: 'Failed to remove user.' }).catch(() => {}); } } @@ -94,16 +102,32 @@ async function handleTransfer(interaction) { return interaction.reply({ content: 'The target member must have the staff role.', flags: MessageFlags.Ephemeral }); } + // Defer before the DB write + rename so the interaction token survives. + await interaction.deferReply(); + try { const claimerLabel = guildMember.displayName || guildMember.user.username; await Ticket.updateOne( { gmailThreadId: ticket.gmailThreadId }, - { $set: { claimedBy: claimerLabel } } + { $set: { claimedBy: claimerLabel, claimerId: guildMember.id } } ); + ticket.claimedBy = claimerLabel; + ticket.claimerId = guildMember.id; + + // Rename the channel to reflect the new claimer — mirrors the /claim + // button flow (applyClaim in handlers/buttons.js). Picks the new + // claimer's emoji from STAFF_EMOJIS and uses the escalated-claimed + // variant when tier >= 1. + const claimerEmoji = CONFIG.STAFF_EMOJIS[guildMember.id] || CONFIG.CLAIMER_EMOJI_FALLBACK; + const creatorNickname = await resolveCreatorNickname(interaction.guild, ticket); + const tier = ticket.escalationTier ?? (ticket.escalated ? 1 : 0); + const state = tier >= 1 ? 'escalated-claimed' : 'claimed'; + enqueueRename(interaction.channel, makeTicketName(state, ticket, creatorNickname, claimerEmoji)) + .catch(err => logError('rename', err).catch(() => {})); // `reason` is staff-supplied freeform text; gate to user pings so @everyone in it can't mass-ping. - await interaction.reply({ + await interaction.editReply({ content: `Ticket transferred to ${member} by ${interaction.user}.\nReason: ${reason}`, allowedMentions: { parse: ['users'] } }); @@ -117,7 +141,7 @@ async function handleTransfer(interaction) { } } catch (err) { console.error('Transfer error:', err); - await interaction.reply({ content: 'Failed to transfer ticket.', flags: MessageFlags.Ephemeral }); + await interaction.editReply({ content: 'Failed to transfer ticket.' }).catch(() => {}); } } @@ -126,9 +150,13 @@ async function handleMove(interaction) { const ticket = await findTicketForChannel(interaction); if (!ticket) return; + // Defer up front — enqueueMove serializes behind any pending rename and + // setParent itself can take a moment on busy channels. + await interaction.deferReply(); + try { await enqueueMove(interaction.channel, category.id); - await interaction.reply(`Moved ticket to **${category.name}**.`); + await interaction.editReply(`Moved ticket to **${category.name}**.`); const logChan = await fetchLoggingChannel(interaction.client); if (logChan) { @@ -138,7 +166,7 @@ async function handleMove(interaction) { } } catch (err) { console.error('Move error:', err); - await interaction.reply({ content: 'Failed to move ticket.', flags: MessageFlags.Ephemeral }); + await interaction.editReply({ content: 'Failed to move ticket.' }).catch(() => {}); } } @@ -147,12 +175,15 @@ async function handleTopic(interaction) { const ticket = await findTicketForChannel(interaction); if (!ticket) return; + // Defer up front — enqueueTopic serializes behind any pending rename/move. + await interaction.deferReply(); + try { await enqueueTopic(interaction.channel, text); - await interaction.reply('Topic updated successfully.'); + await interaction.editReply('Topic updated successfully.'); } catch (err) { console.error('Topic error:', err); - await interaction.reply({ content: 'Failed to update topic.', flags: MessageFlags.Ephemeral }); + await interaction.editReply({ content: 'Failed to update topic.' }).catch(() => {}); } }