From c5076d82a90f94c539f4d2f2f07e3cb5a5f1d081 Mon Sep 17 00:00:00 2001 From: AdenTan123 Date: Mon, 8 Jun 2026 15:59:14 +0800 Subject: [PATCH 1/6] Delete src/commands/Ticket directory useless --- src/commands/Ticket/claim.js | 102 -- src/commands/Ticket/close.js | 113 -- .../Ticket/modules/ticket_dashboard.js | 1036 ----------------- src/commands/Ticket/priority.js | 119 -- src/commands/Ticket/ticket.js | 336 ------ 5 files changed, 1706 deletions(-) delete mode 100644 src/commands/Ticket/claim.js delete mode 100644 src/commands/Ticket/close.js delete mode 100644 src/commands/Ticket/modules/ticket_dashboard.js delete mode 100644 src/commands/Ticket/priority.js delete mode 100644 src/commands/Ticket/ticket.js diff --git a/src/commands/Ticket/claim.js b/src/commands/Ticket/claim.js deleted file mode 100644 index bb98ce668..000000000 --- a/src/commands/Ticket/claim.js +++ /dev/null @@ -1,102 +0,0 @@ -import { getColor } from '../../config/bot.js'; -import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; -import { errorEmbed, successEmbed } from '../../utils/embeds.js'; -import { logger } from '../../utils/logger.js'; -import { handleInteractionError } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { getTicketPermissionContext } from '../../utils/ticketPermissions.js'; -import { claimTicket } from '../../services/ticket.js'; -export default { - data: new SlashCommandBuilder() - .setName("claim") - .setDescription("Claims an open ticket, assigning it to you.") - .setDMPermission(false), - - async execute(interaction, guildConfig, client) { - try { - - const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - if (!deferred) { - return; - } - - const permissionContext = await getTicketPermissionContext({ client, interaction }); - if (!permissionContext.ticketData) { - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - "Not a Ticket Channel", - "This command can only be used in a valid ticket channel.", - ), - ], - }); - } - - if (!permissionContext.canManageTicket) { - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - "Permission Denied", - "You need the `Manage Channels` permission or the configured `Ticket Staff Role` to claim tickets.", - ), - ], - }); - } - - const channel = interaction.channel; - const result = await claimTicket(channel, interaction.user); - - if (!result.success) { - logger.warn('Ticket claim failed - not a valid ticket channel', { - userId: interaction.user.id, - channelId: channel.id, - guildId: interaction.guildId, - error: result.error - }); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - "Not a Ticket Channel", - result.error || "This command can only be used in a valid ticket channel.", - ), - ], - }); - } - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - "Ticket Claimed!", - "You have successfully claimed this ticket.", - ), - ], - }); - - logger.info('Ticket claimed successfully', { - userId: interaction.user.id, - userTag: interaction.user.tag, - channelId: channel.id, - channelName: channel.name, - guildId: interaction.guildId, - commandName: 'claim' - }); - - } catch (error) { - logger.error('Error executing claim command', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - channelId: interaction.channel?.id, - guildId: interaction.guildId, - commandName: 'claim' - }); - await handleInteractionError(interaction, error, { - commandName: 'claim', - source: 'ticket_claim_command' - }); - } - }, -}; - - - diff --git a/src/commands/Ticket/close.js b/src/commands/Ticket/close.js deleted file mode 100644 index f05b6bd56..000000000 --- a/src/commands/Ticket/close.js +++ /dev/null @@ -1,113 +0,0 @@ -import { getColor } from '../../config/bot.js'; -import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, MessageFlags } from 'discord.js'; -import { errorEmbed, successEmbed } from '../../utils/embeds.js'; -import { logger } from '../../utils/logger.js'; -import { handleInteractionError } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { getTicketPermissionContext } from '../../utils/ticketPermissions.js'; -import { closeTicket } from '../../services/ticket.js'; -export default { - data: new SlashCommandBuilder() - .setName("close") - .setDescription("Closes the current ticket.") - .setDMPermission(false) - .addStringOption((option) => - option - .setName("reason") - .setDescription("The reason for closing the ticket.") - .setRequired(false), - ), - - async execute(interaction, guildConfig, client) { - try { - - const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - if (!deferred) { - return; - } - - const permissionContext = await getTicketPermissionContext({ client, interaction }); - if (!permissionContext.ticketData) { - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - "Not a Ticket Channel", - "This command can only be used in a valid ticket channel.", - ), - ], - }); - } - - if (!permissionContext.canCloseTicket) { - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - "Permission Denied", - "You need the `Manage Channels` permission, the configured `Ticket Staff Role`, or be the ticket creator to close this ticket.", - ), - ], - }); - } - - const channel = interaction.channel; - const reason = - interaction.options?.getString("reason") || - "Closed via command without a specific reason."; - - const result = await closeTicket(channel, interaction.user, reason); - - if (!result.success) { - logger.warn('Ticket close failed - not a valid ticket channel', { - userId: interaction.user.id, - channelId: channel.id, - guildId: interaction.guildId, - error: result.error - }); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - "Not a Ticket Channel", - result.error || "This command can only be used in a valid ticket channel.", - ), - ], - }); - } - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - "Ticket Closed!", - "This ticket has been closed successfully.", - ), - ], - }); - - logger.info('Ticket closed successfully', { - userId: interaction.user.id, - userTag: interaction.user.tag, - channelId: channel.id, - channelName: channel.name, - guildId: interaction.guildId, - reason: reason, - commandName: 'close' - }); - - } catch (error) { - logger.error('Error executing close command', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - channelId: interaction.channel?.id, - guildId: interaction.guildId, - commandName: 'close' - }); - await handleInteractionError(interaction, error, { - commandName: 'close', - source: 'ticket_close_command' - }); - } - }, -}; - - - diff --git a/src/commands/Ticket/modules/ticket_dashboard.js b/src/commands/Ticket/modules/ticket_dashboard.js deleted file mode 100644 index 43ece8d98..000000000 --- a/src/commands/Ticket/modules/ticket_dashboard.js +++ /dev/null @@ -1,1036 +0,0 @@ -import { getColor } from '../../../config/bot.js'; -import { - ActionRowBuilder, - StringSelectMenuBuilder, - StringSelectMenuOptionBuilder, - ModalBuilder, - TextInputBuilder, - TextInputStyle, - RoleSelectMenuBuilder, - ChannelSelectMenuBuilder, - UserSelectMenuBuilder, - ButtonBuilder, - ButtonStyle, - ChannelType, - MessageFlags, - ComponentType, - EmbedBuilder, -} from 'discord.js'; -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -import { successEmbed, errorEmbed } from '../../../utils/embeds.js'; -import { logger } from '../../../utils/logger.js'; -import { TitanBotError, ErrorTypes } from '../../../utils/errorHandler.js'; -import { getGuildConfig } from '../../../services/guildConfig.js'; -import { getGuildConfigKey } from '../../../utils/database.js'; -import { getUserTicketCount } from '../../../services/ticket.js'; - -// ─── Embed & Menu Builders ──────────────────────────────────────────────────── - -function buildDashboardEmbed(config, guild) { - const panelChannel = config.ticketPanelChannelId ? `<#${config.ticketPanelChannelId}>` : '`Not set`'; - const staffRole = config.ticketStaffRoleId ? `<@&${config.ticketStaffRoleId}>` : '`Not set`'; - const ticketLogsChannel = config.ticketLogsChannelId ? `<#${config.ticketLogsChannelId}>` : '`Not set`'; - const transcriptChannel = config.ticketTranscriptChannelId ? `<#${config.ticketTranscriptChannelId}>` : '`Not set`'; - - // Get category names from guild - const openCategoryChannel = config.ticketCategoryId ? guild.channels.cache.get(config.ticketCategoryId) : null; - const openCategory = openCategoryChannel ? openCategoryChannel.toString() : '`Not set`'; - - const closedCategoryChannel = config.ticketClosedCategoryId ? guild.channels.cache.get(config.ticketClosedCategoryId) : null; - const closedCategory = closedCategoryChannel ? closedCategoryChannel.toString() : '`Not set`'; - - const rawMsg = config.ticketPanelMessage || 'Click the button below to create a support ticket.'; - const panelMsg = `\`${rawMsg.length > 60 ? rawMsg.substring(0, 60) + '…' : rawMsg}\``; - const btnLabel = `\`${config.ticketButtonLabel || 'Create Ticket'}\``; - - return new EmbedBuilder() - .setTitle('🎫 Ticket System Dashboard') - .setDescription(`Manage ticket system settings for **${guild.name}**.\nSelect an option below to modify a setting.`) - .setColor(getColor('info')) - .addFields( - { name: '📢 Panel Channel', value: panelChannel, inline: true }, - { name: '🛡️ Staff Role', value: staffRole, inline: true }, - { name: '\u200B', value: '\u200B', inline: true }, - { name: '📁 Open Tickets Category', value: openCategory, inline: true }, - { name: '📂 Closed Tickets Category', value: closedCategory, inline: true }, - { name: '\u200B', value: '\u200B', inline: true }, - { name: '📝 Panel Message', value: panelMsg, inline: false }, - { name: '🏷️ Button Label', value: btnLabel, inline: true }, - { name: '🔢 Max Tickets/User', value: String(config.maxTicketsPerUser || 3), inline: true }, - { name: '📬 DM on Close', value: config.dmOnClose !== false ? '✅ Enabled' : '❌ Disabled', inline: true }, - { name: '🎫 Ticket Logs Channel', value: ticketLogsChannel, inline: true }, - { name: '📜 Transcript Channel', value: transcriptChannel, inline: true }, - ) - .setFooter({ text: 'Select an option below • Dashboard closes after 10 minutes of inactivity' }) - .setTimestamp(); -} - -function buildSelectMenu(guildId) { - return new StringSelectMenuBuilder() - .setCustomId(`ticket_config_${guildId}`) - .setPlaceholder('Select a setting to configure...') - .addOptions( - new StringSelectMenuOptionBuilder() - .setLabel('Edit Panel Message') - .setDescription('Change the message displayed on the ticket creation panel') - .setValue('panel_message') - .setEmoji('📝'), - new StringSelectMenuOptionBuilder() - .setLabel('Edit Button Label') - .setDescription('Change the label on the Create Ticket button') - .setValue('button_label') - .setEmoji('🏷️'), - new StringSelectMenuOptionBuilder() - .setLabel('Change Open Tickets Category') - .setDescription('Category where new tickets are created') - .setValue('open_category') - .setEmoji('📁'), - new StringSelectMenuOptionBuilder() - .setLabel('Change Closed Tickets Category') - .setDescription('Category where closed tickets are moved') - .setValue('closed_category') - .setEmoji('📂'), - new StringSelectMenuOptionBuilder() - .setLabel('Set Max Tickets per User') - .setDescription('Limit how many open tickets one user can have at once') - .setValue('max_tickets') - .setEmoji('🔢'), - new StringSelectMenuOptionBuilder() - .setLabel('Set Ticket Logs Channel') - .setDescription('Channel to receive ticket feedback, lifecycle events, and logs') - .setValue('logs_channel') - .setEmoji('🎫'), - new StringSelectMenuOptionBuilder() - .setLabel('Set Transcript Channel') - .setDescription('Channel to receive auto-generated transcripts on deletion') - .setValue('transcript_channel') - .setEmoji('📜'), - ); -} - -function buildButtonRow(guildConfig, guildId, disabled = false) { - const dmEnabled = guildConfig.dmOnClose !== false; - return new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId(`ticket_cfg_dm_toggle_${guildId}`) - .setLabel('DM on Close') - .setStyle(dmEnabled ? ButtonStyle.Success : ButtonStyle.Danger) - .setEmoji(dmEnabled ? '📬' : '📭') - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId(`ticket_cfg_staff_role_btn_${guildId}`) - .setLabel('Staff Role') - .setStyle(ButtonStyle.Secondary) - .setEmoji('🛡️') - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId(`ticket_cfg_delete_${guildId}`) - .setLabel('Delete System') - .setStyle(ButtonStyle.Danger) - .setEmoji('🗑️') - .setDisabled(disabled), - ); -} - -// ─── Helpers ────────────────────────────────────────────────────────────────── - -async function refreshDashboard(rootInteraction, guildConfig, guildId) { - const buttonRow = buildButtonRow(guildConfig, guildId); - const selectRow = new ActionRowBuilder().addComponents(buildSelectMenu(guildId)); - await InteractionHelper.safeEditReply(rootInteraction, { - embeds: [buildDashboardEmbed(guildConfig, rootInteraction.guild)], - components: [buttonRow, selectRow], - }).catch(() => {}); -} - -/** - * Attempts to find and edit the live ticket panel message in the panel channel. - * Returns true if the panel was found and updated, false otherwise. - */ -async function updateLivePanel(client, guild, config) { - if (!config.ticketPanelChannelId) return false; - try { - const channel = await guild.channels.fetch(config.ticketPanelChannelId).catch(() => null); - if (!channel) return false; - - const messages = await channel.messages.fetch({ limit: 50 }); - const panelMsg = messages.find( - m => - m.author.id === client.user.id && - m.components?.length > 0 && - m.components[0]?.components?.[0]?.customId === 'create_ticket', - ); - if (!panelMsg) return false; - - const updatedEmbed = new EmbedBuilder() - .setTitle('🎫 Support Tickets') - .setDescription(config.ticketPanelMessage || 'Click the button below to create a support ticket.') - .setColor(getColor('info')); - - const button = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId('create_ticket') - .setLabel(config.ticketButtonLabel || 'Create Ticket') - .setStyle(ButtonStyle.Primary) - .setEmoji('📩'), - ); - - await panelMsg.edit({ embeds: [updatedEmbed], components: [button] }); - return true; - } catch (error) { - logger.warn('Failed to update live ticket panel:', error.message); - return false; - } -} - -// ─── Main Export ────────────────────────────────────────────────────────────── - -export default { - async execute(interaction, config, client) { - try { - const guildId = interaction.guild.id; - const guildConfig = await getGuildConfig(client, guildId); - - if (!guildConfig.ticketPanelChannelId) { - throw new TitanBotError( - 'Ticket system not configured', - ErrorTypes.CONFIGURATION, - 'The ticket system has not been set up yet. Run `/ticket setup` first to configure it.', - ); - } - - const selectMenu = buildSelectMenu(guildId); - const selectRow = new ActionRowBuilder().addComponents(selectMenu); - const buttonRow = buildButtonRow(guildConfig, guildId); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [buildDashboardEmbed(guildConfig, interaction.guild)], - components: [buttonRow, selectRow], - }); - - const replyMessage = await interaction.fetchReply().catch(() => null); - const replyMessageId = replyMessage?.id; - - const collector = interaction.channel.createMessageComponentCollector({ - componentType: ComponentType.StringSelect, - filter: i => - i.user.id === interaction.user.id && - i.customId === `ticket_config_${guildId}` && - (!replyMessageId || i.message.id === replyMessageId), - time: 600_000, - }); - - const buttonCollector = interaction.channel.createMessageComponentCollector({ - componentType: ComponentType.Button, - filter: i => - i.user.id === interaction.user.id && - (!replyMessageId || i.message.id === replyMessageId) && - (i.customId === `ticket_cfg_dm_toggle_${guildId}` || - i.customId === `ticket_cfg_staff_role_btn_${guildId}` || - i.customId === `ticket_cfg_delete_${guildId}`), - - time: 600_000, - }); - - collector.on('collect', async (selectInteraction) => { - const selectedOption = selectInteraction.values[0]; - try { - switch (selectedOption) { - case 'panel_message': - await handlePanelMessage(selectInteraction, interaction, guildConfig, guildId, client); - break; - case 'button_label': - await handleButtonLabel(selectInteraction, interaction, guildConfig, guildId, client); - break; - case 'staff_role': - await handleStaffRole(selectInteraction, interaction, guildConfig, guildId, client); - break; - case 'open_category': - await handleOpenCategory(selectInteraction, interaction, guildConfig, guildId, client); - break; - case 'closed_category': - await handleClosedCategory(selectInteraction, interaction, guildConfig, guildId, client); - break; - case 'max_tickets': - await handleMaxTickets(selectInteraction, interaction, guildConfig, guildId, client); - break; - case 'logs_channel': - await handleLogsChannel(selectInteraction, interaction, guildConfig, guildId, client); - break; - case 'transcript_channel': - await handleTranscriptChannel(selectInteraction, interaction, guildConfig, guildId, client); - break; - } - } catch (error) { - if (error instanceof TitanBotError) { - logger.debug(`Ticket config validation error: ${error.message}`); - } else { - logger.error('Unexpected ticket config menu error:', error); - } - - const errorMessage = - error instanceof TitanBotError - ? error.userMessage || 'An error occurred while processing your selection.' - : 'An unexpected error occurred while updating the configuration.'; - - // Already deferred at the top of the collector - await selectInteraction - .followUp({ - embeds: [errorEmbed('Configuration Error', errorMessage)], - flags: MessageFlags.Ephemeral, - }) - .catch(() => {}); - } - }); - - buttonCollector.on('collect', async (btnInteraction) => { - try { - if (btnInteraction.customId === `ticket_cfg_dm_toggle_${guildId}`) { - await handleDmOnClose(btnInteraction, interaction, guildConfig, guildId, client); - } else if (btnInteraction.customId === `ticket_cfg_staff_role_btn_${guildId}`) { - await handleStaffRole(btnInteraction, interaction, guildConfig, guildId, client); - } else if (btnInteraction.customId === `ticket_cfg_delete_${guildId}`) { - await handleDeleteSystem(btnInteraction, interaction, guildConfig, guildId, client); - } - } catch (error) { - if (error.code === 40060) return; - if (error instanceof TitanBotError) { - logger.debug(`Ticket config button error: ${error.message}`); - } else { - logger.error('Unexpected ticket config button error:', error); - } - const errorMessage = - error instanceof TitanBotError - ? error.userMessage || 'An error occurred while processing your selection.' - : 'An unexpected error occurred while updating the configuration.'; - - // Already deferred at the top of the collector - await btnInteraction - .followUp({ - embeds: [errorEmbed('Configuration Error', errorMessage)], - flags: MessageFlags.Ephemeral, - }) - .catch(() => {}); - } - }); - - collector.on('end', async (collected, reason) => { - buttonCollector.stop(); - if (reason === 'time') { - const timeoutEmbed = new EmbedBuilder() - .setTitle('⏰ Dashboard Timed Out') - .setDescription('This dashboard has been closed due to inactivity. Please run the command again to continue.') - .setColor(getColor('error')); - await InteractionHelper.safeEditReply(interaction, { - embeds: [timeoutEmbed], - components: [], - }).catch(() => {}); - } - }); - } catch (error) { - if (error instanceof TitanBotError) throw error; - logger.error('Unexpected error in ticket_config:', error); - throw new TitanBotError( - `Ticket config failed: ${error.message}`, - ErrorTypes.UNKNOWN, - 'Failed to open the ticket configuration dashboard.', - ); - } - }, -}; - -// ─── Panel Message ──────────────────────────────────────────────────────────── - -async function handlePanelMessage(selectInteraction, rootInteraction, guildConfig, guildId, client) { - const modal = new ModalBuilder() - .setCustomId('ticket_cfg_panel_msg') - .setTitle('Edit Panel Message') - .addComponents( - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId('panel_msg_input') - .setLabel('Panel Message') - .setStyle(TextInputStyle.Paragraph) - .setValue( - guildConfig.ticketPanelMessage || - 'Click the button below to create a support ticket.', - ) - .setMaxLength(2000) - .setMinLength(1) - .setRequired(true) - .setPlaceholder('Click the button below to create a support ticket.'), - ), - ); - - await selectInteraction.showModal(modal); - - const submitted = await selectInteraction - .awaitModalSubmit({ - filter: i => - i.customId === 'ticket_cfg_panel_msg' && i.user.id === selectInteraction.user.id, - time: 120_000, - }) - .catch(() => null); - - if (!submitted) return; - - const newMessage = submitted.fields.getTextInputValue('panel_msg_input').trim(); - guildConfig.ticketPanelMessage = newMessage; - await client.db.set(getGuildConfigKey(guildId), guildConfig); - - const panelUpdated = await updateLivePanel(client, rootInteraction.guild, guildConfig); - - await submitted.reply({ - embeds: [ - successEmbed( - '✅ Panel Message Updated', - `The panel message has been updated.${ - panelUpdated - ? '\nThe live ticket panel has also been refreshed.' - : '\n> **Note:** The live panel could not be located. The new message will apply the next time you run `/ticket setup`.' - }`, - ), - ], - flags: MessageFlags.Ephemeral, - }); - - await refreshDashboard(rootInteraction, guildConfig, guildId); -} - -// ─── Button Label ───────────────────────────────────────────────────────────── - -async function handleButtonLabel(selectInteraction, rootInteraction, guildConfig, guildId, client) { - const modal = new ModalBuilder() - .setCustomId('ticket_cfg_btn_label') - .setTitle('Edit Button Label') - .addComponents( - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId('btn_label_input') - .setLabel('Button Label (max 80 characters)') - .setStyle(TextInputStyle.Short) - .setValue(guildConfig.ticketButtonLabel || 'Create Ticket') - .setMaxLength(80) - .setMinLength(1) - .setRequired(true) - .setPlaceholder('Create Ticket'), - ), - ); - - await selectInteraction.showModal(modal); - - const submitted = await selectInteraction - .awaitModalSubmit({ - filter: i => - i.customId === 'ticket_cfg_btn_label' && i.user.id === selectInteraction.user.id, - time: 120_000, - }) - .catch(() => null); - - if (!submitted) return; - - const newLabel = submitted.fields.getTextInputValue('btn_label_input').trim(); - guildConfig.ticketButtonLabel = newLabel; - await client.db.set(getGuildConfigKey(guildId), guildConfig); - - const panelUpdated = await updateLivePanel(client, rootInteraction.guild, guildConfig); - - await submitted.reply({ - embeds: [ - successEmbed( - '✅ Button Label Updated', - `Button label changed to \`${newLabel}\`.${ - panelUpdated - ? '\nThe live ticket panel button has also been updated.' - : '\n> **Note:** The live panel could not be located. The new label will apply the next time you run `/ticket setup`.' - }`, - ), - ], - flags: MessageFlags.Ephemeral, - }); - - await refreshDashboard(rootInteraction, guildConfig, guildId); -} - -// ─── Staff Role ─────────────────────────────────────────────────────────────── - -async function handleStaffRole(selectInteraction, rootInteraction, guildConfig, guildId, client) { - await selectInteraction.deferUpdate(); - - const roleSelect = new RoleSelectMenuBuilder() - .setCustomId('ticket_cfg_staff_role') - .setPlaceholder('Select the staff role...') - .setMaxValues(1); - - const row = new ActionRowBuilder().addComponents(roleSelect); - - await selectInteraction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle('🛡️ Change Staff Role') - .setDescription( - `**Current:** ${guildConfig.ticketStaffRoleId ? `<@&${guildConfig.ticketStaffRoleId}>` : '`Not set`'}\n\nSelect the role that should have staff access to manage tickets.`, - ) - .setColor(getColor('info')), - ], - components: [row], - flags: MessageFlags.Ephemeral, - }); - - const roleCollector = rootInteraction.channel.createMessageComponentCollector({ - componentType: ComponentType.RoleSelect, - filter: i => - i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_staff_role', - time: 60_000, - max: 1, - }); - - roleCollector.on('collect', async roleInteraction => { - await roleInteraction.deferUpdate(); - const role = roleInteraction.roles.first(); - - guildConfig.ticketStaffRoleId = role.id; - await client.db.set(getGuildConfigKey(guildId), guildConfig); - - await roleInteraction.followUp({ - embeds: [successEmbed('✅ Staff Role Updated', `Staff role set to ${role}.`)], - flags: MessageFlags.Ephemeral, - }); - - await refreshDashboard(rootInteraction, guildConfig, guildId); - }); - - roleCollector.on('end', (collected, reason) => { - if (reason === 'time' && collected.size === 0) { - selectInteraction - .followUp({ - embeds: [errorEmbed('Timed Out', 'No role was selected. The staff role was not changed.')], - flags: MessageFlags.Ephemeral, - }) - .catch(() => {}); - } - }); -} - -// ─── Open Tickets Category ──────────────────────────────────────────────────── - -async function handleOpenCategory(selectInteraction, rootInteraction, guildConfig, guildId, client) { - await selectInteraction.deferUpdate(); - - const channelSelect = new ChannelSelectMenuBuilder() - .setCustomId('ticket_cfg_open_cat') - .setPlaceholder('Select a category...') - .addChannelTypes(ChannelType.GuildCategory) - .setMaxValues(1); - - const row = new ActionRowBuilder().addComponents(channelSelect); - - await selectInteraction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle('📁 Change Open Tickets Category') - .setDescription( - `**Current:** ${guildConfig.ticketCategoryId ? `<#${guildConfig.ticketCategoryId}>` : '`Not set`'}\n\nSelect the category where new tickets will be created.`, - ) - .setColor(getColor('info')), - ], - components: [row], - flags: MessageFlags.Ephemeral, - }); - - const catCollector = rootInteraction.channel.createMessageComponentCollector({ - componentType: ComponentType.ChannelSelect, - filter: i => - i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_open_cat', - time: 60_000, - max: 1, - }); - - catCollector.on('collect', async catInteraction => { - await catInteraction.deferUpdate(); - const category = catInteraction.channels.first(); - - guildConfig.ticketCategoryId = category.id; - await client.db.set(getGuildConfigKey(guildId), guildConfig); - - await catInteraction.followUp({ - embeds: [ - successEmbed( - '✅ Open Category Updated', - `New tickets will now be created in **${category.name}**.`, - ), - ], - flags: MessageFlags.Ephemeral, - }); - - await refreshDashboard(rootInteraction, guildConfig, guildId); - }); - - catCollector.on('end', (collected, reason) => { - if (reason === 'time' && collected.size === 0) { - selectInteraction - .followUp({ - embeds: [ - errorEmbed('Timed Out', 'No category was selected. The setting was not changed.'), - ], - flags: MessageFlags.Ephemeral, - }) - .catch(() => {}); - } - }); -} - -// ─── Closed Tickets Category ────────────────────────────────────────────────── - -async function handleClosedCategory( - selectInteraction, - rootInteraction, - guildConfig, - guildId, - client, -) { - await selectInteraction.deferUpdate(); - - const channelSelect = new ChannelSelectMenuBuilder() - .setCustomId('ticket_cfg_closed_cat') - .setPlaceholder('Select a category...') - .addChannelTypes(ChannelType.GuildCategory) - .setMaxValues(1); - - const row = new ActionRowBuilder().addComponents(channelSelect); - - await selectInteraction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle('📂 Change Closed Tickets Category') - .setDescription( - `**Current:** ${guildConfig.ticketClosedCategoryId ? `<#${guildConfig.ticketClosedCategoryId}>` : '`Not set`'}\n\nSelect the category where closed tickets will be moved.`, - ) - .setColor(getColor('info')), - ], - components: [row], - flags: MessageFlags.Ephemeral, - }); - - const catCollector = rootInteraction.channel.createMessageComponentCollector({ - componentType: ComponentType.ChannelSelect, - filter: i => - i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_closed_cat', - time: 60_000, - max: 1, - }); - - catCollector.on('collect', async catInteraction => { - await catInteraction.deferUpdate(); - const category = catInteraction.channels.first(); - - guildConfig.ticketClosedCategoryId = category.id; - await client.db.set(getGuildConfigKey(guildId), guildConfig); - - await catInteraction.followUp({ - embeds: [ - successEmbed( - '✅ Closed Category Updated', - `Closed tickets will now be moved to **${category.name}**.`, - ), - ], - flags: MessageFlags.Ephemeral, - }); - - await refreshDashboard(rootInteraction, guildConfig, guildId); - }); - - catCollector.on('end', (collected, reason) => { - if (reason === 'time' && collected.size === 0) { - selectInteraction - .followUp({ - embeds: [ - errorEmbed('Timed Out', 'No category was selected. The setting was not changed.'), - ], - flags: MessageFlags.Ephemeral, - }) - .catch(() => {}); - } - }); -} - -// ─── Max Tickets per User ───────────────────────────────────────────────────── - -async function handleMaxTickets(selectInteraction, rootInteraction, guildConfig, guildId, client) { - const modal = new ModalBuilder() - .setCustomId('ticket_cfg_max_tickets') - .setTitle('Set Max Tickets per User') - .addComponents( - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId('max_tickets_input') - .setLabel('Max Open Tickets (1–10)') - .setStyle(TextInputStyle.Short) - .setValue(String(guildConfig.maxTicketsPerUser || 3)) - .setMaxLength(2) - .setMinLength(1) - .setRequired(true) - .setPlaceholder('3'), - ), - ); - - await selectInteraction.showModal(modal); - - const submitted = await selectInteraction - .awaitModalSubmit({ - filter: i => - i.customId === 'ticket_cfg_max_tickets' && i.user.id === selectInteraction.user.id, - time: 120_000, - }) - .catch(() => null); - - if (!submitted) return; - - const raw = submitted.fields.getTextInputValue('max_tickets_input').trim(); - const newMax = parseInt(raw, 10); - - if (isNaN(newMax) || newMax < 1 || newMax > 10) { - await submitted.reply({ - embeds: [errorEmbed('Invalid Value', 'Max tickets must be a whole number between **1** and **10**.')], - flags: MessageFlags.Ephemeral, - }); - return; - } - - guildConfig.maxTicketsPerUser = newMax; - await client.db.set(getGuildConfigKey(guildId), guildConfig); - - await submitted.reply({ - embeds: [ - successEmbed( - '✅ Max Tickets Updated', - `Users can now have at most **${newMax}** open ticket${newMax !== 1 ? 's' : ''} at a time.`, - ), - ], - flags: MessageFlags.Ephemeral, - }); - - await refreshDashboard(rootInteraction, guildConfig, guildId); -} - -// ─── DM on Close Toggle ─────────────────────────────────────────────────────── - -async function handleDmOnClose(btnInteraction, rootInteraction, guildConfig, guildId, client) { - await btnInteraction.deferUpdate(); - - const newState = guildConfig.dmOnClose === false; - guildConfig.dmOnClose = newState; - await client.db.set(getGuildConfigKey(guildId), guildConfig); - - await btnInteraction.followUp({ - embeds: [ - successEmbed( - '✅ DM on Close Updated', - `Users will **${newState ? 'now' : 'no longer'}** receive a DM when their ticket is closed.`, - ), - ], - flags: MessageFlags.Ephemeral, - }); - - await refreshDashboard(rootInteraction, guildConfig, guildId); -} - -// ─── Feedback Logs Channel ──────────────────────────────────────────────────── - -async function handleLogsChannel(selectInteraction, rootInteraction, guildConfig, guildId, client) { - await selectInteraction.deferUpdate(); - - const channelSelect = new ChannelSelectMenuBuilder() - .setCustomId('ticket_cfg_logs_channel') - .setPlaceholder('Select a channel...') - .addChannelTypes(ChannelType.GuildText) - .setMaxValues(1); - - await selectInteraction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle('🎫 Select Ticket Logs Channel') - .setDescription('Choose where ticket feedback, lifecycle events (open, close, claim, etc.), and other logs will be sent.') - .setColor(getColor('info')) - ], - components: [new ActionRowBuilder().addComponents(channelSelect)], - flags: MessageFlags.Ephemeral - }); - - const collector = rootInteraction.channel.createMessageComponentCollector({ - componentType: ComponentType.ChannelSelect, - filter: i => i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_logs_channel', - time: 60_000, - max: 1 - }); - - collector.on('collect', async channelInteraction => { - await channelInteraction.deferUpdate(); - const channel = channelInteraction.channels.first(); - - guildConfig.ticketLogsChannelId = channel.id; - await client.db.set(getGuildConfigKey(guildId), guildConfig); - - await channelInteraction.followUp({ - embeds: [successEmbed('✅ Logs Channel Updated', `Ticket logs will be sent to ${channel}`)], - flags: MessageFlags.Ephemeral - }); - - await refreshDashboard(rootInteraction, guildConfig, guildId); - }); - - collector.on('end', (collected, reason) => { - if (reason === 'time' && collected.size === 0) { - selectInteraction.followUp({ - embeds: [errorEmbed('Timed Out', 'No channel selected. No changes were made.')], - flags: MessageFlags.Ephemeral - }).catch(() => {}); - } - }); -} - -// ─── Transcript Channel ─────────────────────────────────────────────────────── - -async function handleTranscriptChannel(selectInteraction, rootInteraction, guildConfig, guildId, client) { - await selectInteraction.deferUpdate(); - - const channelSelect = new ChannelSelectMenuBuilder() - .setCustomId('ticket_cfg_transcript_channel') - .setPlaceholder('Select a channel...') - .addChannelTypes(ChannelType.GuildText) - .setMaxValues(1); - - await selectInteraction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle('📜 Select Transcript Channel') - .setDescription('Choose where auto-generated transcripts will be sent when tickets are deleted.') - .setColor(getColor('info')) - ], - components: [new ActionRowBuilder().addComponents(channelSelect)], - flags: MessageFlags.Ephemeral - }); - - const collector = rootInteraction.channel.createMessageComponentCollector({ - componentType: ComponentType.ChannelSelect, - filter: i => i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_transcript_channel', - time: 60_000, - max: 1 - }); - - collector.on('collect', async channelInteraction => { - await channelInteraction.deferUpdate(); - const channel = channelInteraction.channels.first(); - - guildConfig.ticketTranscriptChannelId = channel.id; - await client.db.set(getGuildConfigKey(guildId), guildConfig); - - await channelInteraction.followUp({ - embeds: [successEmbed('✅ Transcript Channel Updated', `Transcripts will be sent to ${channel}`)], - flags: MessageFlags.Ephemeral - }); - - await refreshDashboard(rootInteraction, guildConfig, guildId); - }); - - collector.on('end', (collected, reason) => { - if (reason === 'time' && collected.size === 0) { - selectInteraction.followUp({ - embeds: [errorEmbed('Timed Out', 'No channel selected. No changes were made.')], - flags: MessageFlags.Ephemeral - }).catch(() => {}); - } - }); -} - -// ─── Check User Tickets ─────────────────────────────────────────────────────── - -async function handleCheckUser(selectInteraction, rootInteraction, guildConfig, guildId, client) { - await selectInteraction.deferUpdate(); - - const userSelect = new UserSelectMenuBuilder() - .setCustomId('ticket_cfg_check_user') - .setPlaceholder('Select a user to check...') - .setMaxValues(1); - - const row = new ActionRowBuilder().addComponents(userSelect); - - await selectInteraction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle('🔍 Check User Tickets') - .setDescription('Select a user to view their current open ticket count.') - .setColor(getColor('info')), - ], - components: [row], - flags: MessageFlags.Ephemeral, - }); - - const userCollector = rootInteraction.channel.createMessageComponentCollector({ - componentType: ComponentType.UserSelect, - filter: i => - i.user.id === selectInteraction.user.id && i.customId === 'ticket_cfg_check_user', - time: 60_000, - max: 1, - }); - - userCollector.on('collect', async userInteraction => { - await userInteraction.deferUpdate(); - const targetUser = userInteraction.users.first(); - const maxTickets = guildConfig.maxTicketsPerUser || 3; - const openCount = await getUserTicketCount(guildId, targetUser.id); - const atLimit = openCount >= maxTickets; - - await userInteraction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle(`🎫 Ticket Check — ${targetUser.username}`) - .setDescription( - `**Open Tickets:** ${openCount} / ${maxTickets}\n` + - `**Remaining:** ${Math.max(0, maxTickets - openCount)}\n\n` + - (atLimit - ? '⚠️ This user has reached their ticket limit.' - : '✅ This user can still open more tickets.'), - ) - .setColor(atLimit ? getColor('error') : getColor('success')) - .setThumbnail(targetUser.displayAvatarURL({ size: 64 })) - .setTimestamp(), - ], - flags: MessageFlags.Ephemeral, - }); - }); - - userCollector.on('end', (collected, reason) => { - if (reason === 'time' && collected.size === 0) { - selectInteraction - .followUp({ - embeds: [errorEmbed('Timed Out', 'No user was selected.')], - flags: MessageFlags.Ephemeral, - }) - .catch(() => {}); - } - }); -} - -// ─── Delete Ticket System ───────────────────────────────────────────────────── - -async function handleDeleteSystem(btnInteraction, rootInteraction, guildConfig, guildId, client) { - const deleteModal = new ModalBuilder() - .setCustomId('ticket_delete_confirm_modal') - .setTitle('Delete Ticket System') - .addComponents( - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId('delete_confirmation') - .setLabel('Type "DELETE" to confirm') - .setStyle(TextInputStyle.Short) - .setPlaceholder('DELETE') - .setMaxLength(6) - .setMinLength(6) - .setRequired(true) - ) - ); - - await btnInteraction.showModal(deleteModal); - - const submitted = await btnInteraction - .awaitModalSubmit({ - filter: i => i.customId === 'ticket_delete_confirm_modal' && i.user.id === btnInteraction.user.id, - time: 120_000, - }) - .catch(() => null); - - if (!submitted) { - await refreshDashboard(rootInteraction, guildConfig, guildId); - return; - } - - const confirmation = submitted.fields.getTextInputValue('delete_confirmation').trim(); - - if (confirmation !== 'DELETE') { - await submitted.reply({ - embeds: [errorEmbed('Incorrect Confirmation', 'You must type "DELETE" exactly to confirm deletion.')], - flags: MessageFlags.Ephemeral, - }); - await refreshDashboard(rootInteraction, guildConfig, guildId); - return; - } - - await submitted.deferUpdate(); - - const keysToDelete = [ - 'ticketPanelChannelId', - 'ticketPanelMessageId', - 'ticketStaffRoleId', - 'ticketCategoryId', - 'ticketClosedCategoryId', - 'ticketPanelMessage', - 'ticketButtonLabel', - 'maxTicketsPerUser', - 'dmOnClose', - ]; - - // Delete the panel embed from Discord - if (guildConfig.ticketPanelChannelId) { - try { - const panelChannel = await client.guilds.cache.get(guildId)?.channels.fetch(guildConfig.ticketPanelChannelId).catch(() => null); - if (panelChannel) { - if (guildConfig.ticketPanelMessageId) { - const panelMessage = await panelChannel.messages.fetch(guildConfig.ticketPanelMessageId).catch(() => null); - if (panelMessage) await panelMessage.delete().catch(() => {}); - } else { - // Fallback: scan for the panel by button customId - const messages = await panelChannel.messages.fetch({ limit: 50 }).catch(() => null); - if (messages) { - const found = messages.find( - m => m.author.id === client.user.id && - m.components?.[0]?.components?.[0]?.customId === 'create_ticket' - ); - if (found) await found.delete().catch(() => {}); - } - } - } - } catch (panelDeleteError) { - logger.warn('Could not delete ticket panel message:', panelDeleteError.message); - } - } - - // Clear all open ticket records for the guild from the database - try { - const { pgConfig } = await import('../../../config/postgres.js'); - if (client.db?.db?.pool && typeof client.db.db.isAvailable === 'function' && client.db.db.isAvailable()) { - await client.db.db.pool.query( - `DELETE FROM ${pgConfig.tables.tickets} WHERE guild_id = $1`, - [guildId] - ); - } - } catch (ticketDeleteError) { - logger.warn('Could not clear ticket records from database:', ticketDeleteError.message); - } - - for (const key of keysToDelete) { - delete guildConfig[key]; - } - await client.db.set(getGuildConfigKey(guildId), guildConfig); - - await submitted.followUp({ - embeds: [ - successEmbed( - '✅ Ticket System Deleted', - 'All ticket system configuration has been cleared. Run `/ticket setup` to set it up again.', - ), - ], - flags: MessageFlags.Ephemeral, - }); - - await InteractionHelper.safeEditReply(rootInteraction, { - embeds: [ - new EmbedBuilder() - .setTitle('🗑️ Ticket System Deleted') - .setDescription('The ticket system configuration has been cleared.') - .setColor(getColor('error')) - .setTimestamp(), - ], - components: [], - }).catch(() => {}); -} \ No newline at end of file diff --git a/src/commands/Ticket/priority.js b/src/commands/Ticket/priority.js deleted file mode 100644 index 113bef5ee..000000000 --- a/src/commands/Ticket/priority.js +++ /dev/null @@ -1,119 +0,0 @@ -import { getColor } from '../../config/bot.js'; -import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; -import { errorEmbed, successEmbed } from '../../utils/embeds.js'; -import { logger } from '../../utils/logger.js'; -import { handleInteractionError } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { getTicketPermissionContext } from '../../utils/ticketPermissions.js'; -import { updateTicketPriority } from '../../services/ticket.js'; - -export default { - data: new SlashCommandBuilder() - .setName("priority") - .setDescription("Sets the priority level for the current support ticket.") - .addStringOption((option) => - option - .setName("level") - .setDescription("The priority level for the ticket.") - .setRequired(true) - .addChoices( - { name: "🔴 Urgent", value: "urgent" }, - { name: "🟠 High", value: "high" }, - { name: "🟡 Medium", value: "medium" }, - { name: "🟢 Low", value: "low" }, - { name: "⚪ None", value: "none" }, - ), - ) - .setDMPermission(false), - category: "Ticket", - - async execute(interaction, guildConfig, client) { - try { - - const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - if (!deferred) { - return; - } - - const permissionContext = await getTicketPermissionContext({ client, interaction }); - if (!permissionContext.ticketData) { - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - "Not a Ticket Channel", - "This command can only be used in a valid ticket channel.", - ), - ], - }); - } - - if (!permissionContext.canManageTicket) { - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - "Permission Denied", - "You need the `Manage Channels` permission or the configured `Ticket Staff Role` to change ticket priority.", - ), - ], - }); - } - - const priorityLevel = interaction.options.getString("level"); - const result = await updateTicketPriority(interaction.channel, priorityLevel, interaction.user); - - if (!result.success) { - logger.warn('Priority update failed - not a valid ticket channel', { - userId: interaction.user.id, - channelId: interaction.channel.id, - guildId: interaction.guildId, - error: result.error - }); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - "Not a Ticket Channel", - result.error || "This command can only be used in a valid ticket channel.", - ), - ], - }); - } - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - "Priority Updated", - `Ticket priority set to **${priorityLevel.toUpperCase()}**.`, - ), - ], - }); - - logger.info('Ticket priority updated successfully', { - userId: interaction.user.id, - userTag: interaction.user.tag, - channelId: interaction.channel.id, - channelName: interaction.channel.name, - guildId: interaction.guildId, - priority: priorityLevel, - commandName: 'priority' - }); - - } catch (error) { - logger.error('Error executing priority command', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - channelId: interaction.channel?.id, - guildId: interaction.guildId, - commandName: 'priority' - }); - await handleInteractionError(interaction, error, { - commandName: 'priority', - source: 'ticket_priority_command' - }); - } - }, -}; - - - - diff --git a/src/commands/Ticket/ticket.js b/src/commands/Ticket/ticket.js deleted file mode 100644 index e3a1a5015..000000000 --- a/src/commands/Ticket/ticket.js +++ /dev/null @@ -1,336 +0,0 @@ -import { getColor } from '../../config/bot.js'; -import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getGuildConfig } from '../../services/guildConfig.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { logger } from '../../utils/logger.js'; -import { handleInteractionError } from '../../utils/errorHandler.js'; - -import ticketConfig from './modules/ticket_dashboard.js'; - -export default { - data: new SlashCommandBuilder() - .setName("ticket") - .setDescription("Manages the server's ticket system.") - .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels) - .addSubcommand((subcommand) => - subcommand - .setName("setup") - .setDescription( - "Sets up the ticket creation panel in a specified channel.", - ) - .addChannelOption((option) => - option -.setName("panel_channel") - .setDescription( - "The channel where the ticket panel will be sent.", - ) - .addChannelTypes(ChannelType.GuildText) - .setRequired(true), - ) - - .addStringOption((option) => - option - .setName("panel_message") - .setDescription( - "The main message/description for the ticket panel.", - ) - .setRequired(true), - ) - .addStringOption((option) => - option - .setName("button_label") - .setDescription( - "The label for the ticket creation button (default: Create Ticket)", - ) - .setRequired(false), - ) - .addChannelOption((option) => - option - .setName("category") - .setDescription( - "The category where new tickets will be created (optional).", - ) - .addChannelTypes(ChannelType.GuildCategory) - .setRequired(false), - ) - .addChannelOption((option) => - option - .setName("closed_category") - .setDescription( - "The category where closed tickets will be moved (optional).", - ) - .addChannelTypes(ChannelType.GuildCategory) - .setRequired(false), - ) - .addRoleOption((option) => - option - .setName("staff_role") - .setDescription( - "The role that can access tickets (optional).", - ) - .setRequired(false), - ) - .addIntegerOption((option) => - option - .setName("max_tickets_per_user") - .setDescription("Maximum number of tickets a user can create (default: 3)") - .setMinValue(1) - .setMaxValue(10) - .setRequired(false), - ) - .addBooleanOption((option) => - option - .setName("dm_on_close") - .setDescription("Send DM to user when their ticket is closed (default: true)") - .setRequired(false), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("dashboard") - .setDescription("Open the interactive ticket system dashboard"), - ), - category: "ticket", - - async execute(interaction, config, client) { - try { - - const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - if (!deferred) { - return; - } - - if ( - !interaction.member.permissions.has( - PermissionFlagsBits.ManageChannels, - ) - ) { - logger.warn('Ticket command permission denied', { - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'ticket' - }); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - "Permission Denied", - "You need the `Manage Channels` permission for this action.", - ), - ], - }); - } - - const subcommand = interaction.options.getSubcommand(); - - if (subcommand === "dashboard") { - return ticketConfig.execute(interaction, config, client); - } - - if (subcommand === "setup") { - const existingConfig = await getGuildConfig(client, interaction.guildId); - if (existingConfig?.ticketPanelChannelId) { - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - 'Ticket System Already Active', - `This server already has a ticket system set up (panel in <#${existingConfig.ticketPanelChannelId}>).\n\nOnly one ticket system is supported per server. Use \`/ticket dashboard\` to edit or update the existing setup, or select **Delete System** from the dashboard to remove it and start fresh.`, - ), - ], - }); - } - - const panelChannel = - interaction.options.getChannel("panel_channel"); - const categoryChannel = interaction.options.getChannel("category"); - const closedCategoryChannel = interaction.options.getChannel("closed_category"); - const staffRole = interaction.options.getRole("staff_role"); -const panelMessage = interaction.options.getString("panel_message") || "Click the button below to create a support ticket."; - const buttonLabel = - interaction.options.getString("button_label") || -"Create Ticket"; - const maxTicketsPerUser = interaction.options.getInteger("max_tickets_per_user") || 3; -const dmOnClose = interaction.options.getBoolean("dm_on_close") !== false; - - const setupEmbed = createEmbed({ - title: "🎫 Support Tickets", -description: panelMessage, - color: getColor('info') - }); - - const ticketButton = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("create_ticket") -.setLabel(buttonLabel) - .setStyle(ButtonStyle.Primary) - .setEmoji("📩"), - ); - - try { - await panelChannel.send({ - embeds: [setupEmbed], - components: [ticketButton], - }); - - if (client.db && interaction.guildId) { - const currentConfig = existingConfig; - currentConfig.ticketCategoryId = categoryChannel ? categoryChannel.id : null; - currentConfig.ticketClosedCategoryId = closedCategoryChannel ? closedCategoryChannel.id : null; - currentConfig.ticketStaffRoleId = staffRole ? staffRole.id : null; - currentConfig.ticketPanelChannelId = panelChannel.id; - currentConfig.ticketPanelMessage = panelMessage; - currentConfig.ticketButtonLabel = buttonLabel; - currentConfig.maxTicketsPerUser = maxTicketsPerUser; - currentConfig.dmOnClose = dmOnClose; - - const { getGuildConfigKey } = await import('../../utils/database.js'); - const configKey = getGuildConfigKey(interaction.guildId); - await client.db.set(configKey, currentConfig); - logger.info('Ticket configuration saved', { - guildId: interaction.guildId, - categoryId: categoryChannel?.id, - closedCategoryId: closedCategoryChannel?.id, - staffRoleId: staffRole?.id, - maxTickets: maxTicketsPerUser, - dmOnClose: dmOnClose - }); - } - - let successMessage = `The ticket creation panel has been sent to ${panelChannel}. `; - - if (categoryChannel) { - successMessage += `New tickets will be created in the **${categoryChannel.name}** category. `; - } else { - successMessage += 'New tickets will be created in a new "Tickets" category. '; - } - - if (closedCategoryChannel) { - successMessage += `Closed tickets will be moved to **${closedCategoryChannel.name}**. `; - } - - if (staffRole) { - successMessage += `**${staffRole.name}** role will have access to tickets. `; - } - - successMessage += `\n\n**Max Tickets Per User:** ${maxTicketsPerUser}\n**DM on Close:** ${dmOnClose ? 'Enabled' : 'Disabled'}`; - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - "Ticket Panel Set Up", - successMessage, - ), - ], - }); - - logger.info('Ticket panel setup completed', { - userId: interaction.user.id, - userTag: interaction.user.tag, - guildId: interaction.guildId, - panelChannelId: panelChannel.id, - categoryId: categoryChannel?.id, - closedCategoryId: closedCategoryChannel?.id, - staffRoleId: staffRole?.id, - maxTickets: maxTicketsPerUser, - dmOnClose: dmOnClose, - commandName: 'ticket_setup' - }); - - const logEmbed = createEmbed({ - title: "🔧 Ticket System Setup (Configuration Log)", - description: `The ticket panel was set up in ${panelChannel} by ${interaction.user}.`, - color: getColor('warning') - }) - .addFields( - { - name: "Panel Channel", - value: panelChannel.toString(), - inline: true, - }, - { - name: "Ticket Category", - value: categoryChannel - ? categoryChannel.toString() - : "None specified.", - inline: true, - }, - { - name: "Closed Category", - value: closedCategoryChannel - ? closedCategoryChannel.toString() - : "None specified.", - inline: true, - }, - { - name: "Staff Role", - value: staffRole - ? staffRole.toString() - : "None specified.", - inline: true, - }, - { - name: "Max Tickets Per User", - value: maxTicketsPerUser.toString(), - inline: true, - }, - { - name: "DM on Close", - value: dmOnClose ? 'Enabled' : 'Disabled', - inline: true, - }, - { - name: "Moderator", - value: `${interaction.user.tag} (${interaction.user.id})`, - inline: false, - }, - ); - - - } catch (error) { - logger.error('Ticket setup error', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'ticket_setup' - }); - if (interaction.deferred || interaction.replied) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - errorEmbed( - "Setup Failed", - "Could not send the ticket panel or save configuration. Check the bot's permissions (especially the ability to send messages in the target channel) and database connection.", - ), - ], - }).catch(err => { - logger.error('Failed to send error reply', { - error: err.message, - guildId: interaction.guildId - }); - }); - } else { - await handleInteractionError(interaction, error, { - commandName: 'ticket_setup', - source: 'ticket_setup_command' - }); - } - } - } - } catch (error) { - logger.error('Error executing ticket command', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'ticket' - }); - await handleInteractionError(interaction, error, { - commandName: 'ticket', - source: 'ticket_command_main' - }); - } - } -}; - - - From d7ca2f5fd76151a4d309f6c4d3cab6537046e238 Mon Sep 17 00:00:00 2001 From: AdenTan123 Date: Mon, 8 Jun 2026 16:15:13 +0800 Subject: [PATCH 2/6] Update bot configuration and messages --- src/config/bot.js | 144 ++++++++++++++++++++++------------------------ 1 file changed, 70 insertions(+), 74 deletions(-) diff --git a/src/config/bot.js b/src/config/bot.js index 36e588cd4..b01953b1b 100644 --- a/src/config/bot.js +++ b/src/config/bot.js @@ -25,7 +25,7 @@ export const botConfig = { activities: [ { // Text users will see (example: "Playing /help | Titan Bot"). - name: "Made with ❤️", + name: "h. jack is cool trust", // Activity type number (0 = Playing). type: 0, }, @@ -63,9 +63,9 @@ export const botConfig = { // Embed colors by application status. statusColors: { - pending: "#FFA500", - approved: "#00FF00", - denied: "#FF0000", + pending: "#D98A00", +approved: "#00CC00", +denied: "#CC0000", }, // How long users must wait before submitting another application (hours). @@ -85,70 +85,66 @@ export const botConfig = { // EMBED COLORS & BRANDING // ========================= // IMPORTANT: This is the SINGLE SOURCE OF TRUTH for all bot colors - embeds: { - colors: { - // Main brand colors. - primary: "#336699", - secondary: "#2F3136", - - // Standard status colors for success/error/warning/info messages. - success: "#57F287", - error: "#ED4245", - warning: "#FEE75C", - info: "#3498DB", - - // Neutral utility colors. - light: "#FFFFFF", - dark: "#202225", - gray: "#99AAB5", - - // Discord-style palette shortcuts. - blurple: "#5865F2", - green: "#57F287", - yellow: "#FEE75C", - fuchsia: "#EB459E", - red: "#ED4245", - black: "#000000", - - // Feature-specific colors. - giveaway: { - active: "#57F287", - ended: "#ED4245", - }, - ticket: { - open: "#57F287", - claimed: "#FAA61A", - closed: "#ED4245", - pending: "#99AAB5", - }, - economy: "#F1C40F", - birthday: "#E91E63", - moderation: "#9B59B6", - - // Ticket priority color mapping. - priority: { - none: "#95A5A6", - low: "#3498db", - medium: "#2ecc71", - high: "#f1c40f", - urgent: "#e74c3c", - }, +embeds: { + colors: { + // Main brand colors. + primary: "#336699", + secondary: "#2F3136", + + // Standard status colors for success/error/warning/info messages. + success: "#57F287", + error: "#ED4245", + warning: "#FEE75C", + info: "#3498DB", + + // Neutral utility colors. + light: "#FFFFFF", + dark: "#202225", + gray: "#99AAB5", + + // Discord-style palette shortcuts. + blurple: "#5865F2", + green: "#57F287", + yellow: "#FEE75C", + fuchsia: "#EB459E", + red: "#ED4245", + black: "#000000", + + // Feature-specific colors. + giveaway: { + active: "#57F287", + ended: "#ED4245", }, - footer: { - // Default footer text used in bot embeds. - text: "Titan Bot", - // Footer icon URL (null = no icon). - icon: null, + ticket: { + open: "#57F287", + claimed: "#FAA61A", + closed: "#ED4245", + pending: "#D98A00", // was #FFA500 }, - // Default thumbnail URL for embeds (null = no thumbnail). - thumbnail: null, - author: { - // Optional default embed author block. - name: null, - icon: null, - url: null, + economy: "#F1C40F", + birthday: "#E91E63", + moderation: "#9B59B6", + + // Ticket priority color mapping. + priority: { + none: "#95A5A6", + low: "#3498db", + medium: "#2ecc71", + high: "#f1c40f", + urgent: "#e74c3c", }, }, + footer: { + text: "Titan Bot", + icon: null, + }, + thumbnail: null, + author: { + name: null, + icon: null, + url: null, + }, +}, // ========================= // ECONOMY SETTINGS @@ -156,9 +152,9 @@ export const botConfig = { economy: { currency: { // Currency display name. - name: "coins", + name: "jackcoin", // Plural display name. - namePlural: "coins", + namePlural: "jackcoins", // Currency symbol shown in balances. symbol: "$", }, @@ -421,13 +417,13 @@ export const botConfig = { // GENERIC BOT MESSAGES // ========================= messages: { - noPermission: "You do not have permission to use this command.", - cooldownActive: "Please wait {time} before using this command again.", - errorOccurred: "An error occurred while executing this command.", + noPermission: "Uh-oh! You do not have permission to use this command.", + cooldownActive: "Whoa, You're too fast! Please wait {time} before using this command again.", + errorOccurred: "Uh-oh! An error occurred while executing this command.", missingPermissions: - "I am missing required permissions to perform this action.", - commandDisabled: "This command has been disabled.", - maintenanceMode: "The bot is currently in maintenance mode.", + "Aw man... I am missing required permissions to perform this action!", + commandDisabled: "Too bad! This command has been disabled.", + maintenanceMode: "Hey the bot is currently in maintenance mode. (sleeping)", }, // ========================= @@ -436,14 +432,14 @@ export const botConfig = { // Set any feature to `false` to disable it globally. features: { // Core systems. - economy: true, + economy: false, leveling: true, moderation: true, logging: true, welcome: true, // Community engagement systems. - tickets: true, + tickets: false, giveaways: true, birthday: true, counter: true, From 72e8d3fe5b6c4048b091f01f1277a0b19de9b2d1 Mon Sep 17 00:00:00 2001 From: AdenTan123 Date: Mon, 8 Jun 2026 16:31:19 +0800 Subject: [PATCH 3/6] Delete src/commands/Economy directory --- src/commands/Economy/balance.js | 86 -------- src/commands/Economy/beg.js | 103 ---------- src/commands/Economy/buy.js | 163 --------------- src/commands/Economy/crime.js | 122 ----------- src/commands/Economy/daily.js | 107 ---------- src/commands/Economy/deposit.js | 144 ------------- src/commands/Economy/eleaderboard.js | 94 --------- src/commands/Economy/fish.js | 135 ------------ src/commands/Economy/gamble.js | 136 ------------ src/commands/Economy/inventory.js | 74 ------- src/commands/Economy/mine.js | 98 --------- src/commands/Economy/modules/shop_browse.js | 90 -------- .../Economy/modules/shop_config_setrole.js | 36 ---- src/commands/Economy/pay.js | 158 -------------- src/commands/Economy/rob.js | 156 -------------- src/commands/Economy/shop.js | 60 ------ src/commands/Economy/slut.js | 193 ------------------ src/commands/Economy/withdraw.js | 86 -------- src/commands/Economy/work.js | 127 ------------ 19 files changed, 2168 deletions(-) delete mode 100644 src/commands/Economy/balance.js delete mode 100644 src/commands/Economy/beg.js delete mode 100644 src/commands/Economy/buy.js delete mode 100644 src/commands/Economy/crime.js delete mode 100644 src/commands/Economy/daily.js delete mode 100644 src/commands/Economy/deposit.js delete mode 100644 src/commands/Economy/eleaderboard.js delete mode 100644 src/commands/Economy/fish.js delete mode 100644 src/commands/Economy/gamble.js delete mode 100644 src/commands/Economy/inventory.js delete mode 100644 src/commands/Economy/mine.js delete mode 100644 src/commands/Economy/modules/shop_browse.js delete mode 100644 src/commands/Economy/modules/shop_config_setrole.js delete mode 100644 src/commands/Economy/pay.js delete mode 100644 src/commands/Economy/rob.js delete mode 100644 src/commands/Economy/shop.js delete mode 100644 src/commands/Economy/slut.js delete mode 100644 src/commands/Economy/withdraw.js delete mode 100644 src/commands/Economy/work.js diff --git a/src/commands/Economy/balance.js b/src/commands/Economy/balance.js deleted file mode 100644 index 16cd240eb..000000000 --- a/src/commands/Economy/balance.js +++ /dev/null @@ -1,86 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, getMaxBankCapacity } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -export default { - data: new SlashCommandBuilder() - .setName('balance') - .setDescription("Check your or someone else's balance") - .addUserOption(option => - option - .setName('user') - .setDescription('User to check balance for') - .setRequired(false) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const targetUser = interaction.options.getUser("user") || interaction.user; - const guildId = interaction.guildId; - - logger.debug(`[ECONOMY] Balance check for ${targetUser.id}`, { userId: targetUser.id, guildId }); - - if (targetUser.bot) { - throw createError( - "Bot user queried for balance", - ErrorTypes.VALIDATION, - "Bots don't have an economy balance." - ); - } - - const userData = await getEconomyData(client, guildId, targetUser.id); - - if (!userData) { - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load economy data. Please try again later.", - { userId: targetUser.id, guildId } - ); - } - - const maxBank = getMaxBankCapacity(userData); - - const wallet = typeof userData.wallet === 'number' ? userData.wallet : 0; - const bank = typeof userData.bank === 'number' ? userData.bank : 0; - - const embed = createEmbed({ - title: `💰 ${targetUser.username}'s Balance`, - description: `Here is the current financial status for ${targetUser.username}.`, - }) - .addFields( - { - name: "💵 Cash", - value: `$${wallet.toLocaleString()}`, - inline: true, - }, - { - name: "🏦 Bank", - value: `$${bank.toLocaleString()} / $${maxBank.toLocaleString()}`, - inline: true, - }, - { - name: "💎 Total", - value: `$${(wallet + bank).toLocaleString()}`, - inline: true, - } - ) - .setFooter({ - text: `Requested by ${interaction.user.tag}`, - iconURL: interaction.user.displayAvatarURL(), - }); - - logger.info(`[ECONOMY] Balance retrieved`, { userId: targetUser.id, wallet, bank }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'balance' }) -}; - - - - diff --git a/src/commands/Economy/beg.js b/src/commands/Economy/beg.js deleted file mode 100644 index 260ebc656..000000000 --- a/src/commands/Economy/beg.js +++ /dev/null @@ -1,103 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { botConfig } from '../../config/bot.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { MessageTemplates } from '../../utils/messageTemplates.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const COOLDOWN = 30 * 60 * 1000; -const MIN_WIN = 50; -const MAX_WIN = 200; -const SUCCESS_CHANCE = 0.7; - -export default { - data: new SlashCommandBuilder() - .setName('beg') - .setDescription('Beg for a small amount of money'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - - let userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - const lastBeg = userData.lastBeg || 0; - const remainingTime = lastBeg + COOLDOWN - Date.now(); - - if (remainingTime > 0) { - const minutes = Math.floor(remainingTime / 60000); - const seconds = Math.floor((remainingTime % 60000) / 1000); - - let timeMessage = - minutes > 0 ? `${minutes} minute(s)` : `${seconds} second(s)`; - - throw createError( - "Beg cooldown active", - ErrorTypes.RATE_LIMIT, - `You are tired from begging! Try again in **${timeMessage}**.`, - { remainingTime, minutes, seconds, cooldownType: 'beg' } - ); - } - - const success = Math.random() < SUCCESS_CHANCE; - - let replyEmbed; - let newCash = userData.wallet; - - if (success) { - const amountWon = - Math.floor(Math.random() * (MAX_WIN - MIN_WIN + 1)) + MIN_WIN; - - newCash += amountWon; - - const successMessages = [ - `A kind stranger drops **$${amountWon.toLocaleString()}** into your cup.`, - `You spotted an unattended wallet! You grab **$${amountWon.toLocaleString()}** and run.`, - `Someone took pity on you and gave you **$${amountWon.toLocaleString()}**!`, - `You found **$${amountWon.toLocaleString()}** under a park bench.`, - ]; - - replyEmbed = MessageTemplates.SUCCESS.DATA_UPDATED( - "begging", - successMessages[ - Math.floor(Math.random() * successMessages.length) - ] - ); - } else { - const failMessages = [ - "The police chased you off. You got nothing.", - "Someone yelled, 'Get a job!' and walked past.", - "A squirrel stole the single coin you had.", - "You tried to beg, but you were too embarrassed and gave up.", - ]; - - replyEmbed = MessageTemplates.ERRORS.INSUFFICIENT_FUNDS( - "nothing", - "You failed to get any money from begging." - ); - replyEmbed.data.description = failMessages[Math.floor(Math.random() * failMessages.length)]; - } - - userData.wallet = newCash; -userData.lastBeg = Date.now(); - - await setEconomyData(client, guildId, userId, userData); - - await InteractionHelper.safeEditReply(interaction, { embeds: [replyEmbed] }); - }, { command: 'beg' }) -}; - - diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js deleted file mode 100644 index 72f4c62bb..000000000 --- a/src/commands/Economy/buy.js +++ /dev/null @@ -1,163 +0,0 @@ -import { SlashCommandBuilder, MessageFlags } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { shopItems } from '../../config/shop/items.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { getGuildConfig } from '../../services/guildConfig.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { MessageTemplates } from '../../utils/messageTemplates.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const SHOP_ITEMS = shopItems; - -export default { - data: new SlashCommandBuilder() - .setName('buy') - .setDescription('Buy an item from the shop') - .addStringOption(option => - option - .setName('item_id') - .setDescription('ID of the item to buy') - .setRequired(true) - ) - .addIntegerOption(option => - option - .setName('quantity') - .setDescription('Quantity to buy (default: 1)') - .setRequired(false) - .setMinValue(1) - .setMaxValue(10) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const itemId = interaction.options.getString("item_id").toLowerCase(); - const quantity = interaction.options.getInteger("quantity") || 1; - - const item = SHOP_ITEMS.find(i => i.id === itemId); - - if (!item) { - throw createError( - `Item ${itemId} not found`, - ErrorTypes.VALIDATION, - `The item ID \`${itemId}\` does not exist in the shop.`, - { itemId } - ); - } - - if (quantity < 1) { - throw createError( - "Invalid quantity", - ErrorTypes.VALIDATION, - "You must purchase a quantity of 1 or more.", - { quantity } - ); - } - - const totalCost = item.price * quantity; - - const guildConfig = await getGuildConfig(client, guildId); - const PREMIUM_ROLE_ID = guildConfig.premiumRoleId; - - const userData = await getEconomyData(client, guildId, userId); - - if (userData.wallet < totalCost) { - throw createError( - "Insufficient funds", - ErrorTypes.VALIDATION, - `You need **$${totalCost.toLocaleString()}** to purchase ${quantity}x **${item.name}**, but you only have **$${userData.wallet.toLocaleString()}** in cash.`, - { required: totalCost, current: userData.wallet, itemId, quantity } - ); - } - - if (item.type === "role" && itemId === "premium_role") { - if (!PREMIUM_ROLE_ID) { - throw createError( - "Premium role not configured", - ErrorTypes.CONFIGURATION, - "The **Premium Shop Role** has not been configured by a server administrator yet.", - { itemId } - ); - } - if (interaction.member.roles.cache.has(PREMIUM_ROLE_ID)) { - throw createError( - "Role already owned", - ErrorTypes.VALIDATION, - `You already have the **${item.name}** role.`, - { itemId, roleId: PREMIUM_ROLE_ID } - ); - } - if (quantity > 1) { - throw createError( - "Invalid quantity for role", - ErrorTypes.VALIDATION, - `You can only purchase the **${item.name}** role once.`, - { itemId, quantity } - ); - } - } - - userData.wallet -= totalCost; - - let successDescription = `You successfully purchased ${quantity}x **${item.name}** for **$${totalCost.toLocaleString()}**!`; - - if (item.type === "role" && itemId === "premium_role") { - const member = interaction.member; - - const role = interaction.guild.roles.cache.get(PREMIUM_ROLE_ID); - - if (!role) { - throw createError( - "Role not found", - ErrorTypes.CONFIGURATION, - "The configured premium role no longer exists in this guild.", - { roleId: PREMIUM_ROLE_ID } - ); - } - - try { - await member.roles.add( - role, - `Purchased role: ${item.name}`, - ); - successDescription += `\n\n**👑 The role ${role.toString()} has been granted to you!**`; - } catch (roleError) { - userData.wallet += totalCost; - await setEconomyData(client, guildId, userId, userData); - throw createError( - "Role assignment failed", - ErrorTypes.DISCORD_API, - "Successfully deducted money, but failed to grant the role. Your cash has been refunded.", - { roleId: PREMIUM_ROLE_ID, originalError: roleError.message } - ); - } - } else if (item.type === "upgrade") { - userData.upgrades[itemId] = true; - successDescription += `\n\n**✨ Your upgrade is now active!**`; - } else if (item.type === "consumable") { - userData.inventory[itemId] = - (userData.inventory[itemId] || 0) + quantity; - } - - await setEconomyData(client, guildId, userId, userData); - - const embed = successEmbed( - "💰 Purchase Successful", - successDescription, - ).addFields({ - name: "New Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed], flags: [MessageFlags.Ephemeral] }); - }, { command: 'buy' }) -}; - - - - - diff --git a/src/commands/Economy/crime.js b/src/commands/Economy/crime.js deleted file mode 100644 index 2403fe461..000000000 --- a/src/commands/Economy/crime.js +++ /dev/null @@ -1,122 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { MessageTemplates } from '../../utils/messageTemplates.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const CRIME_COOLDOWN = 60 * 60 * 1000; -const MIN_CRIME_AMOUNT = 100; -const MAX_CRIME_AMOUNT = 2000; -const FAILURE_RATE = 0.4; -const JAIL_TIME = 2 * 60 * 60 * 1000; - -const CRIME_TYPES = [ - { name: "Pickpocketing", min: 100, max: 500, risk: 0.3 }, - { name: "Burglary", min: 300, max: 1000, risk: 0.4 }, - { name: "Bank Heist", min: 1000, max: 5000, risk: 0.6 }, - { name: "Art Theft", min: 2000, max: 10000, risk: 0.7 }, - { name: "Cybercrime", min: 5000, max: 20000, risk: 0.8 }, -]; - -export default { - data: new SlashCommandBuilder() - .setName('crime') - .setDescription('Commit a crime to earn money (risky)') - .addStringOption(option => - option - .setName('type') - .setDescription('Type of crime to commit') - .setRequired(true) - .addChoices( - { name: 'Pickpocketing', value: 'pickpocketing' }, - { name: 'Burglary', value: 'burglary' }, - { name: 'Bank Heist', value: 'bank-heist' }, - { name: 'Art Theft', value: 'art-theft' }, - { name: 'Cybercrime', value: 'cybercrime' }, - ) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - await InteractionHelper.safeDefer(interaction); - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - const userData = await getEconomyData(client, guildId, userId); - const lastCrime = userData.cooldowns?.crime || 0; - const isJailed = userData.jailedUntil && userData.jailedUntil > now; - - if (isJailed) { - const timeLeft = Math.ceil((userData.jailedUntil - now) / (1000 * 60)); - throw createError( - "User is in jail", - ErrorTypes.RATE_LIMIT, - `You're in jail for ${timeLeft} more minutes!`, - { jailTimeRemaining: userData.jailedUntil - now } - ); - } - - if (now < lastCrime + CRIME_COOLDOWN) { - const timeLeft = Math.ceil((lastCrime + CRIME_COOLDOWN - now) / (1000 * 60)); - throw createError( - "Crime cooldown active", - ErrorTypes.RATE_LIMIT, - `You need to wait ${timeLeft} more minutes before committing another crime.`, - { remaining: lastCrime + CRIME_COOLDOWN - now, cooldownType: 'crime' } - ); - } - - const crimeType = interaction.options.getString("type").toLowerCase(); - const crime = CRIME_TYPES.find( - c => c.name.toLowerCase().replace(/\s+/g, '-') === crimeType - ); - - if (!crime) { - throw createError( - "Invalid crime type", - ErrorTypes.VALIDATION, - "Please select a valid crime type.", - { crimeType } - ); - } - - const isSuccess = Math.random() > crime.risk; - const amountEarned = isSuccess - ? Math.floor(Math.random() * (crime.max - crime.min + 1)) + crime.min - : 0; - - userData.cooldowns = userData.cooldowns || {}; - userData.cooldowns.crime = now; - - if (isSuccess) { - userData.wallet = (userData.wallet || 0) + amountEarned; - - await setEconomyData(client, guildId, userId, userData); - - const embed = successEmbed( - "Crime Successful!", - `You successfully committed ${crime.name} and earned **${amountEarned}** coins!` - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - } else { - const fine = Math.floor(amountEarned * 0.2); - userData.wallet = Math.max(0, (userData.wallet || 0) - fine); - userData.jailedUntil = now + JAIL_TIME; - - await setEconomyData(client, guildId, userId, userData); - - const embed = errorEmbed( - "Crime Failed!", - `You were caught while attempting ${crime.name} and have been sent to jail! ` + - `You were fined ${fine} coins and will be in jail for 2 hours.` - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - } - }, { command: 'crime' }) -}; - - diff --git a/src/commands/Economy/daily.js b/src/commands/Economy/daily.js deleted file mode 100644 index ba3d9bee3..000000000 --- a/src/commands/Economy/daily.js +++ /dev/null @@ -1,107 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { getGuildConfig } from '../../services/guildConfig.js'; -import { formatDuration } from '../../utils/helpers.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; -const DAILY_AMOUNT = 1000; -const PREMIUM_BONUS_PERCENTAGE = 0.1; - -export default { - data: new SlashCommandBuilder() - .setName('daily') - .setDescription('Claim your daily cash reward'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - logger.debug(`[ECONOMY] Daily claimed started for ${userId}`, { userId, guildId }); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data for daily", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - const lastDaily = userData.lastDaily || 0; - - if (now < lastDaily + DAILY_COOLDOWN) { - const timeRemaining = lastDaily + DAILY_COOLDOWN - now; - throw createError( - "Daily cooldown active", - ErrorTypes.RATE_LIMIT, - `You need to wait before claiming daily again. Try again in **${formatDuration(timeRemaining)}**.`, - { timeRemaining, cooldownType: 'daily' } - ); - } - - const guildConfig = await getGuildConfig(client, guildId); - const PREMIUM_ROLE_ID = guildConfig.premiumRoleId; - - let earned = DAILY_AMOUNT; - let bonusMessage = ""; - let hasPremiumRole = false; - - if ( - PREMIUM_ROLE_ID && - interaction.member && - interaction.member.roles.cache.has(PREMIUM_ROLE_ID) - ) { - const bonusAmount = Math.floor( - DAILY_AMOUNT * PREMIUM_BONUS_PERCENTAGE, - ); - earned += bonusAmount; - bonusMessage = `\n✨ **Premium Bonus:** +$${bonusAmount.toLocaleString()}`; - hasPremiumRole = true; - } - - userData.wallet = (userData.wallet || 0) + earned; - userData.lastDaily = now; - - await setEconomyData(client, guildId, userId, userData); - - logger.info(`[ECONOMY_TRANSACTION] Daily claimed`, { - userId, - guildId, - amount: earned, - newWallet: userData.wallet, - hasPremium: hasPremiumRole, - timestamp: new Date().toISOString() - }); - - const embed = successEmbed( - "✅ Daily Claimed!", - `You have claimed your daily **$${earned.toLocaleString()}**!${bonusMessage}` - ) - .addFields({ - name: "New Cash Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }) - .setFooter({ - text: hasPremiumRole - ? `Next claim in 24 hours. (Premium Active)` - : `Next claim in 24 hours.`, - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'daily' }) -}; - - - - diff --git a/src/commands/Economy/deposit.js b/src/commands/Economy/deposit.js deleted file mode 100644 index 79cab9b42..000000000 --- a/src/commands/Economy/deposit.js +++ /dev/null @@ -1,144 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData, getMaxBankCapacity } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { MessageTemplates } from '../../utils/messageTemplates.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -export default { - data: new SlashCommandBuilder() - .setName('deposit') - .setDescription('Deposit money from your wallet into your bank') - .addStringOption(option => - option - .setName('amount') - .setDescription('Amount to deposit (number or "all")') - .setRequired(true) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const amountInput = interaction.options.getString("amount"); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - const maxBank = getMaxBankCapacity(userData); - let depositAmount; - - if (amountInput.toLowerCase() === "all") { - depositAmount = userData.wallet; - } else { - depositAmount = parseInt(amountInput); - - if (isNaN(depositAmount) || depositAmount <= 0) { - throw createError( - "Invalid deposit amount", - ErrorTypes.VALIDATION, - `Please enter a valid number or 'all'. You entered: \`${amountInput}\``, - { amountInput, userId } - ); - } - } - - if (depositAmount === 0) { - throw createError( - "Zero deposit amount", - ErrorTypes.VALIDATION, - "You have no cash to deposit.", - { userId, walletBalance: userData.wallet } - ); - } - - if (depositAmount > userData.wallet) { - depositAmount = userData.wallet; - await interaction.followUp({ - embeds: [ - MessageTemplates.ERRORS.INVALID_INPUT( - "deposit amount", - `You tried to deposit more than you have. Depositing your remaining cash: **$${depositAmount.toLocaleString()}**` - ) - ], - flags: ["Ephemeral"], - }); - } - - const availableSpace = maxBank - userData.bank; - - if (availableSpace <= 0) { - throw createError( - "Bank is full", - ErrorTypes.VALIDATION, - `Your bank is currently full (Max Capacity: $${maxBank.toLocaleString()}). Purchase a **Bank Upgrade** to increase your limit.`, - { maxBank, currentBank: userData.bank, userId } - ); - } - - if (depositAmount > availableSpace) { - const originalDepositAmount = depositAmount; - depositAmount = availableSpace; - - if (amountInput.toLowerCase() !== "all") { - await interaction.followUp({ - embeds: [ - MessageTemplates.ERRORS.INVALID_INPUT( - "deposit amount", - `You only had space for **$${depositAmount.toLocaleString()}** in your bank account (Max: $${maxBank.toLocaleString()}). The rest remains in your cash.` - ) - ], - flags: ["Ephemeral"], - }); - } - } - - if (depositAmount === 0) { - throw createError( - "No space or cash for deposit", - ErrorTypes.VALIDATION, - "The amount you tried to deposit was either 0 or exceeded your bank capacity after checking your cash balance.", - { depositAmount, availableSpace, walletBalance: userData.wallet } - ); - } - - userData.wallet -= depositAmount; - userData.bank += depositAmount; - - await setEconomyData(client, guildId, userId, userData); - - const embed = MessageTemplates.SUCCESS.DATA_UPDATED( - "deposit", - `You successfully deposited **$${depositAmount.toLocaleString()}** into your bank.` - ) - .addFields( - { - name: "💵 New Cash Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }, - { - name: "🏦 New Bank Balance", - value: `$${userData.bank.toLocaleString()} / $${maxBank.toLocaleString()}`, - inline: true, - }, - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'deposit' }) -}; - - - - - diff --git a/src/commands/Economy/eleaderboard.js b/src/commands/Economy/eleaderboard.js deleted file mode 100644 index d96711690..000000000 --- a/src/commands/Economy/eleaderboard.js +++ /dev/null @@ -1,94 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed } from '../../utils/embeds.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -export default { - data: new SlashCommandBuilder() - .setName("eleaderboard") - .setDescription("View the server's top 10 richest users.") - .setDMPermission(false), - - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const guildId = interaction.guildId; - - logger.debug(`[ECONOMY] Leaderboard requested`, { guildId }); - - const prefix = `economy:${guildId}:`; - - let allKeys = await client.db.list(prefix); - - if (!Array.isArray(allKeys)) { - allKeys = []; - } - - if (allKeys.length === 0) { - throw createError( - "No economy data found", - ErrorTypes.VALIDATION, - "No economy data found for this server." - ); - } - - let allUserData = []; - - for (const key of allKeys) { - const userId = key.replace(prefix, ""); - const userData = await client.db.get(key); - - if (userData) { - allUserData.push({ - userId: userId, - net_worth: (userData.wallet || 0) + (userData.bank || 0), - }); - } - } - - allUserData.sort((a, b) => b.net_worth - a.net_worth); - - const topUsers = allUserData.slice(0, 10); - const userRank = - allUserData.findIndex((u) => u.userId === interaction.user.id) + - 1; - const rankEmoji = ["🥇", "🥈", "🥉"]; - const leaderboardEntries = []; - - for (let i = 0; i < topUsers.length; i++) { - const user = topUsers[i]; - const rank = i + 1; - const emoji = rankEmoji[i] || `**#${rank}**`; - - leaderboardEntries.push( - `${emoji} <@${user.userId}> - 🏦 ${user.net_worth.toLocaleString()}`, - ); - } - - logger.info(`[ECONOMY] Leaderboard generated`, { - guildId, - userCount: allUserData.length, - userRank - }); - - const description = leaderboardEntries.length > 0 - ? leaderboardEntries.join("\n") - : "No economy data is available for this server yet."; - - const embed = createEmbed({ - title: `Economy Leaderboard`, - description, - footer: `Your Rank: ${userRank > 0 ? `#${userRank}` : "No ranking data available"}`, - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'eleaderboard' }) -}; - - - - - diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js deleted file mode 100644 index 367ad453e..000000000 --- a/src/commands/Economy/fish.js +++ /dev/null @@ -1,135 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { MessageTemplates } from '../../utils/messageTemplates.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const FISH_COOLDOWN = 45 * 60 * 1000; -const BASE_MIN_REWARD = 300; -const BASE_MAX_REWARD = 900; -const FISHING_ROD_MULTIPLIER = 1.5; - -const FISH_TYPES = [ - { name: 'Bass', emoji: '🐟', rarity: 'common' }, - { name: 'Salmon', emoji: '🐟', rarity: 'common' }, - { name: 'Trout', emoji: '🐟', rarity: 'common' }, - { name: 'Tuna', emoji: '🐟', rarity: 'uncommon' }, - { name: 'Swordfish', emoji: '🐟', rarity: 'uncommon' }, - { name: 'Octopus', emoji: '🐙', rarity: 'rare' }, - { name: 'Lobster', emoji: '🦞', rarity: 'rare' }, - { name: 'Shark', emoji: '🦈', rarity: 'epic' }, - { name: 'Whale', emoji: '🐋', rarity: 'legendary' }, -]; - -const CATCH_MESSAGES = [ - "You cast your line into the crystal clear waters...", - "You wait patiently as your bobber floats...", - "After a few minutes of waiting, you feel a tug...", - "The water ripples as something takes your bait...", - "You reel in your catch with expert precision...", -]; - -export default { - data: new SlashCommandBuilder() - .setName('fish') - .setDescription('Go fishing to catch fish and earn money'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - const userData = await getEconomyData(client, guildId, userId); - const lastFish = userData.lastFish || 0; - const hasFishingRod = userData.inventory["fishing_rod"] || 0; - - if (now < lastFish + FISH_COOLDOWN) { - const remaining = lastFish + FISH_COOLDOWN - now; - const hours = Math.floor(remaining / (1000 * 60 * 60)); - const minutes = Math.floor( - (remaining % (1000 * 60 * 60)) / (1000 * 60), - ); - - throw createError( - "Fishing cooldown active", - ErrorTypes.RATE_LIMIT, - `You're too tired to fish right now. Rest for **${hours}h ${minutes}m** before fishing again.`, - { remaining, cooldownType: 'fish' } - ); - } - - - const rand = Math.random(); - let fishCaught; - - if (rand < 0.5) { - - fishCaught = FISH_TYPES.filter(f => f.rarity === 'common')[Math.floor(Math.random() * 3)]; - } else if (rand < 0.75) { - - fishCaught = FISH_TYPES.filter(f => f.rarity === 'uncommon')[Math.floor(Math.random() * 2)]; - } else if (rand < 0.9) { - - fishCaught = FISH_TYPES.filter(f => f.rarity === 'rare')[Math.floor(Math.random() * 2)]; - } else if (rand < 0.98) { - - fishCaught = FISH_TYPES.find(f => f.rarity === 'epic'); - } else { - - fishCaught = FISH_TYPES.find(f => f.rarity === 'legendary'); - } - - const baseEarned = Math.floor( - Math.random() * (BASE_MAX_REWARD - BASE_MIN_REWARD + 1) - ) + BASE_MIN_REWARD; - - let finalEarned = baseEarned; - let multiplierMessage = ""; - - - if (hasFishingRod > 0) { - finalEarned = Math.floor(baseEarned * FISHING_ROD_MULTIPLIER); - multiplierMessage = `\n🎣 **Fishing Rod Bonus: +50%**`; - } - - const catchMessage = CATCH_MESSAGES[Math.floor(Math.random() * CATCH_MESSAGES.length)]; - - userData.wallet += finalEarned; - userData.lastFish = now; - - await setEconomyData(client, guildId, userId, userData); - - const rarityColors = { - common: '#95A5A6', - uncommon: '#2ECC71', - rare: '#3498DB', - epic: '#9B59B6', - legendary: '#F1C40F' - }; - - const embed = createEmbed({ - title: '🎣 Fishing Success!', - description: `${catchMessage}\n\nYou caught a **${fishCaught.emoji} ${fishCaught.name}**! You sold it for **$${finalEarned.toLocaleString()}**!${multiplierMessage}`, - color: rarityColors[fishCaught.rarity] - }) - .addFields( - { - name: "💵 New Cash Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }, - { - name: "🐟 Rarity", - value: fishCaught.rarity.charAt(0).toUpperCase() + fishCaught.rarity.slice(1), - inline: true, - } - ) - .setFooter({ text: `Next fishing trip available in 45 minutes.` }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'fish' }) -}; diff --git a/src/commands/Economy/gamble.js b/src/commands/Economy/gamble.js deleted file mode 100644 index d0bb7c3bd..000000000 --- a/src/commands/Economy/gamble.js +++ /dev/null @@ -1,136 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { MessageTemplates } from '../../utils/messageTemplates.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const BASE_WIN_CHANCE = 0.4; -const CLOVER_WIN_BONUS = 0.1; -const CHARM_WIN_BONUS = 0.08; -const PAYOUT_MULTIPLIER = 2.0; -const GAMBLE_COOLDOWN = 5 * 60 * 1000; - -export default { - data: new SlashCommandBuilder() - .setName('gamble') - .setDescription('Gamble your money for a chance to win more') - .addIntegerOption(option => - option - .setName('amount') - .setDescription('Amount of cash to gamble') - .setRequired(true) - .setMinValue(1) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const betAmount = interaction.options.getInteger("amount"); - const now = Date.now(); - - const userData = await getEconomyData(client, guildId, userId); - const lastGamble = userData.lastGamble || 0; - let cloverCount = userData.inventory["lucky_clover"] || 0; - let charmCount = userData.inventory["lucky_charm"] || 0; - - if (now < lastGamble + GAMBLE_COOLDOWN) { - const remaining = lastGamble + GAMBLE_COOLDOWN - now; - const minutes = Math.floor(remaining / (1000 * 60)); - const seconds = Math.floor((remaining % (1000 * 60)) / 1000); - - throw createError( - "Gamble cooldown active", - ErrorTypes.RATE_LIMIT, - `You need to cool down before gambling again. Wait **${minutes}m ${seconds}s**.`, - { remaining, cooldownType: 'gamble' } - ); - } - - if (userData.wallet < betAmount) { - throw createError( - "Insufficient cash for gamble", - ErrorTypes.VALIDATION, - `You only have $${userData.wallet.toLocaleString()} cash, but you are trying to bet $${betAmount.toLocaleString()}.`, - { required: betAmount, current: userData.wallet } - ); - } - - let winChance = BASE_WIN_CHANCE; - let cloverMessage = ""; - let usedClover = false; - let usedCharm = false; - - - if (cloverCount > 0) { - winChance += CLOVER_WIN_BONUS; - userData.inventory["lucky_clover"] -= 1; - cloverMessage = `\n🍀 **Lucky Clover Consumed:** Your win chance was boosted!`; - usedClover = true; - } - - else if (charmCount > 0) { - winChance += CHARM_WIN_BONUS; - userData.inventory["lucky_charm"] -= 1; - cloverMessage = `\n🍀 **Lucky Charm Used (${charmCount - 1} uses remaining):** Your win chance was boosted!`; - usedCharm = true; - } - - const win = Math.random() < winChance; - let cashChange = 0; - let resultEmbed; - - if (win) { - const amountWon = Math.floor(betAmount * PAYOUT_MULTIPLIER); -cashChange = amountWon; - - resultEmbed = successEmbed( - "🎉 You Won!", - `You successfully gambled and turned your **$${betAmount.toLocaleString()}** bet into **$${amountWon.toLocaleString()}**!${cloverMessage}`, - ); - } else { -cashChange = -betAmount; - - resultEmbed = errorEmbed( - "💔 You Lost...", - `The dice rolled against you. You lost your **$${betAmount.toLocaleString()}** bet.`, - ); - } - - userData.wallet = (userData.wallet || 0) + cashChange; -userData.lastGamble = now; - - await setEconomyData(client, guildId, userId, userData); - - const newCash = userData.wallet; - - resultEmbed.addFields({ - name: "💵 New Cash Balance", - value: `$${newCash.toLocaleString()}`, - inline: true, - }); - - if (usedClover) { - resultEmbed.setFooter({ - text: `You have ${userData.inventory["lucky_clover"]} Lucky Clovers left. Win chance was ${Math.round(winChance * 100)}%.`, - }); - } else if (usedCharm) { - resultEmbed.setFooter({ - text: `You have ${userData.inventory["lucky_charm"]} Lucky Charm uses left. Win chance was ${Math.round(winChance * 100)}%.`, - }); - } else { - resultEmbed.setFooter({ - text: `Next gamble available in 5 minutes. Base win chance: ${Math.round(BASE_WIN_CHANCE * 100)}%.`, - }); - } - - await InteractionHelper.safeEditReply(interaction, { embeds: [resultEmbed] }); - }, { command: 'gamble' }) -}; - - - - diff --git a/src/commands/Economy/inventory.js b/src/commands/Economy/inventory.js deleted file mode 100644 index 7369c8388..000000000 --- a/src/commands/Economy/inventory.js +++ /dev/null @@ -1,74 +0,0 @@ -import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { shopItems } from '../../config/shop/items.js'; -import { getEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const SHOP_ITEMS = shopItems; - -export default { - data: new SlashCommandBuilder() - .setName('inventory') - .setDescription('View your economy inventory'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - - logger.debug(`[ECONOMY] Inventory requested for ${userId}`, { userId, guildId }); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data for inventory", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - const inventory = userData.inventory || {}; - - let inventoryDescription = "Your inventory is currently empty."; - - if (Object.keys(inventory).length > 0) { - inventoryDescription = Object.entries(inventory) - .filter( - ([itemId, quantity]) => { - const item = SHOP_ITEMS.find(i => i.id === itemId); - return quantity > 0 && item; - } - ) - .map( - ([itemId, quantity]) => { - const item = SHOP_ITEMS.find(i => i.id === itemId); - return `**${item.name}:** ${quantity}x`; - } - ) - .join("\n"); - } - - logger.info(`[ECONOMY] Inventory retrieved`, { - userId, - guildId, - itemCount: Object.keys(inventory).length - }); - - const embed = createEmbed({ - title: `📦 ${interaction.user.username}'s Inventory`, - description: inventoryDescription, - }).setThumbnail(interaction.user.displayAvatarURL()); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'inventory' }) -}; - - - - diff --git a/src/commands/Economy/mine.js b/src/commands/Economy/mine.js deleted file mode 100644 index f8dd54cba..000000000 --- a/src/commands/Economy/mine.js +++ /dev/null @@ -1,98 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { MessageTemplates } from '../../utils/messageTemplates.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const MINE_COOLDOWN = 60 * 60 * 1000; -const BASE_MIN_REWARD = 400; -const BASE_MAX_REWARD = 1200; -const PICKAXE_MULTIPLIER = 1.2; -const DIAMOND_PICKAXE_MULTIPLIER = 2.0; - -const MINE_LOCATIONS = [ - "abandoned gold mine", - "dark, damp cave", - "backyard rock quarry", - "volcanic obsidian vent", - "deep-sea mineral trench", -]; - -export default { - data: new SlashCommandBuilder() - .setName('mine') - .setDescription('Go mining to earn money'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - const userData = await getEconomyData(client, guildId, userId); - const lastMine = userData.lastMine || 0; - const hasDiamondPickaxe = userData.inventory["diamond_pickaxe"] || 0; - const hasPickaxe = userData.inventory["pickaxe"] || 0; - - if (now < lastMine + MINE_COOLDOWN) { - const remaining = lastMine + MINE_COOLDOWN - now; - const hours = Math.floor(remaining / (1000 * 60 * 60)); - const minutes = Math.floor( - (remaining % (1000 * 60 * 60)) / (1000 * 60), - ); - - throw createError( - "Mining cooldown active", - ErrorTypes.RATE_LIMIT, - `Your pickaxe is cooling down. Wait for **${hours}h ${minutes}m** before mining again.`, - { remaining, cooldownType: 'mine' } - ); - } - - const baseEarned = - Math.floor( - Math.random() * (BASE_MAX_REWARD - BASE_MIN_REWARD + 1), - ) + BASE_MIN_REWARD; - - let finalEarned = baseEarned; - let multiplierMessage = ""; - - if (hasDiamondPickaxe > 0) { - finalEarned = Math.floor(baseEarned * DIAMOND_PICKAXE_MULTIPLIER); - multiplierMessage = `\n💎 **Diamond Pickaxe Bonus: +100%**`; - } else if (hasPickaxe > 0) { - finalEarned = Math.floor(baseEarned * PICKAXE_MULTIPLIER); - multiplierMessage = `\n⛏️ **Pickaxe Bonus: +20%**`; - } - - const location = - MINE_LOCATIONS[ - Math.floor(Math.random() * MINE_LOCATIONS.length) - ]; - - userData.wallet += finalEarned; -userData.lastMine = now; - - await setEconomyData(client, guildId, userId, userData); - - const embed = successEmbed( - "💰 Mining Expedition Successful!", - `You explored a **${location}** and managed to find minerals worth **$${finalEarned.toLocaleString()}**!${multiplierMessage}`, - ) - .addFields({ - name: "💵 New Cash Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }) - .setFooter({ text: `Next mine available in 1 hour.` }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'mine' }) -}; - - - - diff --git a/src/commands/Economy/modules/shop_browse.js b/src/commands/Economy/modules/shop_browse.js deleted file mode 100644 index 53ae62d4e..000000000 --- a/src/commands/Economy/modules/shop_browse.js +++ /dev/null @@ -1,90 +0,0 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, EmbedBuilder, MessageFlags } from 'discord.js'; -import { shopItems } from '../../../config/shop/items.js'; -import { getColor } from '../../../config/bot.js'; -import { logger } from '../../../utils/logger.js'; - -export default { - async execute(interaction, config, client) { - try { - const TARGET_MAX_PAGES = 3; - const ITEMS_PER_PAGE = Math.max(1, Math.ceil(shopItems.length / TARGET_MAX_PAGES)); - const totalPages = Math.ceil(shopItems.length / ITEMS_PER_PAGE); - let currentPage = 1; - - const createShopEmbed = (page) => { - const startIndex = (page - 1) * ITEMS_PER_PAGE; - const pageItems = shopItems.slice(startIndex, startIndex + ITEMS_PER_PAGE); - const embed = new EmbedBuilder() - .setTitle('🛒 Store') - .setColor(getColor('primary')) - .setDescription('Use `/buy item_id: quantity:` to purchase an item.'); - pageItems.forEach(item => { - embed.addFields({ - name: `${item.name} (${item.id})`, - value: `🏷️ **Type:** ${item.type}\n💚 **Price:** $${item.price.toLocaleString()}\n${item.description}`, - inline: false, - }); - }); - embed.setFooter({ text: `Page ${page}/${totalPages}` }); - return embed; - }; - - const createShopComponents = (page) => { - if (totalPages <= 1) return []; - return [ - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId('shop_prev') - .setLabel('⬅️ Previous') - .setStyle(ButtonStyle.Secondary) - .setDisabled(page === 1), - new ButtonBuilder() - .setCustomId('shop_next') - .setLabel('Next ➡️') - .setStyle(ButtonStyle.Secondary) - .setDisabled(page === totalPages), - ), - ]; - }; - - const message = await interaction.reply({ - embeds: [createShopEmbed(currentPage)], - components: createShopComponents(currentPage), - flags: 0, - }); - - const collector = message.createMessageComponentCollector({ - componentType: ComponentType.Button, - time: 300000, - }); - - collector.on('collect', async (buttonInteraction) => { - if (buttonInteraction.user.id !== interaction.user.id) { - await buttonInteraction.reply({ content: '❌ You cannot use these buttons. Run `/shop browse` to get your own shop view.', flags: 64 }); - return; - } - const { customId } = buttonInteraction; - if (customId === 'shop_prev' || customId === 'shop_next') { - await buttonInteraction.deferUpdate(); - if (customId === 'shop_prev' && currentPage > 1) currentPage--; - else if (customId === 'shop_next' && currentPage < totalPages) currentPage++; - await buttonInteraction.editReply({ - embeds: [createShopEmbed(currentPage)], - components: createShopComponents(currentPage), - }); - } - }); - - collector.on('end', async () => { - try { - const disabledComponents = createShopComponents(currentPage); - disabledComponents.forEach(row => row.components.forEach(btn => btn.setDisabled(true))); - await message.edit({ components: disabledComponents }); - } catch (_) {} - }); - } catch (error) { - logger.error('shop_browse error:', error); - await interaction.reply({ content: '❌ An error occurred while loading the shop.', flags: MessageFlags.Ephemeral }); - } - }, -}; diff --git a/src/commands/Economy/modules/shop_config_setrole.js b/src/commands/Economy/modules/shop_config_setrole.js deleted file mode 100644 index aaf52f33f..000000000 --- a/src/commands/Economy/modules/shop_config_setrole.js +++ /dev/null @@ -1,36 +0,0 @@ -import { PermissionsBitField } from 'discord.js'; -import { errorEmbed, successEmbed } from '../../../utils/embeds.js'; -import { getGuildConfig, setGuildConfig } from '../../../services/guildConfig.js'; -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -import { logger } from '../../../utils/logger.js'; - -export default { - async execute(interaction, config, client) { - if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { - return InteractionHelper.safeReply(interaction, { - embeds: [errorEmbed('Permission Denied', 'You need **Manage Server** permissions to set the premium role.')], - ephemeral: true, - }); - } - - const role = interaction.options.getRole('role'); - const guildId = interaction.guildId; - - try { - const currentConfig = await getGuildConfig(client, guildId); - currentConfig.premiumRoleId = role.id; - await setGuildConfig(client, guildId, currentConfig); - - return InteractionHelper.safeReply(interaction, { - embeds: [successEmbed('✅ Premium Role Set', `The **Premium Shop Role** has been set to ${role.toString()}. Members who purchase the Premium Role item will be granted this role.`)], - ephemeral: true, - }); - } catch (error) { - logger.error('shop_config_setrole error:', error); - return InteractionHelper.safeReply(interaction, { - embeds: [errorEmbed('System Error', 'Could not save the guild configuration.')], - ephemeral: true, - }); - } - }, -}; diff --git a/src/commands/Economy/pay.js b/src/commands/Economy/pay.js deleted file mode 100644 index 40f6424be..000000000 --- a/src/commands/Economy/pay.js +++ /dev/null @@ -1,158 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, addMoney, removeMoney, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { MessageTemplates } from '../../utils/messageTemplates.js'; -import EconomyService from '../../services/economyService.js'; - -export default { - data: new SlashCommandBuilder() - .setName('pay') - .setDescription('Pay another user some of your cash') - .addUserOption(option => - option - .setName('user') - .setDescription('User to pay') - .setRequired(true) - ) - .addIntegerOption(option => - option - .setName('amount') - .setDescription('Amount to pay') - .setRequired(true) - .setMinValue(1) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const senderId = interaction.user.id; - const receiver = interaction.options.getUser("user"); - const amount = interaction.options.getInteger("amount"); - const guildId = interaction.guildId; - - logger.debug(`[ECONOMY] Pay command initiated`, { - senderId, - receiverId: receiver.id, - amount, - guildId - }); - - if (receiver.bot) { - throw createError( - "Cannot pay bot", - ErrorTypes.VALIDATION, - "You cannot pay a bot.", - { receiverId: receiver.id, isBot: true } - ); - } - - if (receiver.id === senderId) { - throw createError( - "Cannot pay self", - ErrorTypes.VALIDATION, - "You cannot pay yourself.", - { senderId, receiverId: receiver.id } - ); - } - - if (amount <= 0) { - throw createError( - "Invalid payment amount", - ErrorTypes.VALIDATION, - "Amount must be greater than zero.", - { amount, senderId } - ); - } - - const [senderData, receiverData] = await Promise.all([ - getEconomyData(client, guildId, senderId), - getEconomyData(client, guildId, receiver.id) - ]); - - if (!senderData) { - throw createError( - "Failed to load sender economy data", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId: senderId, guildId } - ); - } - - if (!receiverData) { - throw createError( - "Failed to load receiver economy data", - ErrorTypes.DATABASE, - "Failed to load the receiver's economy data. Please try again later.", - { userId: receiver.id, guildId } - ); - } - - - - const result = await EconomyService.transferMoney( - client, - guildId, - senderId, - receiver.id, - amount - ); - - - const updatedSenderData = await getEconomyData(client, guildId, senderId); - const updatedReceiverData = await getEconomyData(client, guildId, receiver.id); - - const embed = MessageTemplates.SUCCESS.DATA_UPDATED( - "payment", - `You successfully paid **${receiver.username}** the amount of **$${amount.toLocaleString()}**!` - ) - .addFields( - { - name: "💳 Payment Amount", - value: `$${amount.toLocaleString()}`, - inline: true, - }, - { - name: "💵 Your New Balance", - value: `$${updatedSenderData.wallet.toLocaleString()}`, - inline: true, - }, - ) - .setFooter({ - text: `Paid to ${receiver.tag}`, - iconURL: receiver.displayAvatarURL(), - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - - logger.info(`[ECONOMY] Payment sent successfully`, { - senderId, - receiverId: receiver.id, - amount, - senderBalance: updatedSenderData.wallet, - receiverBalance: updatedReceiverData.wallet - }); - - try { - const receiverEmbed = createEmbed({ - title: "💰 Incoming Payment!", - description: `${interaction.user.username} paid you **$${amount.toLocaleString()}**.` - }).addFields({ - name: "Your New Cash", - value: `$${updatedReceiverData.wallet.toLocaleString()}`, - inline: true, - }); - await receiver.send({ embeds: [receiverEmbed] }); - } catch (e) { - logger.warn(`Could not DM user ${receiver.id}: ${e.message}`); - } - }, { command: 'pay' }) -}; - - - - - diff --git a/src/commands/Economy/rob.js b/src/commands/Economy/rob.js deleted file mode 100644 index d6ae21496..000000000 --- a/src/commands/Economy/rob.js +++ /dev/null @@ -1,156 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { MessageTemplates } from '../../utils/messageTemplates.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const ROB_COOLDOWN = 4 * 60 * 60 * 1000; -const BASE_ROB_SUCCESS_CHANCE = 0.25; -const ROB_PERCENTAGE = 0.15; -const FINE_PERCENTAGE = 0.1; - -export default { - data: new SlashCommandBuilder() - .setName('rob') - .setDescription('Attempt to rob another user (very risky)') - .addUserOption(option => - option - .setName('user') - .setDescription('User to rob') - .setRequired(true) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const robberId = interaction.user.id; - const victimUser = interaction.options.getUser("user"); - const guildId = interaction.guildId; - const now = Date.now(); - - if (robberId === victimUser.id) { - throw createError( - "Cannot rob self", - ErrorTypes.VALIDATION, - "You cannot rob yourself.", - { robberId, victimId: victimUser.id } - ); - } - - if (victimUser.bot) { - throw createError( - "Cannot rob bot", - ErrorTypes.VALIDATION, - "You cannot rob a bot.", - { victimId: victimUser.id, isBot: true } - ); - } - - const robberData = await getEconomyData(client, guildId, robberId); - const victimData = await getEconomyData(client, guildId, victimUser.id); - - if (!robberData || !victimData) { - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load economy data. Please try again later.", - { robberId: !!robberData, victimId: !!victimData, guildId } - ); - } - - const lastRob = robberData.lastRob || 0; - - if (now < lastRob + ROB_COOLDOWN) { - const remaining = lastRob + ROB_COOLDOWN - now; - const hours = Math.floor(remaining / (1000 * 60 * 60)); - const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60)); - - throw createError( - "Robbery cooldown active", - ErrorTypes.RATE_LIMIT, - `You need to lay low. Wait **${hours}h ${minutes}m** before attempting another robbery.`, - { remaining, hours, minutes, cooldownType: 'rob' } - ); - } - - if (victimData.wallet < 500) { - throw createError( - "Victim too poor", - ErrorTypes.VALIDATION, - `${victimUser.username} is too poor. They need at least $500 cash to be worth robbing.`, - { victimWallet: victimData.wallet, required: 500 } - ); - } - - const hasSafe = victimData.inventory["personal_safe"] || 0; - - if (hasSafe > 0) { - robberData.lastRob = now; - await setEconomyData(client, guildId, robberId, robberData); - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - MessageTemplates.ERRORS.CONFIGURATION_REQUIRED( - "robbery protection", - `${victimUser.username} was prepared! Your attempt failed because they own a **Personal Safe**. You got away clean but didn't gain anything.` - ) - ], - }); - } - - const isSuccessful = Math.random() < BASE_ROB_SUCCESS_CHANCE; - let resultEmbed; - - if (isSuccessful) { - const amountStolen = Math.floor(victimData.wallet * ROB_PERCENTAGE); - - robberData.wallet = (robberData.wallet || 0) + amountStolen; - victimData.wallet = (victimData.wallet || 0) - amountStolen; - - resultEmbed = MessageTemplates.SUCCESS.DATA_UPDATED( - "robbery", - `You successfully stole **$${amountStolen.toLocaleString()}** from ${victimUser.username}!` - ); - } else { - const fineAmount = Math.floor((robberData.wallet || 0) * FINE_PERCENTAGE); - - if ((robberData.wallet || 0) < fineAmount) { - robberData.wallet = 0; - } else { - robberData.wallet = (robberData.wallet || 0) - fineAmount; - } - - resultEmbed = MessageTemplates.ERRORS.INSUFFICIENT_PERMISSIONS( - "robbery failed", - `You failed the robbery and were caught! You were fined **$${fineAmount.toLocaleString()}** of your own cash.` - ); - } - - robberData.lastRob = now; - - await setEconomyData(client, guildId, robberId, robberData); - await setEconomyData(client, guildId, victimUser.id, victimData); - - resultEmbed - .addFields( - { - name: `Your New Cash (${interaction.user.username})`, - value: `$${robberData.wallet.toLocaleString()}`, - inline: true, - }, - { - name: `Victim's New Cash (${victimUser.username})`, - value: `$${victimData.wallet.toLocaleString()}`, - inline: true, - }, - ) - .setFooter({ text: `Next robbery available in 4 hours.` }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [resultEmbed] }); - }, { command: 'rob' }) -}; - - - diff --git a/src/commands/Economy/shop.js b/src/commands/Economy/shop.js deleted file mode 100644 index 6ebdfe458..000000000 --- a/src/commands/Economy/shop.js +++ /dev/null @@ -1,60 +0,0 @@ -import { SlashCommandBuilder, MessageFlags } from 'discord.js'; -import { errorEmbed } from '../../utils/embeds.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -import shopBrowse from './modules/shop_browse.js'; -import shopConfigSetrole from './modules/shop_config_setrole.js'; - -export default { - data: new SlashCommandBuilder() - .setName('shop') - .setDescription('Economy shop commands.') - .addSubcommand(subcommand => - subcommand - .setName('browse') - .setDescription('Browse the economy shop.'), - ) - .addSubcommandGroup(group => - group - .setName('config') - .setDescription('Configure shop settings. (Manage Server required)') - .addSubcommand(subcommand => - subcommand - .setName('setrole') - .setDescription('Set the Discord role granted when the Premium Role shop item is purchased.') - .addRoleOption(option => - option - .setName('role') - .setDescription('The role to grant for Premium Role purchases.') - .setRequired(true), - ), - ), - ), - - async execute(interaction, config, client) { - try { - const subcommandGroup = interaction.options.getSubcommandGroup(false); - const subcommand = interaction.options.getSubcommand(); - - if (subcommand === 'browse') { - return await shopBrowse.execute(interaction, config, client); - } - - if (subcommandGroup === 'config' && subcommand === 'setrole') { - return await shopConfigSetrole.execute(interaction, config, client); - } - - return InteractionHelper.safeReply(interaction, { - embeds: [errorEmbed('Error', 'Unknown subcommand.')], - flags: MessageFlags.Ephemeral, - }); - } catch (error) { - logger.error('shop command error:', error); - await InteractionHelper.safeReply(interaction, { - content: '❌ An error occurred while running the shop command.', - flags: MessageFlags.Ephemeral, - }).catch(() => {}); - } - }, -}; \ No newline at end of file diff --git a/src/commands/Economy/slut.js b/src/commands/Economy/slut.js deleted file mode 100644 index 3d4791cf9..000000000 --- a/src/commands/Economy/slut.js +++ /dev/null @@ -1,193 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const SLUT_COOLDOWN = 45 * 60 * 1000; - -const SLUT_ACTIVITIES = [ - { name: "Cam Stream", min: 120, max: 450, risk: 0.2 }, - { name: "Private Dance Session", min: 220, max: 700, risk: 0.25 }, - { name: "After-Hours Club Host", min: 320, max: 900, risk: 0.3 }, - { name: "VIP Companion Booking", min: 550, max: 1400, risk: 0.35 }, - { name: "Exclusive Livestream", min: 850, max: 2200, risk: 0.4 }, -]; - -const POSITIVE_OUTCOMES = [ - "Your stream blew up and tips poured in.", - "A VIP booking paid far above average.", - "Your after-hours shift was packed and profitable.", - "Premium requests came through and your payout jumped.", -]; - -const FINE_OUTCOMES = [ - "Venue security issued a compliance fine.", - "A moderation strike triggered a platform fee.", - "You were flagged and had to pay a penalty.", -]; - -const ROBBED_OUTCOMES = [ - "A fake buyer chargeback wiped part of your earnings.", - "A scam booking cleaned out a chunk of your cash.", - "You got baited by a fraud account and lost money.", -]; - -const LOSS_OUTCOMES = [ - "The set flopped and you had to cover operating costs.", - "You burned budget on prep and made no return.", - "The shift went sideways and left you in the red.", -]; - -function randomInt(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -function randomChoice(items) { - return items[Math.floor(Math.random() * items.length)]; -} - -function resolveOutcome(activity, wallet) { - const successChance = Math.max(0.35, 0.55 - activity.risk * 0.2); - const fineChance = 0.22; - const robbedChance = 0.2; - const roll = Math.random(); - - if (roll < successChance) { - const amount = randomInt(activity.min, activity.max); - return { - type: 'payout', - delta: amount, - message: randomChoice(POSITIVE_OUTCOMES), - title: `💰 ${activity.name} - Payout` - }; - } - - const remainingAfterSuccess = roll - successChance; - - if (remainingAfterSuccess < fineChance) { - const maxFine = Math.min(wallet, Math.max(150, Math.floor(activity.max * 0.4))); - const minFine = Math.min(maxFine, Math.max(50, Math.floor(activity.min * 0.2))); - const amount = maxFine > 0 ? randomInt(minFine, maxFine) : 0; - return { - type: 'fine', - delta: -amount, - message: randomChoice(FINE_OUTCOMES), - title: `🚨 ${activity.name} - Fined` - }; - } - - if (remainingAfterSuccess < fineChance + robbedChance) { - const maxRobbed = Math.min(wallet, Math.max(200, Math.floor(wallet * 0.35))); - const minRobbed = Math.min(maxRobbed, Math.max(75, Math.floor(wallet * 0.1))); - const amount = maxRobbed > 0 ? randomInt(minRobbed, maxRobbed) : 0; - return { - type: 'robbed', - delta: -amount, - message: randomChoice(ROBBED_OUTCOMES), - title: `🕵️ ${activity.name} - Robbed` - }; - } - - const maxLoss = Math.min(wallet, Math.max(100, Math.floor(activity.max * 0.3))); - const minLoss = Math.min(maxLoss, Math.max(40, Math.floor(activity.min * 0.15))); - const amount = maxLoss > 0 ? randomInt(minLoss, maxLoss) : 0; - return { - type: 'loss', - delta: -amount, - message: randomChoice(LOSS_OUTCOMES), - title: `❌ ${activity.name} - Loss` - }; -} - -export default { - data: new SlashCommandBuilder() - .setName('slut') - .setDescription('Take a risky provocative job for random payout or loss'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - logger.debug(`[ECONOMY] Slut command started for ${userId}`, { userId, guildId }); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data for slut command", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - const lastSlut = userData.lastSlut || 0; - - if (now - lastSlut < SLUT_COOLDOWN) { - const remainingTime = lastSlut + SLUT_COOLDOWN - now; - throw createError( - "Slut cooldown active", - ErrorTypes.RATE_LIMIT, - `You need to wait before you can work again! Try again in **${Math.ceil(remainingTime / 60000)}** minutes.`, - { timeRemaining: remainingTime, cooldownType: 'slut' } - ); - } - - const activity = randomChoice(SLUT_ACTIVITIES); - - const outcome = resolveOutcome(activity, userData.wallet || 0); - - userData.lastSlut = now; - userData.totalSluts = (userData.totalSluts || 0) + 1; - userData.totalSlutEarnings = (userData.totalSlutEarnings || 0) + Math.max(0, outcome.delta); - userData.totalSlutLosses = (userData.totalSlutLosses || 0) + Math.max(0, -outcome.delta); - - if (outcome.type !== 'payout') { - userData.failedSluts = (userData.failedSluts || 0) + 1; - } - - userData.wallet = Math.max(0, (userData.wallet || 0) + outcome.delta); - - await setEconomyData(client, guildId, userId, userData); - - logger.info(`[ECONOMY_TRANSACTION] Slut activity resolved`, { - userId, - guildId, - activity: activity.name, - outcomeType: outcome.type, - amountDelta: outcome.delta, - newWallet: userData.wallet, - timestamp: new Date().toISOString() - }); - - const amountLabel = `${outcome.delta >= 0 ? '+' : '-'}$${Math.abs(outcome.delta).toLocaleString()}`; - const summaryLines = [ - `${outcome.message}`, - `💸 **Net Result:** ${amountLabel}`, - `💳 **Current Balance:** $${userData.wallet.toLocaleString()}`, - `📊 **Total Sessions:** ${userData.totalSluts}`, - `💵 **Total Earned:** $${(userData.totalSlutEarnings || 0).toLocaleString()}`, - `🧾 **Total Lost:** $${(userData.totalSlutLosses || 0).toLocaleString()}` - ]; - - const embed = createEmbed({ - title: outcome.title, - description: summaryLines.join('\n'), - color: outcome.delta >= 0 ? 'success' : 'error', - timestamp: true - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'slut' }) -}; - - - - - diff --git a/src/commands/Economy/withdraw.js b/src/commands/Economy/withdraw.js deleted file mode 100644 index 2fc0b46a1..000000000 --- a/src/commands/Economy/withdraw.js +++ /dev/null @@ -1,86 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData, getMaxBankCapacity } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { MessageTemplates } from '../../utils/messageTemplates.js'; - -import { InteractionHelper } from '../../utils/interactionHelper.js'; -export default { - data: new SlashCommandBuilder() - .setName('withdraw') - .setDescription('Withdraw money from your bank to your wallet') - .addIntegerOption(option => - option - .setName('amount') - .setDescription('Amount to withdraw') - .setRequired(true) - .setMinValue(1) - ), - - execute: withErrorHandling(async (interaction, config, client) => { - await InteractionHelper.safeDefer(interaction); - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const amountInput = interaction.options.getInteger("amount"); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - let withdrawAmount = amountInput; - - if (withdrawAmount <= 0) { - throw createError( - "Invalid withdrawal amount", - ErrorTypes.VALIDATION, - "You must withdraw a positive amount.", - { amount: withdrawAmount, userId } - ); - } - - if (withdrawAmount > userData.bank) { - withdrawAmount = userData.bank; - } - - if (withdrawAmount === 0) { - throw createError( - "Empty bank account", - ErrorTypes.VALIDATION, - "Your bank account is empty.", - { userId, bankBalance: userData.bank } - ); - } - - userData.wallet += withdrawAmount; - userData.bank -= withdrawAmount; - - await setEconomyData(client, guildId, userId, userData); - - const embed = MessageTemplates.SUCCESS.DATA_UPDATED( - "withdrawal", - `You successfully withdrew **$${withdrawAmount.toLocaleString()}** from your bank.` - ) - .addFields( - { - name: "💵 New Cash Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }, - { - name: "🏦 New Bank Balance", - value: `$${userData.bank.toLocaleString()}`, - inline: true, - }, - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'withdraw' }) -}; \ No newline at end of file diff --git a/src/commands/Economy/work.js b/src/commands/Economy/work.js deleted file mode 100644 index ca39b2077..000000000 --- a/src/commands/Economy/work.js +++ /dev/null @@ -1,127 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const WORK_COOLDOWN = 30 * 60 * 1000; -const MIN_WORK_AMOUNT = 50; -const MAX_WORK_AMOUNT = 300; -const LAPTOP_MULTIPLIER = 1.5; -const WORK_JOBS = [ - "Software Developer", - "Barista", - "Janitor", - "YouTuber", - "Discord Bot Developer", - "Cashier", - "Pizza Delivery Driver", - "Librarian", - "Gardener", - "Data Analyst", -]; - -export default { - data: new SlashCommandBuilder() - .setName('work') - .setDescription('Work to earn some money'), - - execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); - if (!deferred) return; - - const userId = interaction.user.id; - const guildId = interaction.guildId; - const now = Date.now(); - - const userData = await getEconomyData(client, guildId, userId); - - if (!userData) { - throw createError( - "Failed to load economy data for work", - ErrorTypes.DATABASE, - "Failed to load your economy data. Please try again later.", - { userId, guildId } - ); - } - - logger.debug(`[ECONOMY] Work command started for ${userId}`, { userId, guildId }); - - const lastWork = userData.lastWork || 0; - const inventory = userData.inventory || {}; - const extraWorkShifts = inventory["extra_work"] || 0; - const hasLaptop = inventory["laptop"] || 0; - - let cooldownActive = now < lastWork + WORK_COOLDOWN; - let usedConsumable = false; - - if (cooldownActive) { - if (extraWorkShifts > 0) { - inventory["extra_work"] = (inventory["extra_work"] || 0) - 1; - usedConsumable = true; - } else { - const remaining = lastWork + WORK_COOLDOWN - now; - throw createError( - "Work cooldown active", - ErrorTypes.RATE_LIMIT, - `You're working too fast! Wait **${Math.floor(remaining / 3600000)}h ${Math.floor((remaining % 3600000) / 60000)}m** before working again.`, - { timeRemaining: remaining, cooldownType: 'work' } - ); - } - } - - let earned = Math.floor(Math.random() * (MAX_WORK_AMOUNT - MIN_WORK_AMOUNT + 1)) + MIN_WORK_AMOUNT; - const job = WORK_JOBS[Math.floor(Math.random() * WORK_JOBS.length)]; - - - let multiplierMessage = ""; - if (hasLaptop > 0) { - earned = Math.floor(earned * LAPTOP_MULTIPLIER); - multiplierMessage = "\n💻 **Laptop Bonus:** +50% earnings!"; - } - - userData.wallet = (userData.wallet || 0) + earned; - userData.lastWork = now; - - await setEconomyData(client, guildId, userId, userData); - - logger.info(`[ECONOMY_TRANSACTION] Work completed`, { - userId, - guildId, - amount: earned, - job, - usedConsumable, - hasLaptop: hasLaptop > 0, - newWallet: userData.wallet, - timestamp: new Date().toISOString() - }); - - const embed = successEmbed( - "💼 Work Complete!", - `You worked as a **${job}** and earned **$${earned.toLocaleString()}**!${multiplierMessage}` - ) - .addFields( - { - name: "💰 New Balance", - value: `$${userData.wallet.toLocaleString()}`, - inline: true, - }, - { - name: "⏰ Next Work", - value: ``, - inline: true, - } - ) - .setFooter({ - text: `Requested by ${interaction.user.tag}`, - iconURL: interaction.user.displayAvatarURL(), - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - }, { command: 'work' }) -}; - - - - From 1107afb2424988d0d01adeaf78fb667bda990f23 Mon Sep 17 00:00:00 2001 From: AdenTan123 Date: Mon, 8 Jun 2026 16:32:22 +0800 Subject: [PATCH 4/6] Update README.md --- README.md | 252 +----------------------------------------------------- 1 file changed, 4 insertions(+), 248 deletions(-) diff --git a/README.md b/README.md index 98755bccb..5bfe7b8d3 100644 --- a/README.md +++ b/README.md @@ -1,257 +1,13 @@ -# TitanBot - Ultimate Discord Bot +# jackbot - Ultimate Discord Bot -**TitanBot** is a powerful, feature-rich Discord bot designed to enhance your server experience with comprehensive moderation tools, engaging economy systems, utility features, and much more. Built with modern Discord.js v14 and PostgreSQL for optimal performance and data persistence. - -[![Support Server](https://img.shields.io/badge/-Support%20Server-%235865F2?logo=discord&logoColor=white&style=flat-square&logoWidth=20)](https://discord.gg/8kJBYhTGW9) -[![Discord.js](https://img.shields.io/npm/v/discord.js?style=flat-square&labelColor=%23202225&color=%23202225&logo=npm&logoColor=white&logoWidth=20)](https://www.npmjs.com/package/discord.js) -![PostgreSQL](https://img.shields.io/badge/-PostgreSQL-%23336791?logo=postgresql&logoColor=white&style=flat-square&logoWidth=20) - -## Table of Contents - -- [Features Overview](#features-overview) -- [Quick Setup](#quick-setup) -- [Manual Installation Steps](#manual-installation-steps) -- [Support Server](https://discord.gg/QnWNz2dKCE) -- [Required Bot Intents](#bot-intents) -- [Contributing](#contributing) - - -## Features Overview - -TitanBot offers a complete suite of tools for Discord server management and community engagement: - - - - - - -
- -### Moderation & Administration -- **Mass Actions** - Bulk ban/kick capabilities -- **User Notes** - Keep detailed moderation records -- **Case Management** - View and track all mod actions - -### Economy System -- **Shop & Inventory** - Buy and manage items -- **Gambling** - Risk it for rewards -- **Pay System** - Transfer money between users - -### Fun & Entertainment -- **Random Facts** - Learn something new -- **Wanted Poster** - Create fun wanted images -- **Text Reversal** - Reverse any text - -### Advanced Ticket System -- **Claim & Priority** - Staff ticket management -- **Ticket Limits** - Prevent spam -- **Transcript System** - Save ticket history - -### Server Stats -- **Member Counter** - Live member count channels -- **Voice Counters** - Track voice stats -- **Dynamic Updates** - Real-time channel updates - -### Reaction Roles -- **Role Assignment** - Self-assignable roles -- **Emoji Selection** - Reaction-based system -- **Multi-role Support** - Multiple role options - - - -### Leveling & XP System -- **XP Tracking** - Automatic message-based XP -- **Level Roles** - Auto-assign roles by level -- **Custom Configuration** - Personalize leveling - -### Giveaways & Events -- **Multiple Winners** - Support multi-winner giveaways -- **Auto Picking** - Automatic winner selection -- **Reroll System** - Pick new winners if needed - -### Birthday System -- **Birthday Tracking** - Never miss a birthday -- **Auto Announcements** - Celebrate automatically -- **Timezone Support** - Accurate worldwide tracking - -### Utility Tools -- **Report System** - Report issues to staff -- **Todo Lists** - Personal task management -- **First Message** - Jump to channel's first message - -### Welcome System -- **Welcome Messages** - Greet new members -- **Auto Roles** - Assign roles on join -- **Custom Embeds** - Personalized messages - -
- - -## Quick Setup (Recommended for non-coders) - -### Video Tutorial -For a detailed step-by-step setup guide, watch our comprehensive video tutorial: -[**TitanBot Setup Tutorial**](https://www.youtube.com/@TouchDisc) - -## Docker Deployment (Recommended) - -TitanBot is fully containerized for easy deployment. - -1. **Clone the repository:** - ```bash - git clone https://github.com/codebymitch/TitanBot.git - cd TitanBot - ``` - -2. **Configure environment variables:** - Create a `.env` file from `.env.example` and fill in your bot details and PostgreSQL credentials. - -3. **Start the containers:** - ```bash - docker-compose up -d - ``` - -This will start both the bot and a persistent PostgreSQL database. - -### Using GitHub Container Registry - -The bot is automatically published to GitHub Container Registry on every push to main. - -```bash -docker pull ghcr.io/codebymitch/titanbot:main -``` - - -## Manual Installation Steps - -### Prerequisites -- Node.js 18.0.0 or higher -- PostgreSQL server (recommended) or memory storage fallback -- Discord bot application with proper intents - -1. **Clone the Repository** - ```bash - git clone https://github.com/codebymitch/TitanBot.git - cd TitanBot - ``` - -2. **Install Dependencies** - ```bash - npm install - ``` - -3. **Configure Environment Variables** - ```bash - cp .env.example .env - ``` - Edit `.env` with your configuration (only the following variables require configuration, leave remaining variables as default): - ```env - # Discord Bot Configuration - DISCORD_TOKEN=your_discord_bot_token_here - CLIENT_ID=your_discord_client_id_here - GUILD_ID=your_discord_guild_id_here - - # PostgreSQL Configuration (Primary Database) - POSTGRES_URL=postgresql://postgres:yourpassword@localhost:5432/titanbot - POSTGRES_HOST=localhost - POSTGRES_PORT=5432 - POSTGRES_DB=titanbot - POSTGRES_USER=postgres - POSTGRES_PASSWORD=yourpassword - ``` - - Production note: - - `NODE_ENV=production` - - `LOG_LEVEL=warn` for a clean production console (critical issues + startup status) - - `LOG_LEVEL=info` if you want more detailed operational logs - - If your chosen `PORT` is already used, TitanBot automatically tries the next port(s) - - Environment options reference: - - `NODE_ENV`: `development`, `production`, `test` (any non-`production` value is treated as non-production) - - `LOG_LEVEL`: `error`, `warn`, `info`, `http`, `verbose`, `debug`, `silly` - - Accepted aliases for `LOG_LEVEL` in this bot: `warns`, `warning`, `warnings` → `warn` - - Recommended production `.env` (easy mode + default mode): - ```env - NODE_ENV=production - LOG_LEVEL=warn - WEB_HOST=0.0.0.0 - PORT=3000 - PORT_RETRY_ATTEMPTS=5 - ``` - This gives clear startup/online status messages while keeping logs simple for non-technical operators. - If port `3000` is busy, the bot tries the next available ports automatically (up to `PORT_RETRY_ATTEMPTS`). - -4. **Setup PostgreSQL Database** (Optional but recommended) - ```bash - # Create database and user - createdb titanbot - createuser titanbot - psql -c "ALTER USER titanbot PASSWORD 'yourpassword';" - psql -c "GRANT ALL PRIVILEGES ON DATABASE titanbot TO titanbot;" - ``` - -5. **Test Database Connection** - ```bash - npm run test-postgres - ``` - -6. **Start the Bot** - ```bash - npm start - ``` - - -## Required Bot Intents -TitanBot requires the following Discord intents: -- **Guilds** -- **Guild Messages** -- **Message Content** -- **Guild Members** -- **Guild Message Reactions** -- **Guild Voice States** -- **Direct Messages** -- **Bot** -- **Applications.commands** - -### Required Permissions -- **View Channels** -- **Send Messages** -- **Embed Links** -- **Attach Files** -- **Read Message History** -- **Manage Messages** -- **Manage Channels** -- **Manage Roles** -- **Kick Members** -- **Manage Messages** -- **Ban Members** -- **Moderate Members** -- **Connect** - - -## Contributing - -We welcome contributions to TitanBot! Here's how you can help: - -1. **Fork the repository** -2. **Create a feature branch** -3. **Make your changes** -4. **Test thoroughly** -5. **Submit a pull request** - -### Development Guidelines -- Follow existing code style -- Add proper error handling -- Include documentation for new features -- Test with PostgreSQL and memory storage +Assisted with the Help of TitanBot Materials. ## License -TitanBot is released under the MIT License. See [LICENSE](LICENSE) for details. +JackBot is released under the MIT License. See [LICENSE](LICENSE) for details. ## Thank You -Thank you for choosing TitanBot for your Discord server! We're constantly working to improve and add new features based on community feedback. +Thank you for choosing JackBot for your Discord server! We're constantly working to improve and add new features based on community feedback. *Last updated: May 2026* From f16e2a2e51d725beb5d531dfe438cebd6fd3650c Mon Sep 17 00:00:00 2001 From: AdenTan123 Date: Mon, 8 Jun 2026 16:32:51 +0800 Subject: [PATCH 5/6] updated license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 8df7f6c94..07cccfcae 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 codebymitch +Copyright (c) 2026 hjackstudios Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From f6478ac9dfa6905665623ee58a410ef2ad260115 Mon Sep 17 00:00:00 2001 From: Ng Debbie Date: Mon, 8 Jun 2026 16:46:15 +0800 Subject: [PATCH 6/6] Add /testmodal command and modal handler --- src/commands/Core/testmodal.js | 68 +++++++++++++++++++++++++++ src/interactions/modals/test_debug.js | 30 ++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/commands/Core/testmodal.js create mode 100644 src/interactions/modals/test_debug.js diff --git a/src/commands/Core/testmodal.js b/src/commands/Core/testmodal.js new file mode 100644 index 000000000..f79b5d1f9 --- /dev/null +++ b/src/commands/Core/testmodal.js @@ -0,0 +1,68 @@ +import { SlashCommandBuilder, ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; +import { InteractionHelper } from '../../utils/interactionHelper.js'; +import { logger } from '../../utils/logger.js'; +import fs from 'fs/promises'; +import path from 'path'; + +async function getRecentLogs(maxChars = 3500) { + try { + const logsDir = path.resolve(new URL('../../logs', import.meta.url).pathname); + const files = await fs.readdir(logsDir).catch(() => []); + const combinedFiles = files.filter(f => f.startsWith('combined-') && f.endsWith('.log')); + if (combinedFiles.length === 0) return ''; + + // pick newest file by mtime + let newest = null; + for (const f of combinedFiles) { + const full = path.join(logsDir, f); + const stat = await fs.stat(full).catch(() => null); + if (!stat) continue; + if (!newest || stat.mtimeMs > newest.mtimeMs) newest = { file: full, mtimeMs: stat.mtimeMs }; + } + if (!newest) return ''; + + const content = await fs.readFile(newest.file, 'utf8').catch(() => ''); + if (!content) return ''; + // return last maxChars characters + return content.slice(-maxChars); + } catch (e) { + logger.warn('Failed to read recent logs:', e.message || e); + return ''; + } +} + +export default { + data: new SlashCommandBuilder() + .setName('testmodal') + .setDescription('Open the Debug Test modal (developer logs)'), + + async execute(interaction) { + const ready = await InteractionHelper.ensureReady(interaction, { flags: 0 }); + if (!ready) return; + + try { + const modal = new ModalBuilder() + .setCustomId('test_debug_modal') + .setTitle('Debug Test'); + + const logs = await getRecentLogs(3500); + + const input = new TextInputBuilder() + .setCustomId('dev_logs') + .setLabel('Dev Logs (read-only)') + .setStyle(TextInputStyle.Paragraph) + .setRequired(false) + .setMaxLength(4000) + .setPlaceholder('No logs available') + .setValue(logs || 'No recent logs available'); + + const row = new ActionRowBuilder().addComponents(input); + modal.addComponents(row); + + await interaction.showModal(modal); + } catch (error) { + logger.error('Failed to show TestModal:', error); + await InteractionHelper.safeReply(interaction, { content: 'Could not open debug modal.', flags: 1 << 6 }); + } + }, +}; diff --git a/src/interactions/modals/test_debug.js b/src/interactions/modals/test_debug.js new file mode 100644 index 000000000..575d63f4c --- /dev/null +++ b/src/interactions/modals/test_debug.js @@ -0,0 +1,30 @@ +import { InteractionHelper } from '../../utils/interactionHelper.js'; +import { createEmbed } from '../../utils/embeds.js'; +import { logger } from '../../utils/logger.js'; + +export default { + name: 'test_debug_modal', + async execute(interaction) { + try { + // Ensure interaction is usable + const ready = await InteractionHelper.ensureReady(interaction, { flags: 1 << 6 }); + if (!ready) return; + + const submittedLogs = interaction.fields.getTextInputValue('dev_logs') || 'No logs submitted'; + + const safeContent = submittedLogs.length > 1900 ? submittedLogs.slice(0, 1900) + '\n\n[truncated]' : submittedLogs; + + const embed = createEmbed({ title: 'Debug Test - Logs', description: 'Recent dev logs (truncated):' }) + .setDescription(`\n\n\`\`\`\n${safeContent}\n\`\`\``); + + await InteractionHelper.safeReply(interaction, { embeds: [embed], flags: 1 << 6 }); + } catch (error) { + logger.error('Error handling test_debug_modal submission:', error); + try { + await InteractionHelper.safeReply(interaction, { content: 'Failed to retrieve submitted logs.', flags: 1 << 6 }); + } catch (e) { + logger.error('Failed to send error reply for test_debug_modal:', e); + } + } + } +};