/** * Right-click "Apps" menu commands: * - "Create Ticket From Message" — turn a Discord message into a ticket. * - "View User Tickets" — show last 10 tickets for the targeted user. */ const { ChannelType, EmbedBuilder, MessageFlags, PermissionFlagsBits } = require('discord.js'); const { mongoose } = require('../../db-connection'); const { CONFIG } = require('../../config'); const { getPriorityEmoji } = require('../../utils'); const { checkTicketCreationRateLimit, getOrCreateTicketCategory } = require('../../services/tickets'); const { getTicketActionRow } = require('../../utils/ticketComponents'); const { enqueueSend } = require('../../services/channelQueue'); const { logError } = require('../../services/debugLog'); const Ticket = mongoose.model('Ticket'); async function handleCreateTicketFromMessage(interaction) { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); 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 message = interaction.targetMessage; const subject = `Message from ${message.author.tag}`; const description = message.content || 'No content'; const guild = interaction.guild; const lastTicket = await Ticket.findOne().sort({ ticketNumber: -1 }).select('ticketNumber').lean(); const ticketNumber = (lastTicket?.ticketNumber || 0) + 1; let parentCategoryIdForTicket; try { parentCategoryIdForTicket = await getOrCreateTicketCategory( guild, CONFIG.DISCORD_TICKET_CATEGORY_ID, CONFIG.TICKET_CATEGORY_NAME ); } catch (err) { console.error('getOrCreateTicketCategory (context menu ticket):', err); return interaction.editReply('❌ Discord ticket category could not be resolved. Contact an administrator.'); } let channel; try { channel = await guild.channels.create({ name: `ticket-${ticketNumber}`, type: ChannelType.GuildText, parent: parentCategoryIdForTicket, permissionOverwrites: [ { id: guild.id, deny: [PermissionFlagsBits.ViewChannel] }, { id: message.author.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 (context menu ticket):', err); return interaction.editReply('❌ Failed to create ticket channel. Contact an administrator.'); } const gmailThreadId = `discord-msg-${Date.now()}-${message.id}`; const now = new Date(); await Ticket.create({ gmailThreadId, discordThreadId: channel.id, senderEmail: message.author.tag, subject, createdAt: now, status: 'open', ticketNumber, priority: 'normal', lastActivity: now, creatorId: message.author.id, parentCategoryId: parentCategoryIdForTicket }); const welcomeEmbed = new EmbedBuilder() .setDescription(CONFIG.TICKET_WELCOME_MESSAGE) .setColor(CONFIG.EMBED_COLOR_INFO); const infoEmbed = new EmbedBuilder() .setColor(CONFIG.EMBED_COLOR_INFO) .addFields( { name: 'From message', value: `[Jump to message](${message.url})` }, { name: 'Creator', value: message.author.toString(), inline: true }, { name: 'Created by Staff', value: interaction.user.toString(), inline: true }, { name: 'Content', value: description.slice(0, 1000) || 'No content', inline: false } ); const row = getTicketActionRow({ escalationTier: 0 }); try { const welcomeMsg = await enqueueSend(channel, { content: `<@&${CONFIG.ROLE_ID_TO_PING}>\nHey There ${message.author} 🥦`, embeds: [welcomeEmbed, infoEmbed], components: [row] }); await Ticket.updateOne( { discordThreadId: channel.id }, { $set: { welcomeMessageId: welcomeMsg.id } } ); } catch (err) { console.error('welcomeMessageId-save', err); } await interaction.editReply(`✅ Ticket created: ${channel}`); } catch (err) { logError('create-ticket-from-message', err, interaction).catch(() => {}); await interaction.editReply('❌ Failed to create ticket from message.'); } } async function handleViewUserTickets(interaction) { await interaction.deferReply({ flags: MessageFlags.Ephemeral }); try { const targetUser = interaction.targetUser; const tickets = await Ticket.find({ senderEmail: targetUser.tag }) .sort({ createdAt: -1 }) .limit(10) .lean(); if (!tickets || tickets.length === 0) { return interaction.editReply(`📋 No tickets found for ${targetUser.tag}`); } const embed = new EmbedBuilder() .setTitle(`📋 Tickets for ${targetUser.tag}`) .setDescription(`Found ${tickets.length} ticket(s)`) .setColor(CONFIG.EMBED_COLOR_INFO); for (const ticket of tickets.slice(0, 5)) { const priorityEmoji = getPriorityEmoji(ticket.priority || 'normal'); const statusEmoji = ticket.status === 'open' ? '🟢' : '🔴'; embed.addFields({ name: `${priorityEmoji} Ticket #${ticket.ticketNumber} ${statusEmoji}`, value: `**Subject:** ${ticket.subject || 'No subject'}\n**Status:** ${ticket.status}\n**Claimed:** ${ticket.claimedBy || 'Unclaimed'}`, inline: false }); } if (tickets.length > 5) { embed.setFooter({ text: `Showing 5 of ${tickets.length} tickets` }); } await interaction.editReply({ embeds: [embed] }); } catch (err) { logError('view-user-tickets', err, interaction).catch(() => {}); await interaction.editReply('❌ Failed to fetch user tickets.'); } } module.exports = { handleCreateTicketFromMessage, handleViewUserTickets };