/** * Slash command and context-menu registration. */ const { REST, Routes, SlashCommandBuilder, PermissionFlagsBits, ChannelType, InteractionContextType, ApplicationIntegrationType, ContextMenuCommandBuilder, ApplicationCommandType } = require('discord.js'); const { CONFIG, TICKET_TAGS } = 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 3 (or one step if no tier chosen)') .setContexts([InteractionContextType.Guild]) .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) .addStringOption(opt => opt .setName('reason') .setDescription('Reason for escalating') .setMinLength(10) .setMaxLength(500) .setRequired(false) ) .addIntegerOption(opt => opt .setName('tier') .setDescription('Target tier (2 or 3); omit to escalate one step') .setRequired(false) .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('tag') .setDescription('Set ticket category (dropdown)') .setContexts([InteractionContextType.Guild]) .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) .addStringOption(o => o .setName('category') .setDescription('Ticket category tag') .setRequired(true) .addChoices(...(TICKET_TAGS || []).map(({ value, emoji, name }) => ({ name: `${emoji} ${name}`, value }))) ), 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('setup') .setDescription('Run the panel setup wizard (name, support role, category, transcript channel, panel channel)') .setContexts([InteractionContextType.Guild]) .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels), 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('email-routing') .setDescription('Switch where new email tickets are created: threads or category channels') .setContexts([InteractionContextType.Guild]) .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild), new SlashCommandBuilder() .setName('backup') .setDescription('Export full ticket list to a .txt file in the backup/export channel') .setContexts([InteractionContextType.Guild]) .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), new SlashCommandBuilder() .setName('export') .setDescription('Export tickets (optional filter and limit) to a .txt file in the backup/export channel') .setContexts([InteractionContextType.Guild]) .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .addStringOption(opt => opt .setName('status') .setDescription('Filter by status') .setRequired(false) .addChoices( { name: 'Open', value: 'open' }, { name: 'Closed', value: 'closed' } ) ) .addIntegerOption(opt => opt .setName('limit') .setDescription('Max number of tickets to export (default 500)') .setMinValue(1) .setMaxValue(5000) .setRequired(false) ), new SlashCommandBuilder() .setName('priority') .setDescription('Set the priority of this ticket') .setContexts([InteractionContextType.Guild]) .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) .addStringOption(opt => opt .setName('level') .setDescription('Priority level') .setRequired(true) .addChoices( { name: '🟢 Low', value: 'low' }, { name: '🟡 Normal', value: 'normal' }, { name: '🟠 Medium', value: 'medium' }, { name: '🔴 High', value: 'high' } ) ), new SlashCommandBuilder() .setName('search') .setDescription('Search for tickets') .setContexts([InteractionContextType.Guild]) .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) .addStringOption(opt => opt .setName('query') .setDescription('Search query (email, subject, or ticket number)') .setMinLength(2) .setMaxLength(100) .setRequired(true) ) .addStringOption(opt => opt .setName('status') .setDescription('Filter by status') .setRequired(false) .addChoices( { name: 'Open', value: 'open' }, { name: 'Closed', value: 'closed' }, { name: 'All', value: 'all' } ) ), new SlashCommandBuilder() .setName('stats') .setDescription('View bot statistics and analytics') .setContexts([InteractionContextType.Guild]) .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), new SlashCommandBuilder() .setName('accountinfo') .setDescription('Look up website account info by email or Discord user') .setContexts([InteractionContextType.Guild]) .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) .addSubcommand(sub => sub .setName('email') .setDescription('Look up by email address') .addStringOption(opt => opt.setName('email').setDescription('Account email').setRequired(true) ) ) .addSubcommand(sub => sub .setName('discord') .setDescription('Look up by Discord user') .addUserOption(opt => opt.setName('user').setDescription('Discord user').setRequired(true) ) ) ]; 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 };