/** * /response (saved tags) and its autocomplete. * * /response is itself a router over its subcommands: * send / create / edit / delete / list * The autocomplete handler also lives here since the only autocompleting * slash command is /response. */ const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, MessageFlags } = require('discord.js'); const { mongoose } = require('../../db-connection'); const { CONFIG } = require('../../config'); const { replaceVariables } = require('../../utils'); const { logError } = require('../../services/debugLog'); const Tag = mongoose.model('Tag'); const Ticket = mongoose.model('Ticket'); async function handleResponse(interaction) { const subcommand = interaction.options.getSubcommand(); const handler = RESPONSE_SUBCOMMANDS[subcommand]; if (!handler) return; try { await handler(interaction); } catch (err) { logError('response-command', err, interaction).catch(() => {}); const errorMsg = '❌ An error occurred while processing the response command.'; if (interaction.deferred) { await interaction.editReply(errorMsg); } else { await interaction.reply({ content: errorMsg, flags: MessageFlags.Ephemeral }); } } } async function handleResponseSend(interaction) { const name = interaction.options.getString('name'); const tag = await Tag.findOne({ name }).lean(); if (!tag) { return interaction.reply({ content: `❌ Tag "${name}" not found.`, flags: MessageFlags.Ephemeral }); } const ticket = await Ticket.findOne({ discordThreadId: interaction.channel.id }).lean(); const context = { ticket: ticket || {}, staff: { username: interaction.user.username, displayName: interaction.member?.displayName, mention: interaction.user.toString() }, guild: interaction.guild }; const content = replaceVariables(tag.content, context); await Tag.updateOne({ name }, { $inc: { useCount: 1 } }); // Tag bodies are staff-authored but may include variable substitutions from user/ticket data. // Disable mention parsing so a `@everyone` in a tag body never pings. await interaction.reply({ content, allowedMentions: { parse: [] } }); } async function handleResponseCreate(interaction) { const name = interaction.options.getString('name'); const content = interaction.options.getString('content'); try { await Tag.create({ name, content, createdBy: interaction.user.id }); await interaction.reply({ content: `✅ Tag "${name}" created successfully.`, flags: MessageFlags.Ephemeral }); } catch (err) { if (err.code === 11000 || err.message?.includes('duplicate')) { await interaction.reply({ content: `❌ Tag "${name}" already exists.`, flags: MessageFlags.Ephemeral }); } else { logError('tag-create', err, interaction).catch(() => {}); await interaction.reply({ content: '❌ Failed to create tag.', flags: MessageFlags.Ephemeral }); } } } async function handleResponseEdit(interaction) { const name = interaction.options.getString('name'); const content = interaction.options.getString('content'); try { const result = await Tag.updateOne({ name }, { $set: { content } }); if (result.matchedCount === 0) { await interaction.reply({ content: `❌ Tag "${name}" not found.`, flags: MessageFlags.Ephemeral }); } else { await interaction.reply({ content: `✅ Tag "${name}" updated successfully.`, flags: MessageFlags.Ephemeral }); } } catch (err) { logError('tag-edit', err, interaction).catch(() => {}); await interaction.reply({ content: '❌ Failed to edit tag.', flags: MessageFlags.Ephemeral }); } } async function handleResponseDelete(interaction) { const name = interaction.options.getString('name'); // Use :: delimiter so tag names with underscores parse correctly (Discord customId max 100 chars). const customId = `confirm_delete_tag::${name}`.slice(0, 100); const confirmRow = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId(customId) .setLabel('Yes, Delete Tag') .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setCustomId('cancel_delete_tag') .setLabel('Cancel') .setStyle(ButtonStyle.Secondary) ); return interaction.reply({ content: `⚠️ Are you sure you want to delete the tag "${name}"? This action cannot be undone.`, components: [confirmRow], flags: MessageFlags.Ephemeral }); } async function handleResponseList(interaction) { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const tags = await Tag.find().sort({ useCount: -1 }).select('name useCount').lean(); if (!tags || tags.length === 0) { return interaction.editReply({ content: '📋 No tags available.' }); } const embed = new EmbedBuilder() .setTitle('📋 Available Saved Responses') .setDescription( tags.map((t, i) => `${i + 1}. **${t.name}** (used ${t.useCount || 0}x)`).join('\n') ) .setColor(CONFIG.EMBED_COLOR_INFO) .setFooter({ text: `Total: ${tags.length} tags` }); await interaction.editReply({ embeds: [embed] }); } const RESPONSE_SUBCOMMANDS = { send: handleResponseSend, create: handleResponseCreate, edit: handleResponseEdit, delete: handleResponseDelete, list: handleResponseList }; /** Autocomplete handler. Currently only /response uses it. */ async function handleAutocomplete(interaction) { if (interaction.commandName !== 'response') return; const subcommand = interaction.options.getSubcommand(); if (!['send', 'edit', 'delete'].includes(subcommand)) return; const focusedValue = interaction.options.getFocused(); const tags = await Tag.find().sort({ name: 1 }).select('name').lean(); const filtered = tags .filter(t => t.name.toLowerCase().includes(focusedValue.toLowerCase())) .slice(0, 25) .map(t => ({ name: t.name, value: t.name })); await interaction.respond(filtered); } module.exports = { handleResponse, handleAutocomplete };