/** * Button interaction handlers – claim, close, priority, tag delete, * open-ticket panel button, and ticket_modal submission. */ const { ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder, EmbedBuilder, PermissionFlagsBits, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); const { mongoose } = require('../db-connection'); const { CONFIG } = require('../config'); const { makeTicketName, resolveCreatorNickname, getOrCreateTicketCategory, cleanupEmptyOverflowCategory, checkTicketCreationRateLimit, toDiscordSafeName } = require('../services/tickets'); const { sendTicketClosedEmail } = require('../services/gmail'); const { getTicketActionRow } = require('../utils/ticketComponents'); const { sanitizeEmbedText, truncateEmbedDescription, enforceEmbedLimit } = require('../utils'); const { enqueueRename, enqueueSend } = require('../services/channelQueue'); const { runEscalation, runDeescalation } = require('./commands'); const { pendingCloses } = require('./pendingCloses'); const { logError } = require('../services/debugLog'); const Ticket = mongoose.model('Ticket'); const Transcript = mongoose.model('Transcript'); const Tag = mongoose.model('Tag'); /** * Main button/modal handler – called from interactionCreate. */ async function handleButton(interaction) { // --- "Open Ticket" panel buttons → show modal --- if (interaction.customId === 'open_ticket' || interaction.customId === 'open_ticket_thread' || interaction.customId === 'open_ticket_channel') { const modalCustomId = interaction.customId === 'open_ticket' ? 'ticket_modal' : interaction.customId === 'open_ticket_thread' ? 'ticket_modal_thread' : 'ticket_modal_channel'; const modal = new ModalBuilder() .setCustomId(modalCustomId) .setTitle('Please Enter Your Information'); const emailInput = new TextInputBuilder() .setCustomId('ticket_email') .setLabel('Account Email:') .setStyle(TextInputStyle.Short) .setPlaceholder('Example: broccoli@indifferentbroccoli.com') .setRequired(true) .setMaxLength(100); const gameInput = new TextInputBuilder() .setCustomId('ticket_game') .setLabel('What game do you need help with?') .setStyle(TextInputStyle.Short) .setPlaceholder('Example: Project Zomboid, Minecraft') .setRequired(true) .setMaxLength(100); const descriptionInput = new TextInputBuilder() .setCustomId('ticket_description') .setLabel('What do you need help with?') .setStyle(TextInputStyle.Paragraph) .setPlaceholder("Example: I can't connect to my server.") .setRequired(true) .setMaxLength(1000); modal.addComponents( new ActionRowBuilder().addComponents(emailInput), new ActionRowBuilder().addComponents(gameInput), new ActionRowBuilder().addComponents(descriptionInput) ); return await interaction.showModal(modal); } // --- Ticket-scoped buttons (need ticket lookup) --- const ticket = await Ticket.findOne({ discordThreadId: interaction.channel.id }).lean(); if (!ticket) { return interaction.reply({ content: 'This channel is not linked to a ticket, or the ticket could not be found.', ephemeral: true }); } // --- CLAIM / UNCLAIM --- if (interaction.customId === 'claim_ticket') { return handleClaim(interaction, ticket); } // --- CLOSE --- if (interaction.customId === 'close_ticket') { const isEmailTicket = ticket.gmailThreadId && !ticket.gmailThreadId.startsWith('discord-'); const confirmRow = new ActionRowBuilder(); if (isEmailTicket) { confirmRow.addComponents( new ButtonBuilder() .setCustomId('confirm_close_with_email') .setLabel('Confirm Close With Email') .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setCustomId('confirm_close_no_email') .setLabel('Confirm Close Without Email') .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setCustomId('cancel_close') .setLabel('Cancel') .setStyle(ButtonStyle.Secondary) ); } else { confirmRow.addComponents( new ButtonBuilder() .setCustomId('confirm_close') .setLabel('Confirm Close') .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setCustomId('cancel_close') .setLabel('Cancel') .setStyle(ButtonStyle.Secondary) ); } return interaction.reply({ content: 'Are you sure you want to close this ticket?', components: [confirmRow] }); } if ( interaction.customId === 'confirm_close' || interaction.customId === 'confirm_close_with_email' || interaction.customId === 'confirm_close_no_email' ) { const sendEmail = interaction.customId !== 'confirm_close_no_email'; const timerSeconds = CONFIG.FORCE_CLOSE_TIMER; if (pendingCloses.has(interaction.channel.id)) { return interaction.reply({ content: 'A close is already pending for this ticket.', ephemeral: true }); } const cancelRow = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('cancel_close') .setLabel('Cancel Close') .setStyle(ButtonStyle.Secondary) ); await interaction.update({ content: `Closing ticket in ${timerSeconds} seconds.`, components: [cancelRow] }); const timerId = setTimeout(async () => { const pending = pendingCloses.get(interaction.channel.id); pendingCloses.delete(interaction.channel.id); const freshTicket = await Ticket.findOne({ discordThreadId: interaction.channel.id }).lean(); if (!freshTicket || freshTicket.status === 'closed') return; const { logTicketEvent } = require('../services/debugLog'); logTicketEvent('Force-close timer fired', [ { name: 'Ticket', value: interaction.channel.name || interaction.channel.id }, { name: 'Set by', value: interaction.user.tag }, { name: 'Duration', value: `${timerSeconds}s` } ]).catch(() => {}); const effectiveSendEmail = pending?.sendEmail ?? true; await handleConfirmClose(interaction, freshTicket, effectiveSendEmail); }, timerSeconds * 1000); pendingCloses.set(interaction.channel.id, { timeout: timerId, userId: interaction.user.id, username: interaction.user.tag, sendEmail }); return; } if (interaction.customId === 'cancel_close') { const pending = pendingCloses.get(interaction.channel.id); if (pending) { clearTimeout(pending.timeout); pendingCloses.delete(interaction.channel.id); } return interaction.update({ content: 'Close cancelled.', components: [] }); } // --- ESCALATE (prompt for tier 2 or 3) --- if (interaction.customId === 'escalate_ticket') { const currentTier = ticket.escalationTier ?? (ticket.escalated ? 1 : 0); if (currentTier >= 2) { return interaction.reply({ content: 'This ticket is already at tier 3 support.', ephemeral: true }); } const escalateButtons = []; if (currentTier < 1) { escalateButtons.push( new ButtonBuilder() .setCustomId('escalate_to_tier2') .setLabel('To Tier 2') .setStyle(ButtonStyle.Secondary) ); } if (currentTier < 2) { escalateButtons.push( new ButtonBuilder() .setCustomId('escalate_to_tier3') .setLabel('To Tier 3') .setStyle(ButtonStyle.Secondary) ); } const choiceRow = new ActionRowBuilder().addComponents(escalateButtons); return interaction.reply({ content: 'Escalate to which tier?', components: [choiceRow], ephemeral: true }); } if (interaction.customId === 'escalate_to_tier2') { const currentTier = ticket.escalationTier ?? (ticket.escalated ? 1 : 0); if (currentTier >= 1) { return interaction.reply({ content: 'This ticket is already at tier 2.', ephemeral: true }); } const categoryId = ticket.gmailThreadId.startsWith('discord-') ? CONFIG.DISCORD_ESCALATED2_CHANNEL_ID : CONFIG.EMAIL_ESCALATED2_CHANNEL_ID; if (!categoryId && !interaction.channel.isThread()) { return interaction.reply({ content: 'Tier 2 (ESCALATED2) is not configured for this ticket type.', ephemeral: true }); } try { await interaction.deferReply(); await runEscalation(interaction, ticket, 1, null); } catch (err) { logError('escalate-button-tier2', err, interaction).catch(() => {}); await interaction.editReply({ content: 'Failed to escalate to tier 2.' }).catch(() => interaction.followUp({ content: 'Failed to escalate to tier 2.', ephemeral: true }).catch(() => {}) ); } return; } if (interaction.customId === 'escalate_to_tier3') { const currentTier = ticket.escalationTier ?? (ticket.escalated ? 1 : 0); if (currentTier >= 2) { return interaction.reply({ content: 'This ticket is already at tier 3.', ephemeral: true }); } const categoryId = ticket.gmailThreadId.startsWith('discord-') ? CONFIG.DISCORD_ESCALATED3_CHANNEL_ID : CONFIG.EMAIL_ESCALATED3_CHANNEL_ID; if (!categoryId && !interaction.channel.isThread()) { return interaction.reply({ content: 'Tier 3 (ESCALATED3) is not configured for this ticket type.', ephemeral: true }); } try { await interaction.deferReply(); await runEscalation(interaction, ticket, 2, null); } catch (err) { logError('escalate-button-tier3', err, interaction).catch(() => {}); await interaction.editReply({ content: 'Failed to escalate to tier 3.' }).catch(() => interaction.followUp({ content: 'Failed to escalate to tier 3.', ephemeral: true }).catch(() => {}) ); } return; } // --- DEESCALATE --- if (interaction.customId === 'deescalate_ticket') { const currentTier = ticket.escalationTier ?? (ticket.escalated ? 1 : 0); if (currentTier === 0) { return interaction.reply({ content: 'This ticket is not escalated.', ephemeral: true }); } try { await interaction.deferReply({ ephemeral: true }); await runDeescalation(interaction, ticket); } catch (err) { logError('deescalate-button', err, interaction).catch(() => {}); await interaction.editReply({ content: 'Failed to deescalate this ticket.' }).catch(() => interaction.followUp({ content: 'Failed to deescalate this ticket.', ephemeral: true }).catch(() => {}) ); } return; } // --- TAG DELETE CONFIRM --- if (interaction.customId.startsWith('confirm_delete_tag::')) { const tagName = interaction.customId.slice('confirm_delete_tag::'.length); try { const result = await Tag.deleteOne({ name: tagName }); if (result.deletedCount === 0) { await interaction.update({ content: `❌ Tag "${tagName}" not found.`, components: [] }); } else { await interaction.update({ content: `✅ Tag "${tagName}" deleted successfully.`, components: [] }); } } catch (err) { logError('tag-delete-confirm', err, interaction).catch(() => {}); await interaction.update({ content: '❌ Failed to delete tag.', components: [] }); } } if (interaction.customId === 'cancel_delete_tag') { return interaction.update({ content: 'Tag deletion cancelled.', components: [] }); } // Priority is set via /priority slash command only; no priority buttons in tickets. } // --- CLAIM LOGIC --- async function handleClaim(interaction, ticket) { const freshTicket = await Ticket.findOne({ gmailThreadId: ticket.gmailThreadId }).lean(); if (!freshTicket) { return interaction.reply({ content: 'Ticket data missing.', ephemeral: true }); } const isClaimed = !!freshTicket.claimedBy; const claimerLabel = interaction.member?.displayName || interaction.user.username; const guild = interaction.guild; const isClaimedByMe = freshTicket.claimedBy === claimerLabel; const [row0] = interaction.message.components; if (!row0) { return interaction.reply({ content: 'No components to update.', ephemeral: true }); } const row = ActionRowBuilder.from(row0); const [btnClose, btnClaim] = row.components; if (!btnClose || !btnClaim) { return interaction.reply({ content: 'Buttons missing.', ephemeral: true }); } if (isClaimed && !isClaimedByMe && !CONFIG.ALLOW_CLAIM_OVERWRITE) { return interaction.reply({ content: `This ticket is already claimed by **${freshTicket.claimedBy}**.`, ephemeral: true }); } if (!isClaimed || (isClaimed && !isClaimedByMe && CONFIG.ALLOW_CLAIM_OVERWRITE)) { await Ticket.updateOne( { gmailThreadId: freshTicket.gmailThreadId }, { $set: { claimedBy: claimerLabel, claimerId: interaction.user.id } } ); freshTicket.claimedBy = claimerLabel; freshTicket.claimerId = interaction.user.id; const claimerEmoji = CONFIG.STAFF_EMOJIS[interaction.user.id] || CONFIG.CLAIMER_EMOJI_FALLBACK; const creatorNickname = await resolveCreatorNickname(guild, freshTicket); const state = freshTicket.escalated ? 'escalated-claimed' : 'claimed'; const newName = makeTicketName(state, freshTicket, creatorNickname, claimerEmoji); enqueueRename(interaction.channel, newName).catch(err => logError('rename', err).catch(() => {})); const label = `Unclaim (${claimerLabel})`; btnClose .setCustomId('close_ticket') .setLabel(CONFIG.BUTTON_LABEL_CLOSE) .setEmoji(CONFIG.BUTTON_EMOJI_CLOSE) .setStyle(ButtonStyle.Secondary) .setDisabled(false); btnClaim .setCustomId('claim_ticket') .setEmoji(CONFIG.BUTTON_EMOJI_UNCLAIM) .setStyle(ButtonStyle.Secondary) .setDisabled(false) .setLabel(label); await interaction.update({ components: [row] }); const claimText = CONFIG.TICKET_CLAIMED_MESSAGE .replace(/\{staff_mention\}/g, interaction.user.toString()) .replace(/\{staff_name\}/g, interaction.member?.displayName || interaction.user.username); const claimEmbed = new EmbedBuilder() .setTitle('✅ Ticket Claimed') .setDescription(claimText) .setColor(CONFIG.EMBED_COLOR_CLAIMED) .setFooter({ text: `Claimed by ${claimerLabel}` }); await interaction.followUp({ embeds: [claimEmbed] }); const { addMemberToStaffThread } = require('../services/staffThread'); await addMemberToStaffThread(interaction.channel, interaction.user.id).catch(() => {}); } else { // Unclaim await Ticket.updateOne( { gmailThreadId: freshTicket.gmailThreadId }, { $set: { claimedBy: null, claimerId: null } } ); freshTicket.claimedBy = null; freshTicket.claimerId = null; const creatorNicknameUnclaim = await resolveCreatorNickname(guild, freshTicket); const unclaimState = (freshTicket.escalationTier ?? 0) >= 1 ? 'escalated' : 'unclaimed'; enqueueRename(interaction.channel, makeTicketName(unclaimState, freshTicket, creatorNicknameUnclaim)).catch(err => logError('rename', err).catch(() => {})); btnClose .setCustomId('close_ticket') .setLabel(CONFIG.BUTTON_LABEL_CLOSE) .setEmoji(CONFIG.BUTTON_EMOJI_CLOSE) .setStyle(ButtonStyle.Secondary) .setDisabled(false); btnClaim .setCustomId('claim_ticket') .setEmoji(CONFIG.BUTTON_EMOJI_CLAIM) .setStyle(ButtonStyle.Secondary) .setDisabled(false) .setLabel(CONFIG.BUTTON_LABEL_CLAIM); await interaction.update({ components: [row] }); const unclaimText = CONFIG.TICKET_UNCLAIMED_MESSAGE .replace(/\{staff_mention\}/g, interaction.user.toString()) .replace(/\{staff_name\}/g, interaction.member?.displayName || interaction.user.username); const unclaimEmbed = new EmbedBuilder() .setTitle('🔓 Ticket Unclaimed') .setDescription(unclaimText) .setColor(0x808080) .setFooter({ text: `Unclaimed by ${claimerLabel}` }); await interaction.followUp({ embeds: [unclaimEmbed] }); } } // --- CONFIRM CLOSE --- async function handleConfirmClose(interaction, ticket, sendEmail = true) { const closedAt = new Date(); try { await interaction.update({ content: 'Archiving and closing...', components: [] }); } catch { // Already acknowledged – fall back to editReply await interaction.editReply({ content: 'Archiving and closing...', components: [] }).catch(() => {}); } try { const messages = await interaction.channel.messages.fetch({ limit: 100 }); const log = `TRANSCRIPT: ${ticket.subject}\nUser: ${ticket.senderEmail}\n---\n` + messages .reverse() .map( m => `[${m.createdAt.toLocaleString()}] ${m.author.tag}: ${m.cleanContent}` ) .join('\n'); const file = new AttachmentBuilder(Buffer.from(log), { name: `transcript-${interaction.channel.name}.txt` }); const channelName = interaction.channel.name; const opened = new Date(ticket.createdAt); const openedStr = opened.toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true, timeZoneName: 'short' }); const closedStr = closedAt.toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true, timeZoneName: 'short' }); // In-ticket message before transcript is posted (Discord close message) const discordCloseContent = CONFIG.DISCORD_CLOSE_MESSAGE; await enqueueSend(interaction.channel, discordCloseContent); const transcriptChan = await interaction.client.channels .fetch(CONFIG.TRANSCRIPT_CHAN) .catch(() => null); let transcriptMsg = null; const transcriptContent = CONFIG.DISCORD_TRANSCRIPT_MESSAGE .replace(/\{channel_name\}/g, channelName) .replace(/\{email\}/g, ticket.senderEmail || '') .replace(/\{date_opened\}/g, openedStr) .replace(/\{date_closed\}/g, closedStr) + `\n\nDate Opened: ${openedStr}\nDate Closed: ${closedStr}`; if (transcriptChan) { transcriptMsg = await enqueueSend(transcriptChan, { content: transcriptContent, files: [file] }); } // DM the transcript to the ticket creator (Discord-originated tickets). // Gated because many users have DMs from server members disabled — the send // then 50007s and generates noise. Default off; enable via env when desired. if (CONFIG.TRANSCRIPT_DM_TO_CREATOR && ticket.gmailThreadId?.startsWith('discord-')) { const creatorId = ticket.gmailThreadId.split('-').pop(); try { const creator = await interaction.client.users.fetch(creatorId); const dmFile = new AttachmentBuilder(Buffer.from(log), { name: `transcript-${channelName}.txt` }); const dmContent = CONFIG.DISCORD_TRANSCRIPT_MESSAGE .replace(/\{channel_name\}/g, channelName) .replace(/\{email\}/g, ticket.senderEmail || '') .replace(/\{date_opened\}/g, openedStr) .replace(/\{date_closed\}/g, closedStr); await creator.send({ content: dmContent, files: [dmFile] }); } catch (dmErr) { // 50007 = "Cannot send messages to this user" — user has DMs off. Expected; ignore. if (dmErr?.code !== 50007) { logError('transcript-dm', dmErr).catch(() => {}); } } } const logChan = await interaction.client.channels .fetch(CONFIG.LOG_CHAN) .catch(() => null); if (logChan) { const closerMention = interaction.user.toString(); const closerDisplayName = interaction.member?.displayName || interaction.user.username; let logMsg; if (ticket.gmailThreadId?.startsWith('discord-')) { const creatorId = ticket.gmailThreadId.split('-').pop(); try { const creator = await interaction.client.users.fetch(creatorId); const creatorMention = creator.toString(); logMsg = `Closed ${creatorMention}'s **${channelName}** by ${closerMention} (${closerDisplayName})`; } catch { logMsg = `Closed **${channelName}** by ${closerMention} (${closerDisplayName})`; } } else { logMsg = `Closed **${channelName}** (${ticket.senderEmail}) by ${closerMention} (${closerDisplayName})`; } await enqueueSend(logChan, logMsg); } const closerDisplayName = interaction.member?.displayName || interaction.user.username; if (sendEmail && !ticket.gmailThreadId?.startsWith('discord-')) { await sendTicketClosedEmail(ticket, closerDisplayName, interaction.user.id); } await Ticket.updateOne( { gmailThreadId: ticket.gmailThreadId }, { $set: { discordThreadId: null, status: 'closed' } } ); if (transcriptMsg?.id) { await Transcript.create({ gmailThreadId: ticket.gmailThreadId, transcriptMessageId: transcriptMsg.id, createdAt: new Date() }); } const parentCatId = ticket.parentCategoryId; const guildRef = interaction.guild; setTimeout( () => interaction.channel.delete().catch(() => {}), 5000 ); setTimeout(() => { (async () => { if (parentCatId && guildRef) { await cleanupEmptyOverflowCategory(guildRef, parentCatId, CONFIG.TICKET_CATEGORY_NAME); } })(); }, 6000); } catch (e) { console.error('Close ticket error:', e); } } /** * Handle the ticket_modal submission (from the open-ticket panel button). */ async function handleTicketModal(interaction) { await interaction.deferReply({ ephemeral: true }); const email = interaction.fields.getTextInputValue('ticket_email').trim().toLowerCase(); const game = interaction.fields.getTextInputValue('ticket_game').trim(); const description = interaction.fields.getTextInputValue('ticket_description'); const subject = game ? `[${game}] ${description.slice(0, 60)}` : description.slice(0, 80); const priority = CONFIG.PRIORITY_ENABLED ? CONFIG.DEFAULT_PRIORITY : 'normal'; const rateLimit = checkTicketCreationRateLimit(interaction.user.id); if (!rateLimit.allowed) { const mins = Math.ceil((rateLimit.retryAfterMs || 0) / 60000); return interaction.editReply(`You can only create ${CONFIG.RATE_LIMIT_TICKETS_PER_USER} ticket(s) per ${CONFIG.RATE_LIMIT_WINDOW_MINUTES} minutes. Try again in ${mins} minute(s).`); } try { const guild = interaction.guild; const lastTicket = await Ticket.findOne().sort({ ticketNumber: -1 }).select('ticketNumber').lean(); const ticketNumber = (lastTicket?.ticketNumber || 0) + 1; const creatorNicknameModal = interaction.member?.displayName || interaction.user.username; const unclaimedName = toDiscordSafeName(`unclaimed-${creatorNicknameModal}-${ticketNumber}`); let channel; let parentCategoryIdForTicket = null; let parentId; try { parentId = await getOrCreateTicketCategory( guild, CONFIG.DISCORD_TICKET_CATEGORY_ID, CONFIG.TICKET_CATEGORY_NAME ); } catch (err) { console.error('getOrCreateTicketCategory (ticket modal):', err); return interaction.editReply('Discord ticket category could not be resolved. Contact an administrator.'); } parentCategoryIdForTicket = parentId; try { // TODO(queue-migrate): initial permissionOverwrites here are fine since the channel is just being created, but any later permissionOverwrites mutation on this channel should go through channelQueue. channel = await guild.channels.create({ name: unclaimedName, type: ChannelType.GuildText, parent: parentId, permissionOverwrites: [ { id: guild.id, deny: [PermissionFlagsBits.ViewChannel] }, { id: interaction.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory] }, { id: CONFIG.ROLE_ID_TO_PING, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory] } ] }); } catch (err) { console.error('guild.channels.create (ticket modal):', err); return interaction.editReply('Failed to create ticket channel. Contact an administrator.'); } const gmailThreadId = `discord-${Date.now()}-${interaction.user.id}`; const now = new Date(); await Ticket.create({ gmailThreadId, discordThreadId: channel.id, senderEmail: email, subject, game: game || null, createdAt: now, status: 'open', ticketNumber, priority, lastActivity: now, parentCategoryId: parentCategoryIdForTicket }); const descTrimmed = description.length > 500 ? description.slice(0, 497) + '…' : description; const welcomeEmbed = new EmbedBuilder() .setTitle("We got your ticket.") .setDescription("We'll be with you as soon as possible.") .setColor(5763719) .setThumbnail("https://indifferentbroccoli.com/img/broccoli_shadow_square.png"); const infoEmbed = new EmbedBuilder() .setColor(5763719) .setDescription(truncateEmbedDescription( `**Account Email:**\n\`\`\`\n${sanitizeEmbedText(email)}\n\`\`\`\n` + `**Game:**\n\`\`\`\n${sanitizeEmbedText(game) || "Not specified"}\n\`\`\`\n` + `**What do you need help with?**\n\`\`\`\n${sanitizeEmbedText(descTrimmed)}\n\`\`\`` )); const resourcesEmbed = new EmbedBuilder() .setTitle("We're ~~happy~~ indifferent to help. :indifferentbroccoli:") .setDescription("Please feel free to add any additional information to the ticket, including recent changes to the server, if any.") .setColor(5763719) .addFields( { name: "Check out our wiki for guides:", value: "[Indifferent Broccolipedia](https://wiki.indifferentbroccoli.com)", inline: false } ) .setFooter({ text: "indifferent broccoli tickets (:|)", iconURL: "https://i.ibb.co/sJdytfFM/Untitled-design-6.png" }); const actionRow = getTicketActionRow({ escalationTier: 0 }); enforceEmbedLimit([welcomeEmbed, infoEmbed, resourcesEmbed]); let welcomeMsg; try { welcomeMsg = await enqueueSend(channel, { content: `Hey There ${interaction.user} 🥦`, embeds: [welcomeEmbed, infoEmbed, resourcesEmbed], components: [actionRow] }); await Ticket.updateOne( { discordThreadId: channel.id }, { $set: { welcomeMessageId: welcomeMsg.id } } ); } catch (err) { console.error('welcomeMessageId-save', err); } const { createStaffThread } = require('../services/staffThread'); await createStaffThread(channel, interaction.client).catch(() => {}); if (CONFIG.PIN_INITIAL_MESSAGE_ENABLED && welcomeMsg) { const { pinMessage } = require('../services/pinMessage'); await pinMessage(welcomeMsg, interaction.client).catch(() => {}); } await interaction.deleteReply().catch(() => {}); const logChan = await interaction.client.channels.fetch(CONFIG.LOG_CHAN).catch(() => null); if (logChan) { await enqueueSend(logChan, `📝 ${channel.name} created by ${interaction.user.tag}` ); } } catch (err) { console.error('Ticket creation error:', err); await interaction.editReply('Failed to create ticket. Please contact an administrator.'); } } module.exports = { handleButton, handleTicketModal };