QUAL-006 store ticket.creatorId on creation; legacy split-pop returned the
message ID for discord-msg-* tickets, breaking transcript DM, close
log, and channel rename for context-menu-created tickets. Adds the
field to the Ticket schema and writes a one-shot backfill script
(scripts/backfill-creatorId.js, dry-run by default).
QUEUE-001 add enqueueOverwrite + enqueueTopic to services/channelQueue.js
(chain on renameChains alongside enqueueMove). Migrate handleAdd /
handleRemove / handleMove / handleTopic so permissionOverwrites,
setParent, and setTopic no longer race pending renames or sends.
handleMove now uses the existing enqueueMove. Initial overwrites in
handleTicketModal stay inline; channel doesn't exist yet so no race.
DISCORD-001 replace ephemeral: true with flags: MessageFlags.Ephemeral across
broccolini-discord.js, handlers/sharedHelpers.js, handlers/buttons.js,
handlers/commands.js. runDeferred opts now take { flags } directly.
SEC-003 /gmailpoll min interval is 30s. Drop the 5s/10s slash-command
choices and clamp Math.max(30000, ms) in handleGmailPoll for
defense in depth.
QUAL-001 upgrade silent .catch(() => {}) on the lastActivity updateOne in
handlers/messages.js to log via logError, so transient Mongo errors
surface in the debug channel instead of disappearing.
QUAL-002 drop await from logError/logWarn calls in services/staffThread.js
and services/pinMessage.js — fire-and-forget per CLAUDE.md hard rule.
QUAL-003 wrap stray setTimeouts (handleConfirmCloseRequest force-close timer,
runFinalClose channel-delete + overflow-cleanup, checkAutoClose
delete-after-email) in trackTimeout via lazy require so they clear
on shutdown.
411 lines
14 KiB
JavaScript
411 lines
14 KiB
JavaScript
/**
|
|
* Slash command and context-menu registration.
|
|
*/
|
|
const {
|
|
REST,
|
|
Routes,
|
|
SlashCommandBuilder,
|
|
PermissionFlagsBits,
|
|
ChannelType,
|
|
InteractionContextType,
|
|
ApplicationIntegrationType,
|
|
ContextMenuCommandBuilder,
|
|
ApplicationCommandType
|
|
} = require('discord.js');
|
|
const { CONFIG } = require('../config');
|
|
|
|
async function registerCommands() {
|
|
if (!CONFIG.CLIENT_ID || !CONFIG.DISCORD_GUILD_ID) return;
|
|
|
|
const rest = new REST({ version: '10' }).setToken(CONFIG.DISCORD_TOKEN);
|
|
|
|
const commands = [
|
|
new SlashCommandBuilder()
|
|
.setName('escalate')
|
|
.setDescription('Escalate this ticket to tier 2 or tier 3 (always unclaims)')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('level')
|
|
.setDescription('Target escalation level')
|
|
.setRequired(true)
|
|
.addChoices(
|
|
{ name: 'Tier 2', value: '2' },
|
|
{ name: 'Tier 3', value: '3' }
|
|
)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('deescalate')
|
|
.setDescription('De-escalate this ticket (tier 3 → tier 2, or tier 2 → normal)')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('add')
|
|
.setDescription('Add a user to this ticket thread')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
|
.addUserOption(opt =>
|
|
opt.setName('user').setDescription('User to add').setRequired(true)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('remove')
|
|
.setDescription('Remove a user from this ticket thread')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
|
.addUserOption(opt =>
|
|
opt.setName('user').setDescription('User to remove').setRequired(true)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('transfer')
|
|
.setDescription('Transfer this ticket to another staff member')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
|
.addUserOption(opt =>
|
|
opt.setName('member').setDescription('Staff member to transfer to').setRequired(true)
|
|
)
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('reason')
|
|
.setDescription('Reason for transfer')
|
|
.setMinLength(10)
|
|
.setMaxLength(500)
|
|
.setRequired(false)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('move')
|
|
.setDescription('Move this ticket to another category')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels)
|
|
.addChannelOption(opt =>
|
|
opt
|
|
.setName('category')
|
|
.setDescription('Category to move to')
|
|
.setRequired(true)
|
|
.addChannelTypes(ChannelType.GuildCategory)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('force-close')
|
|
.setDescription('Force close this ticket without confirmation')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('topic')
|
|
.setDescription('Set the topic/description for this ticket')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('text')
|
|
.setDescription('Topic text')
|
|
.setMinLength(5)
|
|
.setMaxLength(1024)
|
|
.setRequired(true)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('response')
|
|
.setDescription('Saved response tags (custom templates)')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
|
.addSubcommand(sub =>
|
|
sub
|
|
.setName('send')
|
|
.setDescription('Send a saved response')
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('name')
|
|
.setDescription('Tag name')
|
|
.setRequired(true)
|
|
.setAutocomplete(true)
|
|
)
|
|
)
|
|
.addSubcommand(sub =>
|
|
sub
|
|
.setName('create')
|
|
.setDescription('Create a new saved response')
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('name')
|
|
.setDescription('Tag name (unique)')
|
|
.setMinLength(2)
|
|
.setMaxLength(50)
|
|
.setRequired(true)
|
|
)
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('content')
|
|
.setDescription('Tag content (supports variables)')
|
|
.setMinLength(10)
|
|
.setMaxLength(2000)
|
|
.setRequired(true)
|
|
)
|
|
)
|
|
.addSubcommand(sub =>
|
|
sub
|
|
.setName('edit')
|
|
.setDescription('Edit an existing saved response')
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('name')
|
|
.setDescription('Tag name')
|
|
.setRequired(true)
|
|
.setAutocomplete(true)
|
|
)
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('content')
|
|
.setDescription('New tag content')
|
|
.setMinLength(10)
|
|
.setMaxLength(2000)
|
|
.setRequired(true)
|
|
)
|
|
)
|
|
.addSubcommand(sub =>
|
|
sub
|
|
.setName('delete')
|
|
.setDescription('Delete a saved response')
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('name')
|
|
.setDescription('Tag name')
|
|
.setRequired(true)
|
|
.setAutocomplete(true)
|
|
)
|
|
)
|
|
.addSubcommand(sub =>
|
|
sub.setName('list').setDescription('List all saved responses')
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('help')
|
|
.setDescription('Show all available commands and information')
|
|
.setIntegrationTypes([
|
|
ApplicationIntegrationType.GuildInstall,
|
|
ApplicationIntegrationType.UserInstall
|
|
])
|
|
.setContexts([
|
|
InteractionContextType.Guild,
|
|
InteractionContextType.BotDM,
|
|
InteractionContextType.PrivateChannel
|
|
]),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('panel')
|
|
.setDescription('Create a ticket panel for users to open Discord tickets')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels)
|
|
.addChannelOption(opt =>
|
|
opt
|
|
.setName('channel')
|
|
.setDescription('Channel to send the panel to')
|
|
.setRequired(true)
|
|
.addChannelTypes(ChannelType.GuildText)
|
|
)
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('type')
|
|
.setDescription('Panel type: thread only, category only, or both')
|
|
.setRequired(false)
|
|
.addChoices(
|
|
{ name: 'Thread', value: 'thread' },
|
|
{ name: 'Category', value: 'category' },
|
|
{ name: 'Both (thread + category)', value: 'both' }
|
|
)
|
|
)
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('title')
|
|
.setDescription('Panel title')
|
|
.setMinLength(5)
|
|
.setMaxLength(100)
|
|
.setRequired(false)
|
|
)
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('description')
|
|
.setDescription('Panel description')
|
|
.setMinLength(10)
|
|
.setMaxLength(500)
|
|
.setRequired(false)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('notifydm')
|
|
.setDescription('Toggle DM notifications when your ticket receives a customer reply.')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('setting')
|
|
.setDescription('on or off')
|
|
.setRequired(true)
|
|
.addChoices(
|
|
{ name: 'on', value: 'on' },
|
|
{ name: 'off', value: 'off' }
|
|
)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('closetimer')
|
|
.setDescription('Set the force-close countdown duration')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('seconds')
|
|
.setDescription('Countdown duration')
|
|
.setRequired(true)
|
|
.addChoices(
|
|
{ name: '5s', value: '5' },
|
|
{ name: '10s', value: '10' },
|
|
{ name: '30s', value: '30' },
|
|
{ name: '45s', value: '45' },
|
|
{ name: '1m', value: '60' },
|
|
{ name: '2m', value: '120' },
|
|
{ name: '3m', value: '180' },
|
|
{ name: '4m', value: '240' },
|
|
{ name: '5m', value: '300' },
|
|
{ name: '10m', value: '600' }
|
|
)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('staffthread')
|
|
.setDescription('Manage staff discussion threads on ticket channels')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
|
.addSubcommand(sub =>
|
|
sub.setName('toggle').setDescription('Toggle staff threads on/off')
|
|
)
|
|
.addSubcommand(sub =>
|
|
sub
|
|
.setName('name')
|
|
.setDescription('Set the staff thread name')
|
|
.addStringOption(opt =>
|
|
opt.setName('thread_name').setDescription('Thread name').setMaxLength(100).setRequired(true)
|
|
)
|
|
)
|
|
.addSubcommand(sub =>
|
|
sub
|
|
.setName('autorole')
|
|
.setDescription('Toggle auto-adding role members to staff thread')
|
|
.addBooleanOption(opt =>
|
|
opt.setName('enabled').setDescription('Enable or disable').setRequired(true)
|
|
)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('pinmessages')
|
|
.setDescription('Manage auto-pinning of ticket messages')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
|
.addSubcommand(sub =>
|
|
sub
|
|
.setName('initial')
|
|
.setDescription('Toggle auto-pin of welcome message')
|
|
.addBooleanOption(opt =>
|
|
opt.setName('enabled').setDescription('Enable or disable').setRequired(true)
|
|
)
|
|
)
|
|
.addSubcommand(sub =>
|
|
sub
|
|
.setName('escalation')
|
|
.setDescription('Toggle auto-pin of escalation messages')
|
|
.addBooleanOption(opt =>
|
|
opt.setName('enabled').setDescription('Enable or disable').setRequired(true)
|
|
)
|
|
)
|
|
.addSubcommand(sub =>
|
|
sub
|
|
.setName('suppress')
|
|
.setDescription('Toggle suppression of pin system messages')
|
|
.addBooleanOption(opt =>
|
|
opt.setName('enabled').setDescription('Enable or disable').setRequired(true)
|
|
)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('gmailpoll')
|
|
.setDescription('Set the Gmail poll interval')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
|
.addStringOption(opt =>
|
|
opt
|
|
.setName('interval')
|
|
.setDescription('Poll interval')
|
|
.setRequired(true)
|
|
.addChoices(
|
|
{ name: '30s', value: '30' },
|
|
{ name: '45s', value: '45' },
|
|
{ name: '1m', value: '60' },
|
|
{ name: '2m', value: '120' },
|
|
{ name: '3m', value: '180' },
|
|
{ name: '4m', value: '240' },
|
|
{ name: '5m', value: '300' },
|
|
{ name: '10m', value: '600' }
|
|
)
|
|
),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('cancel-close')
|
|
.setDescription('Cancel a pending force-close countdown')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
|
|
|
|
new SlashCommandBuilder()
|
|
.setName('signature')
|
|
.setDescription('Set your personal email signature (valediction, display name, tagline)')
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
|
];
|
|
|
|
const contextMenuCommands = [
|
|
new ContextMenuCommandBuilder()
|
|
.setName('Create Ticket From Message')
|
|
.setType(ApplicationCommandType.Message)
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
|
|
|
|
new ContextMenuCommandBuilder()
|
|
.setName('View User Tickets')
|
|
.setType(ApplicationCommandType.User)
|
|
.setContexts([InteractionContextType.Guild])
|
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall])
|
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
|
];
|
|
|
|
await rest.put(
|
|
Routes.applicationGuildCommands(CONFIG.CLIENT_ID, CONFIG.DISCORD_GUILD_ID),
|
|
{ body: [...commands.map(cmd => cmd.toJSON()), ...contextMenuCommands.map(cmd => cmd.toJSON())] }
|
|
);
|
|
|
|
console.log(`✅ Registered ${commands.length} slash commands + ${contextMenuCommands.length} context menu commands`);
|
|
}
|
|
|
|
module.exports = { registerCommands };
|