const { Client, GatewayIntentBits, Collection, Partials, Events, PermissionsBitField, InteractionType, ButtonBuilder, ButtonStyle, ActionRowBuilder, EmbedBuilder } = require('discord.js'); const fs = require('node:fs'); const path = require('path'); const config = require('./config.json'); const { getOrderEmbed, getOrderTypeDropdown, getOrderPictureEmbed } = require('./orders/embed.js'); const { getOrderModal } = require('./orders/modals.js'); const { getRoleId, createOrderChannel, renameOrderChannel } = require('./orders/utils.js'); const { getOrderTicketEmbed, getClaimedEmbed, getClaimActionRow, getUnclaimActionRow, getCloseActionRow } = require('./orders/orderEmbedGenerator.js'); const { logOrderClosure } = require('./orders/logging/orderLogger.js'); const { generateTranscript } = require('./orders/logging/transcript.js'); const taxSlashCommand = require('./taxcalculator/slashcommand.js'); const taxPrefixCommand = require('./taxcalculator/prefixcommand.js'); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers ], partials: [ Partials.Message, Partials.Channel, Partials.Reaction ] }); client.commands = new Collection(); const commandsPath = path.join(__dirname, 'orders', 'commands'); const commandFiles = fs.existsSync(commandsPath) ? fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')) : []; for (const file of commandFiles) { const filePath = path.join(commandsPath, file); const command = require(filePath); if ('data' in command && 'execute' in command) { client.commands.set(command.data.name, command); } } client.commands.set(taxSlashCommand.data.name, taxSlashCommand); const orderChannels = new Map(); client.once(Events.ClientReady, () => { console.log(`Logged in as ${client.user.tag}!`); }); client.on(Events.InteractionCreate, async interaction => { try { // 1. Slash commands if (interaction.isChatInputCommand()) { const command = client.commands.get(interaction.commandName); if (!command) return await interaction.reply({ content: 'Unknown command.', ephemeral: true }); try { await command.execute(interaction); } catch (error) { console.error(error); if (interaction.replied || interaction.deferred) { await interaction.followUp({ content: 'There was an error executing this command.', ephemeral: true }); } else { await interaction.reply({ content: 'There was an error executing this command.', ephemeral: true }); } } return; } // 2. Dropdown order type selection if (interaction.isStringSelectMenu() && interaction.customId === 'order-type-select') { try { const type = interaction.values[0]; function toConfigKey(val) { switch (val) { case "graphics": return "Graphics"; case "liveries": return "Liveries"; case "clothing": return "Clothing"; case "discordservers": return "DiscordServers"; case "photography": return "Photography"; case "els": return "ELS"; case "custombots": return "CustomBots"; default: return null; } } const configKey = toConfigKey(type); if (!configKey) { await interaction.reply({ content: "Unknown order type, please try again.", ephemeral: true }); return; } if (config.orderStatus[configKey] === "unavailable") { await interaction.reply({ content: "Sorry, this order type is currently unavailable!", ephemeral: true }); return; } const modal = getOrderModal(type); await interaction.showModal(modal); return; } catch (err) { if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: "An error occurred. Please try again.", ephemeral: true }); } console.error(err); return; } } // 3. Modal submission: create order channel if (interaction.type === InteractionType.ModalSubmit && interaction.customId.startsWith('order-modal-')) { const type = interaction.customId.replace('order-modal-', ''); let values = {}; if (type === 'graphics') { values.graphics_type = interaction.fields.getTextInputValue('graphics_type'); } values.description = interaction.fields.getTextInputValue('description'); values.quantity = interaction.fields.getTextInputValue('quantity'); const { channel, orderNumber } = await createOrderChannel(interaction, type); orderChannels.set(channel.id, { orderNumber, type, openedBy: interaction.user.id, modalFields: values, createdAt: Date.now(), claimedBy: null, description: values.description, channelId: channel.id, ticketMsgId: null, claimedMsgId: null, unclaimMsgId: null }); const roleId = getRoleId(type); const embed = getOrderTicketEmbed(type, values, interaction.user, roleId, orderNumber); const ticketMsg = await channel.send({ content: `<@&${roleId}> <@${interaction.user.id}>`, embeds: [embed], components: [getClaimActionRow(), getCloseActionRow()] }); orderChannels.get(channel.id).ticketMsgId = ticketMsg.id; await interaction.reply({ content: `Order created: <#${channel.id}>`, ephemeral: true }); return; } // 4. Claim order (first claim or 'claim2') if (interaction.isButton() && (interaction.customId === 'claim-order' || interaction.customId === 'claim-order-2')) { const channelId = interaction.channel.id; const order = orderChannels.get(channelId); if (!order) return await interaction.reply({ content: 'Order data missing.', ephemeral: true }); const requiredRoleId = getRoleId(order.type); if (!interaction.member.roles.cache.has(requiredRoleId)) { return await interaction.reply({ content: 'You do not have permission to claim this order.', ephemeral: true }); } order.claimedBy = interaction.user.id; orderChannels.set(channelId, order); // Try/catch fetch/edit in case message was deleted try { await renameOrderChannel(interaction.channel, order.type, order.orderNumber, 'claimed'); } catch (err) { console.warn('Channel rename failed:', err); } // Hide old claim button on ticket message if (order.ticketMsgId) { try { const ticketMsg = await interaction.channel.messages.fetch(order.ticketMsgId); await ticketMsg.edit({ components: [getCloseActionRow()] }); } catch (err) { console.warn('Could not edit previous ticket message:', err); } } // Send claimed embed + unclaim button const claimedEmbed = getClaimedEmbed( order.type, order.modalFields, { id: order.openedBy }, requiredRoleId, order.orderNumber, interaction.user.id ); const claimedMsg = await interaction.channel.send({ embeds: [claimedEmbed], components: [getUnclaimActionRow()] }); order.claimedMsgId = claimedMsg.id; orderChannels.set(channelId, order); await interaction.reply({ content: 'Order claimed!', ephemeral: true }); await interaction.channel.setTopic(`Claimed by ${interaction.user.tag}`); return; } // 5. Unclaim order (only by claimer) if (interaction.isButton() && interaction.customId === 'unclaim-order') { const channelId = interaction.channel.id; const order = orderChannels.get(channelId); if (!order) return await interaction.reply({ content: 'Order data missing.', ephemeral: true }); if (order.claimedBy !== interaction.user.id) { return await interaction.reply({ content: 'Only the user who claimed this order can unclaim it.', ephemeral: true }); } await interaction.deferReply({ ephemeral: true }); try { order.claimedBy = null; await interaction.editReply({ content: 'Order unclaimed and now available for claim.' }); // Create new embed with claim2 and close buttons const newEmbed = new EmbedBuilder() .setColor(0x0099ff) .setTitle('Order Available') .setDescription(`Order unclaimed by <@${interaction.user.id}>. The order is now available for claim again.`) .addFields( { name: 'Order Number', value: order.orderNumber.toString(), inline: true }, { name: 'Order Type', value: order.type, inline: true }, { name: 'Description', value: order.modalFields?.description || 'No description' }, { name: 'Quantity', value: order.modalFields?.quantity || '1', inline: true }, { name: 'Opened By', value: `<@${order.openedBy}>`, inline: true } ) .setTimestamp(new Date()) .setFooter({ text: 'Order System' }); // "claim2" button const claim2Button = new ButtonBuilder() .setCustomId('claim-order-2') .setLabel('Claim') .setStyle(ButtonStyle.Success); const closeButton = new ButtonBuilder() .setCustomId('close-order') .setLabel('Close') .setStyle(ButtonStyle.Danger); const row = new ActionRowBuilder().addComponents(claim2Button, closeButton); const sent = await interaction.channel.send({ embeds: [newEmbed], components: [row] }); order.ticketMsgId = sent.id; orderChannels.set(channelId, order); await interaction.channel.setTopic('Not claimed yet.'); // Delete all previous claim buttons try { const fetched = await interaction.channel.messages.fetch(); for (const msg of fetched.values()) { if ( msg.id !== sent.id && msg.author.id === interaction.client.user.id && msg.components.length && msg.components[0].components.find(c => c.customId === 'claim-order' || c.customId === 'claim-order-2') ) { await msg.delete().catch(() => {}); } } } catch (e) {} renameOrderChannel(interaction.channel, order.type, order.orderNumber, 'pending').catch(console.error); } catch (err) { console.error('[Unclaim] Error:', err); try { await interaction.editReply({ content: `An error occurred while unclaiming the order: ${err.message}` }); } catch (e) {} } return; } // 6. Close order confirmation if (interaction.isButton() && interaction.customId === 'close-order') { const confirmRow = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId('close-order-yes') .setLabel('Yes') .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setCustomId('close-order-no') .setLabel('No') .setStyle(ButtonStyle.Secondary) ); await interaction.reply({ content: 'Are you sure you want to close this order?', components: [confirmRow], ephemeral: false }); return; } if (interaction.isButton() && interaction.customId === 'close-order-yes') { const channelId = interaction.channel.id; const order = orderChannels.get(channelId); if (!order) return await interaction.reply({ content: 'Order data missing.', ephemeral: true }); const transcriptFile = `order-${order.orderNumber}.html`; await generateTranscript(interaction.channel, transcriptFile); await logOrderClosure({ orderData: order, closer: interaction.user, transcriptUrl: transcriptFile, client }); await interaction.reply({ content: 'Order will be closed in 5 seconds.', ephemeral: true }); setTimeout(async () => { orderChannels.delete(channelId); await interaction.channel.delete('Order closed'); }, 5000); return; } if (interaction.isButton() && interaction.customId === 'close-order-no') { await interaction.reply({ content: 'Order closure cancelled.', ephemeral: true }); return; } // 7. View transcript button handler if (interaction.isButton() && interaction.customId.startsWith('view-transcript-')) { const orderNumber = interaction.customId.replace('view-transcript-', ''); const transcriptFile = `order-${orderNumber}.html`; const transcriptPath = path.join(__dirname, 'transcripts', transcriptFile); if (!fs.existsSync(transcriptPath)) { await interaction.reply({ content: "Transcript file not found.", ephemeral: true }); return; } await interaction.reply({ content: `Here is the transcript for order ${orderNumber}:`, files: [{ attachment: transcriptPath, name: transcriptFile }] }); return; } // 8. Defensive: always reply if not handled above if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: "This interaction is not yet handled. Please contact an admin.", ephemeral: true }); } } catch (err) { // Defensive: Always reply if something unexpected happened if (interaction && !interaction.replied && !interaction.deferred) { await interaction.reply({ content: "An error occurred while processing your interaction.", ephemeral: true }); } console.error("Global handler error:", err); } }); client.on(Events.MessageCreate, async message => { if (!message.guild || message.author.bot) return; if (message.member.permissions.has(PermissionsBitField.Flags.Administrator) && message.content === '!do order embed') { const pictureEmbed = getOrderPictureEmbed(); const infoEmbed = getOrderEmbed(); const dropdown = getOrderTypeDropdown(); await message.guild.channels.fetch(config.orderchannel) .then(channel => channel.send({ embeds: [pictureEmbed, infoEmbed], components: [dropdown] }) ); return; } if (message.content.startsWith('!tax')) { const args = message.content.trim().split(/\s+/).slice(1); await taxPrefixCommand.execute(message, args); } }); client.login(config.token);