/transfer: rename the channel + fix 10062 Unknown interaction errors
Two real bugs in handleTransfer plus a class issue across all the
channel-mod handlers.
/transfer didn't rename
handleTransfer set claimedBy but never called enqueueRename, so the
channel name stayed at whatever the previous claimer left it as.
/claim (applyClaim in handlers/buttons.js) does the rename via
makeTicketName + STAFF_EMOJIS; /transfer now does the same, plus
writes claimerId (was only writing claimedBy). Uses
'escalated-claimed' state when tier >= 1, 'claimed' otherwise.
DiscordAPIError 10062 (Unknown interaction)
handleAdd / handleRemove / handleTransfer / handleMove / handleTopic
all called interaction.reply() at the end after awaiting one or more
channelQueue ops. Those ops serialize behind any pending rename or
move on the same channel — easily exceeding Discord's 3s interaction-
token window. The reply then 404s with code 10062. Production logs
showed handleRemove failing this way (the visible 'Remove user
error: DiscordAPIError[10062]' lines); transfer had the same pattern.
Switch each handler to deferReply() up front + editReply() at the end
+ editReply() in the catch (with .catch(() => {}) to swallow the rare
case where even the deferred reply context is gone).
handleTransfer keeps the up-front isStaff role check as a reply()
because that path is synchronous and the token is fresh.
This commit is contained in:
@@ -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(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user