From 860dba57a978cd3f168d48ac4f8688a8450eb4ee Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Tue, 10 Nov 2026 13:20:30 +0000 Subject: [PATCH 01/14] feat: Ubah Slash ke Prefix Co-authored-by: traeagent --- src/commands/Community/apply.js | 1 - .../Community/modules/app_dashboard.js | 1 - src/commands/Core/configWizard.js | 1 - src/commands/Core/help.js | 1 - src/commands/Economy/economy.js | 1 - .../Economy/modules/economy_dashboard.js | 1 - src/commands/Economy/shop-config.js | 1 - src/commands/Economy/shop.js | 1 - .../Leveling/modules/level_dashboard.js | 1 - .../Logging/modules/logging_channel.js | 1 - .../Logging/modules/logging_dashboard.js | 1 - src/commands/Music/play.js | 1 - src/commands/Music/queue.js | 1 - .../Ticket/modules/ticket_dashboard.js | 1 - src/commands/Tools/embedbuilder.js | 1 - src/commands/Utility/wipedata.js | 1 - .../modules/autoVerifyDashboard.js | 1 - .../modules/verification_dashboard.js | 1 - src/commands/Welcome/greet.js | 1 - .../Welcome/modules/greet_dashboard.js | 1 - src/config/commands/prefixRestrictions.js | 131 ------------------ src/events/messageCreate.js | 14 +- src/utils/messageAdapter.js | 16 +-- 23 files changed, 3 insertions(+), 178 deletions(-) delete mode 100644 src/config/commands/prefixRestrictions.js diff --git a/src/commands/Community/apply.js b/src/commands/Community/apply.js index 0e2c2c9249..075a93a310 100644 --- a/src/commands/Community/apply.js +++ b/src/commands/Community/apply.js @@ -35,7 +35,6 @@ function getApplicationStatusPresentation(statusValue) { } export default { - slashOnly: true, data: new SlashCommandBuilder() .setName("apply") .setDescription("Manage role applications") diff --git a/src/commands/Community/modules/app_dashboard.js b/src/commands/Community/modules/app_dashboard.js index 856a28b918..a66fef4717 100644 --- a/src/commands/Community/modules/app_dashboard.js +++ b/src/commands/Community/modules/app_dashboard.js @@ -137,7 +137,6 @@ async function refreshDashboard(rootInteraction, settings, roles, guildId, clien } export default { - prefixOnly: false, async execute(interaction, config, client, selectedAppName = null) { try { const guildId = interaction.guild.id; diff --git a/src/commands/Core/configWizard.js b/src/commands/Core/configWizard.js index 22ac013ec0..02bf6385ce 100644 --- a/src/commands/Core/configWizard.js +++ b/src/commands/Core/configWizard.js @@ -603,7 +603,6 @@ async function handleSettingModalSubmit(selectInteraction, rootInteraction, sett } export default { - slashOnly: true, data: new SlashCommandBuilder() .setName('configwizard') .setDescription('Open the server configuration dashboard and setup wizard') diff --git a/src/commands/Core/help.js b/src/commands/Core/help.js index 3d9d7eb783..15fe6219bf 100644 --- a/src/commands/Core/help.js +++ b/src/commands/Core/help.js @@ -140,7 +140,6 @@ export async function createInitialHelpMenu(client) { } export default { - slashOnly: true, data: new SlashCommandBuilder() .setName("help") .setDescription("Displays the help menu with all available commands"), diff --git a/src/commands/Economy/economy.js b/src/commands/Economy/economy.js index d702eb6157..ca73dee69a 100644 --- a/src/commands/Economy/economy.js +++ b/src/commands/Economy/economy.js @@ -4,7 +4,6 @@ import { InteractionHelper } from '../../utils/interactionHelper.js'; import economyDashboard from './modules/economy_dashboard.js'; export default { - slashOnly: true, data: new SlashCommandBuilder() .setName('economy') .setDescription('Economy management commands') diff --git a/src/commands/Economy/modules/economy_dashboard.js b/src/commands/Economy/modules/economy_dashboard.js index 04f507ff97..b5633309d6 100644 --- a/src/commands/Economy/modules/economy_dashboard.js +++ b/src/commands/Economy/modules/economy_dashboard.js @@ -140,7 +140,6 @@ async function updateConfigFile(currencySymbol, currencyName) { } export default { - prefixOnly: false, async execute(interaction, config, client) { try { const guild = interaction.guild; diff --git a/src/commands/Economy/shop-config.js b/src/commands/Economy/shop-config.js index fb3be41dc3..66a162d45c 100644 --- a/src/commands/Economy/shop-config.js +++ b/src/commands/Economy/shop-config.js @@ -2,7 +2,6 @@ import { SlashCommandBuilder } from 'discord.js'; import shopConfigSetrole from './modules/shop_config_setrole.js'; export default { - slashOnly: true, data: new SlashCommandBuilder() .setName('shop-config') .setDescription('Configure shop settings. (Manage Server required)') diff --git a/src/commands/Economy/shop.js b/src/commands/Economy/shop.js index 16b2b62d96..365c8491de 100644 --- a/src/commands/Economy/shop.js +++ b/src/commands/Economy/shop.js @@ -2,7 +2,6 @@ import { SlashCommandBuilder } from 'discord.js'; import shopBrowse from './modules/shop_browse.js'; export default { - slashOnly: true, data: new SlashCommandBuilder() .setName('shop') .setDescription('Browse the economy shop.'), diff --git a/src/commands/Leveling/modules/level_dashboard.js b/src/commands/Leveling/modules/level_dashboard.js index 3d8ee34b61..1dfbe786b7 100644 --- a/src/commands/Leveling/modules/level_dashboard.js +++ b/src/commands/Leveling/modules/level_dashboard.js @@ -142,7 +142,6 @@ async function refreshDashboard(rootInteraction, cfg, guildId) { } export default { - prefixOnly: false, async execute(interaction, config, client) { try { const guildId = interaction.guild.id; diff --git a/src/commands/Logging/modules/logging_channel.js b/src/commands/Logging/modules/logging_channel.js index f92e6a6d3b..f324a11fc8 100644 --- a/src/commands/Logging/modules/logging_channel.js +++ b/src/commands/Logging/modules/logging_channel.js @@ -12,7 +12,6 @@ const DESTINATION_LABELS = { }; export default { - prefixOnly: false, async execute(interaction, config, client) { try { if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { diff --git a/src/commands/Logging/modules/logging_dashboard.js b/src/commands/Logging/modules/logging_dashboard.js index f317698ccf..008c62d419 100644 --- a/src/commands/Logging/modules/logging_dashboard.js +++ b/src/commands/Logging/modules/logging_dashboard.js @@ -176,7 +176,6 @@ export async function refreshDashboardMessage(interaction, client) { } export default { - prefixOnly: false, async execute(interaction, config, client) { try { if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { diff --git a/src/commands/Music/play.js b/src/commands/Music/play.js index 1d07665382..3ac621d2cf 100644 --- a/src/commands/Music/play.js +++ b/src/commands/Music/play.js @@ -3,7 +3,6 @@ import { InteractionHelper } from '../../utils/interactionHelper.js'; import { playQuery, replyMusicSuccess } from '../../services/music/musicActions.js'; export default { - slashOnly: true, category: 'Music', data: new SlashCommandBuilder() .setName('play') diff --git a/src/commands/Music/queue.js b/src/commands/Music/queue.js index 66faa4eb9d..ac2d5b9c92 100644 --- a/src/commands/Music/queue.js +++ b/src/commands/Music/queue.js @@ -3,7 +3,6 @@ import { InteractionHelper } from '../../utils/interactionHelper.js'; import { buildQueueReply } from '../../services/music/musicActions.js'; export default { - slashOnly: true, category: 'Music', data: new SlashCommandBuilder() .setName('queue') diff --git a/src/commands/Ticket/modules/ticket_dashboard.js b/src/commands/Ticket/modules/ticket_dashboard.js index 17f3f43f88..beb54f9534 100644 --- a/src/commands/Ticket/modules/ticket_dashboard.js +++ b/src/commands/Ticket/modules/ticket_dashboard.js @@ -256,7 +256,6 @@ async function updateLivePanel(client, guild, config, guildId) { } export default { - prefixOnly: false, async execute(interaction, config, client) { try { const guildId = interaction.guild.id; diff --git a/src/commands/Tools/embedbuilder.js b/src/commands/Tools/embedbuilder.js index 3da94f3dc1..29ecd5097f 100644 --- a/src/commands/Tools/embedbuilder.js +++ b/src/commands/Tools/embedbuilder.js @@ -1039,7 +1039,6 @@ async function handleJsonExport(selectInteraction, rootInteraction, state) { } export default { - slashOnly: true, data: new SlashCommandBuilder() .setName('embedbuilder') .setDescription('Build and post a fully custom embed with live preview') diff --git a/src/commands/Utility/wipedata.js b/src/commands/Utility/wipedata.js index 941265dca8..8a17cbeb26 100644 --- a/src/commands/Utility/wipedata.js +++ b/src/commands/Utility/wipedata.js @@ -5,7 +5,6 @@ import { logger } from '../../utils/logger.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { - slashOnly: true, data: new SlashCommandBuilder() .setName('wipedata') .setDescription('Delete all your personal data from the bot (irreversible)'), diff --git a/src/commands/Verification/modules/autoVerifyDashboard.js b/src/commands/Verification/modules/autoVerifyDashboard.js index 7209da79fd..75b49296f5 100644 --- a/src/commands/Verification/modules/autoVerifyDashboard.js +++ b/src/commands/Verification/modules/autoVerifyDashboard.js @@ -137,7 +137,6 @@ async function refreshDashboard(rootInteraction, cfg, guildId, client) { } export default { - prefixOnly: false, async execute(interaction, config, client) { try { const guildId = interaction.guild.id; diff --git a/src/commands/Verification/modules/verification_dashboard.js b/src/commands/Verification/modules/verification_dashboard.js index ab32689527..9ca37c28e7 100644 --- a/src/commands/Verification/modules/verification_dashboard.js +++ b/src/commands/Verification/modules/verification_dashboard.js @@ -226,7 +226,6 @@ async function refreshDashboard(rootInteraction, cfg, guildId, client) { } export default { - prefixOnly: false, async execute(interaction, config, client) { try { const guildId = interaction.guild.id; diff --git a/src/commands/Welcome/greet.js b/src/commands/Welcome/greet.js index aa823e14bb..14644ef626 100644 --- a/src/commands/Welcome/greet.js +++ b/src/commands/Welcome/greet.js @@ -5,7 +5,6 @@ import { handleInteractionError, TitanBotError, ErrorTypes, replyUserError } fro import greetDashboard from './modules/greet_dashboard.js'; export default { - slashOnly: true, data: new SlashCommandBuilder() .setName('greet') .setDescription('Manage welcome & goodbye settings') diff --git a/src/commands/Welcome/modules/greet_dashboard.js b/src/commands/Welcome/modules/greet_dashboard.js index 97581e7cd5..06e4f5f9f8 100644 --- a/src/commands/Welcome/modules/greet_dashboard.js +++ b/src/commands/Welcome/modules/greet_dashboard.js @@ -170,7 +170,6 @@ async function refreshDashboard(rootInteraction, cfg, guildId) { } export default { - prefixOnly: false, async execute(interaction, config, client) { try { const guildId = interaction.guild.id; diff --git a/src/config/commands/prefixRestrictions.js b/src/config/commands/prefixRestrictions.js deleted file mode 100644 index 2b57f7bd60..0000000000 --- a/src/config/commands/prefixRestrictions.js +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Prefix command restrictions — dashboard and advanced setup flows stay slash-only. - */ - -/** Top-level commands that cannot be invoked via prefix at all. */ -export const SLASH_ONLY_COMMANDS = new Set([ - 'configwizard', - 'help', - 'embedbuilder', - 'wipedata', - 'apply', -]); - -/** Subcommands blocked for every command when invoked via prefix. */ -export const GLOBAL_BLOCKED_SUBCOMMANDS = new Set([ - 'dashboard', - 'setup', -]); - -/** Subcommand groups blocked for every command when invoked via prefix. */ -export const GLOBAL_BLOCKED_SUBCOMMAND_GROUPS = new Set([ - 'config', -]); - -/** Per-command subcommands that stay slash-only (beyond the global block list). */ -export const COMMAND_BLOCKED_SUBCOMMANDS = { - music: new Set([ - 'shuffle', - 'loop', - 'seek', - 'remove', - 'move', - 'clear', - '247', - ]), - birthday: new Set(['setchannel']), - report: new Set(['setchannel']), -}; - -function collectSubcommandNames(commandJson) { - const subcommandGroup = commandJson.options?.find((opt) => opt.type === 2); - - if (subcommandGroup) { - const names = []; - for (const group of subcommandGroup.options || []) { - names.push(...(group.options?.map((opt) => opt.name) || [])); - } - return names; - } - - return (commandJson.options?.filter((opt) => opt.type === 1) || []).map((sub) => sub.name); -} - -function isSubcommandBlocked(commandName, subcommandName) { - if (!subcommandName) { - return false; - } - - if (GLOBAL_BLOCKED_SUBCOMMANDS.has(subcommandName)) { - return true; - } - - const commandBlocked = COMMAND_BLOCKED_SUBCOMMANDS[commandName]; - return commandBlocked?.has(subcommandName) ?? false; -} - -/** - * Returns whether a prefix invocation should be rejected. - * @param {object} command - Loaded command module - * @param {string[]} args - Parsed prefix arguments (after command name) - * @param {(name: string) => string} resolveSubcommandAlias - * @returns {{ blocked: boolean, reason?: string }} - */ -export function getPrefixRestriction(command, args, resolveSubcommandAlias) { - if (!command?.data?.toJSON) { - return { blocked: false }; - } - - const commandJson = command.data.toJSON(); - const commandName = commandJson.name?.toLowerCase(); - - if (command.prefixOnly === false || command.slashOnly === true) { - return { blocked: true, reason: 'This command is only available as a slash command.' }; - } - - if (SLASH_ONLY_COMMANDS.has(commandName)) { - return { blocked: true, reason: 'This command is only available as a slash command.' }; - } - - const [firstArg, secondArg] = args.map((arg) => arg?.toLowerCase?.() || null); - const resolvedFirstArg = firstArg ? resolveSubcommandAlias(firstArg) : null; - const resolvedSecondArg = secondArg ? resolveSubcommandAlias(secondArg) : null; - - const subcommandGroup = commandJson.options?.find((opt) => opt.type === 2); - - const allSubcommandNames = collectSubcommandNames(commandJson); - const allSubcommandsBlocked = - allSubcommandNames.length > 0 && - allSubcommandNames.every((name) => isSubcommandBlocked(commandName, name)); - - if (allSubcommandsBlocked) { - return { blocked: true, reason: 'This command is only available as a slash command.' }; - } - - if (firstArg && GLOBAL_BLOCKED_SUBCOMMAND_GROUPS.has(firstArg)) { - return { - blocked: true, - reason: 'This configuration flow is only available as a slash command.', - }; - } - - if (resolvedFirstArg && isSubcommandBlocked(commandName, resolvedFirstArg)) { - return { - blocked: true, - reason: 'This subcommand is only available as a slash command.', - }; - } - - if (subcommandGroup && resolvedSecondArg && isSubcommandBlocked(commandName, resolvedSecondArg)) { - return { - blocked: true, - reason: 'This subcommand is only available as a slash command.', - }; - } - - return { blocked: false }; -} - -export function isPrefixRestrictedCommand(command, args, resolveSubcommandAlias) { - return getPrefixRestriction(command, args, resolveSubcommandAlias).blocked; -} diff --git a/src/events/messageCreate.js b/src/events/messageCreate.js index fb1a165f31..970566c783 100644 --- a/src/events/messageCreate.js +++ b/src/events/messageCreate.js @@ -5,8 +5,7 @@ import { addXp } from '../services/leveling/xpSystem.js'; import { checkRateLimit } from '../utils/rateLimiter.js'; import { parsePrefixCommand } from '../utils/prefixParser.js'; import { supportsPrefixExecution, executePrefixCommand, resolvePrefixAccessKey } from '../utils/messageAdapter.js'; -import { resolveCommandAlias, resolveSubcommandAlias } from '../config/commands/commandAliases.js'; -import { getPrefixRestriction } from '../config/commands/prefixRestrictions.js'; +import { resolveCommandAlias } from '../config/commands/commandAliases.js'; import { getGuildConfig } from '../services/config/guildConfig.js'; import { getCommandPrefix, getBotMessage, isBotOwner, isCommandCategoryEnabled, isMaintenanceMode } from '../config/bot.js'; import { enforceAbuseProtection, formatCooldownDuration } from '../utils/abuseProtection.js'; @@ -95,16 +94,7 @@ async function handlePrefixCommand(message, client) { return; } - const restriction = getPrefixRestriction(command, args, resolveSubcommandAlias); - if (!supportsPrefixExecution(command) || restriction.blocked) { - if (restriction.blocked && restriction.reason) { - const embed = createEmbed({ - title: 'Slash Command Only', - description: `${restriction.reason}\nUse \`/${resolvedCommandName}\` instead.`, - color: 'info', - }); - await message.channel.send({ embeds: [embed] }).catch(() => {}); - } + if (!supportsPrefixExecution(command)) { return; } diff --git a/src/utils/messageAdapter.js b/src/utils/messageAdapter.js index 2d69d689b4..96916fc711 100644 --- a/src/utils/messageAdapter.js +++ b/src/utils/messageAdapter.js @@ -5,7 +5,6 @@ import { createEmbed } from './embeds.js'; import { handleInteractionError } from './errorHandler.js'; import { logger } from './logger.js'; import { InteractionHelper } from './interactionHelper.js'; -import { SLASH_ONLY_COMMANDS } from '../config/commands/prefixRestrictions.js'; import { getCommandPrefix } from '../config/bot.js'; import { ResponseCoordinator, buildPrefixUsage } from './responseCoordinator.js'; import { enforceDefaultCommandPermissions } from './permissionGuard.js'; @@ -172,20 +171,7 @@ export function createMockInteraction(message, commandData, args) { } export function supportsPrefixExecution(command) { - if (command.prefixOnly === false || command.slashOnly === true) { - return false; - } - - const commandName = command.data?.name?.toLowerCase(); - if (commandName && SLASH_ONLY_COMMANDS.has(commandName)) { - return false; - } - - if (command.prefixExecute) { - return true; - } - - return !!command.execute; + return typeof command?.execute === 'function' || typeof command?.prefixExecute === 'function'; } export async function executePrefixCommand(command, message, args, client, prefixOverride = null, guildConfig = null) { From 16dd7c62b58bd20dc7fb94f3f2a6aac2123ebf84 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Tue, 10 Nov 2026 13:25:26 +0000 Subject: [PATCH 02/14] feat: Ubah Slash ke Prefix Co-authored-by: traeagent --- src/app.js | 14 +- src/config/bot.js | 2 +- src/events/interactionCreate.js | 283 +------------------------- src/handlers/loaders/commandLoader.js | 180 ++-------------- src/utils/commandInputValidation.js | 52 ----- src/utils/messageAdapter.js | 15 -- 6 files changed, 24 insertions(+), 522 deletions(-) delete mode 100644 src/utils/commandInputValidation.js diff --git a/src/app.js b/src/app.js index 693cd228ea..227c38819c 100644 --- a/src/app.js +++ b/src/app.js @@ -11,7 +11,7 @@ import { getServerCounters, saveServerCounters, updateCounter } from './services import { logger, startupLog, shutdownLog } from './utils/logger.js'; import { checkBirthdays } from './services/birthdayService.js'; import { checkGiveaways } from './services/giveawayService.js'; -import { loadCommands, registerCommands as registerSlashCommands } from './handlers/loaders/commandLoader.js'; +import { loadCommands, clearSlashCommands as clearGlobalSlashCommands } from './handlers/loaders/commandLoader.js'; import { runSafeTask, handleTaskError, ErrorCodes } from './utils/errorHandler.js'; import { initializeMusic } from './services/music/riffySetup.js'; import { shutdownMusic } from './services/music/playerHandler.js'; @@ -90,9 +90,9 @@ class TitanBot extends Client { await this.login(this.config.bot.token); startupLog('Discord login successful'); - startupLog('Registering slash commands globally...'); - await this.registerCommands(); - startupLog('Slash commands registration complete'); + startupLog('Prefix-only mode: clearing registered slash commands...'); + await this.clearSlashCommands(); + startupLog('Slash commands cleared'); const databaseMode = dbStatus.isDegraded ? 'Optional in-memory mode (data resets after restart)' @@ -322,11 +322,11 @@ class TitanBot extends Client { } } - async registerCommands() { + async clearSlashCommands() { try { - await registerSlashCommands(this, { clientId: this.config.bot.clientId }); + await clearGlobalSlashCommands(this, { clientId: this.config.bot.clientId }); } catch (error) { - logger.error('Error registering commands:', error); + logger.error('Error clearing slash commands:', error); } } diff --git a/src/config/bot.js b/src/config/bot.js index 86dc861cc3..6bb31a2741 100644 --- a/src/config/bot.js +++ b/src/config/bot.js @@ -51,7 +51,7 @@ export const botConfig = { maintenanceMode: process.env.MAINTENANCE_MODE === "true", // Command prefix for text-based commands (e.g., "!" for "!ping"). - // Supports both slash commands and prefix commands. + // The bot runs in prefix-only mode; slash commands are disabled. prefix: process.env.PREFIX || "!", }, diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js index 50a468b73e..a6d26ff605 100644 --- a/src/events/interactionCreate.js +++ b/src/events/interactionCreate.js @@ -1,40 +1,11 @@ -import { Events, MessageFlags } from 'discord.js'; +import { Events } from 'discord.js'; import { logger } from '../utils/logger.js'; -import { getGuildConfig } from '../services/config/guildConfig.js'; -import { - getBotMessage, - isBotOwner, - isCommandCategoryEnabled, - isMaintenanceMode, -} from '../config/bot.js'; -import botConfig from '../config/bot.js'; import { handleApplicationModal } from '../commands/Community/apply.js'; import { handleInteractionError, createError, ErrorTypes, ErrorCodes } from '../utils/errorHandler.js'; import { InteractionHelper } from '../utils/interactionHelper.js'; import { createInteractionTraceContext, runWithTraceContext } from '../utils/logger.js'; -import { validateChatInputPayloadOrThrow } from '../utils/commandInputValidation.js'; -import { enforceAbuseProtection, formatCooldownDuration } from '../utils/abuseProtection.js'; -import { isCommandEnabled } from '../services/commandAccessService.js'; -import { resolveSlashAccessKey } from '../utils/messageAdapter.js'; import { isCollectorManagedComponent } from '../utils/collectorComponents.js'; import { ResponseCoordinator } from '../utils/responseCoordinator.js'; -import { enforceDefaultCommandPermissions } from '../utils/permissionGuard.js'; - -const COMMAND_ERROR_SUBTYPES = { - warn: 'warn_failed', - kick: 'kick_failed', - ban: 'ban_failed', - unban: 'unban_failed', - timeout: 'timeout_failed', - untimeout: 'untimeout_failed', - warnings: 'warnings_view_failed', - ticket: 'ticket_failed', - serverstats: 'serverstats_failed', - gcreate: 'giveaway_failed', - gend: 'giveaway_failed', - gdelete: 'giveaway_failed', - greroll: 'giveaway_failed', -}; function withTraceContext(context = {}, traceContext = {}) { return { @@ -58,255 +29,7 @@ export default { InteractionHelper.patchInteractionResponses(interaction); ResponseCoordinator.attach(interaction); - if (interaction.isChatInputCommand()) { - try { - logger.info(`Command executed: /${interaction.commandName} by ${interaction.user.tag}`, { - event: 'interaction.command.received', - traceId: interactionTraceContext.traceId, - guildId: interaction.guildId, - userId: interaction.user?.id, - command: interaction.commandName - }); - - validateChatInputPayloadOrThrow(interaction, withTraceContext({ - type: 'command_input_validation', - commandName: interaction.commandName - }, interactionTraceContext)); - - const command = client.commands.get(interaction.commandName); - - if (!command) { - throw createError( - `No command matching ${interaction.commandName} was found.`, - ErrorTypes.CONFIGURATION, - 'Sorry, that command does not exist.', - withTraceContext({ commandName: interaction.commandName }, interactionTraceContext) - ); - } - - if (isMaintenanceMode() && !isBotOwner(interaction.user.id)) { - throw createError( - 'Bot is in maintenance mode', - ErrorTypes.CONFIGURATION, - getBotMessage('maintenanceMode'), - withTraceContext({ commandName: interaction.commandName }, interactionTraceContext) - ); - } - - if (!isCommandCategoryEnabled(command.category)) { - throw createError( - `Feature disabled for category ${command.category}`, - ErrorTypes.CONFIGURATION, - getBotMessage('commandDisabled'), - withTraceContext({ commandName: interaction.commandName, category: command.category }, interactionTraceContext) - ); - } - - const defaultCooldownSec = Number(botConfig.commands?.defaultCooldown) || 0; - if (defaultCooldownSec > 0 && !isBotOwner(interaction.user.id)) { - const cooldownKey = `${interaction.user.id}:${interaction.commandName}`; - const expiresAt = client.cooldowns.get(cooldownKey); - - if (expiresAt && Date.now() < expiresAt) { - const remainingSec = Math.ceil((expiresAt - Date.now()) / 1000); - throw createError( - `Default command cooldown active for ${interaction.commandName}`, - ErrorTypes.RATE_LIMIT, - getBotMessage('cooldownActive', { time: `${remainingSec}s` }), - withTraceContext({ commandName: interaction.commandName, remainingSec }, interactionTraceContext) - ); - } - - client.cooldowns.set(cooldownKey, Date.now() + defaultCooldownSec * 1000); - } - - const abuseProtection = await enforceAbuseProtection(interaction, command, interaction.commandName); - if (!abuseProtection.allowed) { - const formattedCooldown = formatCooldownDuration(abuseProtection.remainingMs); - throw createError( - `Risky command cooldown active for ${interaction.commandName}`, - ErrorTypes.RATE_LIMIT, - `This command is on cooldown. Please wait ${formattedCooldown} before trying again.`, - withTraceContext({ - commandName: interaction.commandName, - subtype: 'command_cooldown', - expected: true, - cooldownMs: abuseProtection.remainingMs, - cooldownWindowMs: abuseProtection.policy?.windowMs, - cooldownMaxAttempts: abuseProtection.policy?.maxAttempts - }, interactionTraceContext) - ); - } - - let guildConfig = null; - if (interaction.guild) { - guildConfig = await getGuildConfig(client, interaction.guild.id, interactionTraceContext); - const accessKey = resolveSlashAccessKey(interaction); - if (!(await isCommandEnabled(client, interaction.guild.id, accessKey, command.category))) { - throw createError( - `Command ${accessKey} is disabled in this guild`, - ErrorTypes.CONFIGURATION, - 'This command has been disabled for this server.', - withTraceContext({ commandName: accessKey, guildId: interaction.guild.id }, interactionTraceContext) - ); - } - } - - const permissionAllowed = await enforceDefaultCommandPermissions(interaction, command, { - source: 'interactionCreate', - guildConfig, - }); - if (!permissionAllowed) { - return; - } - - await command.execute(interaction, guildConfig, client); - } catch (error) { - await handleInteractionError(interaction, error, withTraceContext({ - type: 'command', - commandName: interaction.commandName, - subtype: COMMAND_ERROR_SUBTYPES[interaction.commandName] || error?.context?.subtype, - }, interactionTraceContext)); - } - } else if (interaction.isAutocomplete()) { - const autocompleteCommand = client.commands.get(interaction.commandName); - if (autocompleteCommand?.autocomplete) { - try { - await autocompleteCommand.autocomplete(interaction, client); - } catch (error) { - logger.error('Error handling command autocomplete:', { - error: error.message, - guildId: interaction.guildId, - commandName: interaction.commandName, - }); - await interaction.respond([]).catch(() => {}); - } - return; - } - - const focusedOption = interaction.options.getFocused(true); - - if (interaction.commandName === 'apply' && focusedOption.name === 'application') { - try { - const { getApplicationRoles } = await import('../utils/database.js'); - const roles = await getApplicationRoles(client, interaction.guildId); - const roleName = interaction.options.getString('application', false); - - const filtered = roles.filter(role => - role.enabled !== false && - role.name.toLowerCase().startsWith(roleName?.toLowerCase() || '') - ); - - await interaction.respond( - filtered.slice(0, 25).map(role => ({ - name: `${role.name}${role.enabled === false ? ' (disabled)' : ''}`, - value: role.name - })) - ); - } catch (error) { - logger.error('Error handling autocomplete:', { - error: error.message, - guildId: interaction.guildId, - commandName: interaction.commandName - }); - await interaction.respond([]); - } - } else if (interaction.commandName === 'app-admin' && focusedOption.name === 'application') { - try { - const { getApplicationRoles } = await import('../utils/database.js'); - const roles = await getApplicationRoles(client, interaction.guildId); - const appName = interaction.options.getString('application', false); - - const filtered = roles.filter(role => - role.name.toLowerCase().startsWith(appName?.toLowerCase() || '') - ); - - await interaction.respond( - filtered.slice(0, 25).map(role => ({ - name: `${role.name}${role.enabled === false ? ' (disabled)' : ''}`, - value: role.name - })) - ); - } catch (error) { - logger.error('Error handling app-admin autocomplete:', { - error: error.message, - guildId: interaction.guildId, - commandName: interaction.commandName - }); - await interaction.respond([]); - } - } else if (interaction.commandName === 'reactroles' && focusedOption.name === 'panel') { - try { - const { getAllReactionRoleMessages, deleteReactionRoleMessage } = await import('../services/reactionRoleService.js'); - const guildId = interaction.guildId; - const guild = interaction.guild; - - let panels = await getAllReactionRoleMessages(client, guildId); - - if (!panels || panels.length === 0) { - await interaction.respond([]); - return; - } - - const validPanels = []; - for (const panel of panels) { - if (!panel.messageId || !panel.channelId) { - continue; - } - - const channel = guild.channels.cache.get(panel.channelId); - if (!channel) { - await deleteReactionRoleMessage(client, guildId, panel.messageId).catch(() => {}); - continue; - } - - const msg = await channel.messages.fetch(panel.messageId).catch(() => null); - if (!msg) { - await deleteReactionRoleMessage(client, guildId, panel.messageId).catch(() => {}); - continue; - } - validPanels.push(panel); - } - - if (validPanels.length === 0) { - await interaction.respond([]); - return; - } - - const choices = await Promise.all( - validPanels.slice(0, 25).map(async panel => { - try { - const channel = guild.channels.cache.get(panel.channelId); - if (!channel) return null; - - const msg = await channel.messages.fetch(panel.messageId).catch(() => null); - if (!msg) return null; - - const title = msg?.embeds?.[0]?.title ?? 'Untitled Panel'; - const channelName = channel?.name ?? 'unknown'; - - return { - name: `${title} (${channelName})`.substring(0, 100), - value: panel.messageId - }; - } catch (e) { - return null; - } - }) - ); - - const validChoices = choices.filter(c => c !== null); - await interaction.respond(validChoices); - } catch (error) { - logger.error('Error handling reactroles autocomplete:', { - error: error.message, - guildId: interaction.guildId, - commandName: interaction.commandName - }); - await interaction.respond([]); - } - } - } else if (interaction.isButton()) { + if (interaction.isButton()) { if (interaction.customId.startsWith('shared_todo_')) { const parts = interaction.customId.split('_'); const buttonType = parts.slice(0, 3).join('_'); @@ -468,4 +191,4 @@ export default { } }); } -}; \ No newline at end of file +}; diff --git a/src/handlers/loaders/commandLoader.js b/src/handlers/loaders/commandLoader.js index ecb7ad96d4..c481ab3432 100644 --- a/src/handlers/loaders/commandLoader.js +++ b/src/handlers/loaders/commandLoader.js @@ -3,12 +3,9 @@ import path from 'path'; import { fileURLToPath, pathToFileURL } from 'url'; import { Collection } from 'discord.js'; import { logger } from '../../utils/logger.js'; -import botConfig from '../../config/bot.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const MAX_COMMANDS = 100; -const COMMAND_COUNT_WARN_THRESHOLD = 90; function getSubcommandInfo(commandData) { const subcommands = []; @@ -100,179 +97,28 @@ export async function loadCommands(client) { } } - const commandsWithSubcommands = Array.from(client.commands.values()).filter(cmd => { - const subcommands = getSubcommandInfo(cmd.data.toJSON()); - return subcommands.length > 0; - }); - - const totalSubcommands = commandsWithSubcommands.reduce((total, cmd) => { - return total + getSubcommandInfo(cmd.data.toJSON()).length; - }, 0); - - const uniqueCommands = new Set(); - for (const [name, command] of client.commands.entries()) { - if (command.data && command.data.name) { - uniqueCommands.add(command.data.name); - } - } - - logger.info(`Loaded ${uniqueCommands.size} commands`); + logger.info(`Loaded ${client.commands.size} commands`); return client.commands; } -function collectCommandPayloads(client) { - const commands = []; - let totalSubcommands = 0; - const registeredNames = new Set(); - - for (const command of client.commands.values()) { - if (!command.data || typeof command.data.toJSON !== 'function') { - logger.warn(`Command missing data or toJSON method: ${command}`); - continue; - } - - const commandName = command.data.name; - logger.debug(`Processing command for registration: ${commandName}`); - - if (registeredNames.has(commandName)) { - logger.debug(`Skipping duplicate command: ${commandName}`); - continue; - } - - registeredNames.add(commandName); - const commandJson = command.data.toJSON(); - commands.push(commandJson); - totalSubcommands += getSubcommandInfo(commandJson).length; - - if (process.env.NODE_ENV !== 'production') { - logger.debug(`Registering command: ${commandName}`); - } - } - - return { commands, totalSubcommands }; -} - -function validateCommands(commands) { - const validationErrors = []; - - for (const cmd of commands) { - if (cmd.name && cmd.name.length > 32) { - validationErrors.push(`Command ${cmd.name} has name longer than 32 chars: "${cmd.name}" (${cmd.name.length} chars)`); - } - if (cmd.description && cmd.description.length > 110) { - validationErrors.push(`Command ${cmd.name} has description longer than 110 chars: "${cmd.description}" (${cmd.description.length} chars)`); - } - - if (!cmd.options) { - continue; - } - - for (const option of cmd.options) { - if (option.name && option.name.length > 32) { - validationErrors.push(`Command ${cmd.name} option ${option.name} has name longer than 32 chars: "${option.name}" (${option.name.length} chars)`); - } - if (option.description && option.description.length > 110) { - validationErrors.push(`Command ${cmd.name} option ${option.name} has description longer than 110 chars: "${option.description}" (${option.description.length} chars)`); - } - - if (option.choices) { - for (const choice of option.choices) { - if (choice.name && choice.name.length > 110) { - validationErrors.push(`Command ${cmd.name} option ${option.name} choice ${choice.name} has name longer than 110 chars: "${choice.name}" (${choice.name.length} chars)`); - } - if (choice.value && choice.value.length > 100) { - validationErrors.push(`Command ${cmd.name} option ${option.name} choice ${choice.name} has value longer than 100 chars: "${choice.value}" (${choice.value.length} chars)`); - } - } - } - - if (!option.options) { - continue; - } - - for (const subOption of option.options) { - if (subOption.name && subOption.name.length > 32) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} has name longer than 32 chars: "${subOption.name}" (${subOption.name.length} chars)`); - } - if (subOption.description && subOption.description.length > 110) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} has description longer than 110 chars: "${subOption.description}" (${subOption.description.length} chars)`); - } - - if (!subOption.choices) { - continue; - } - - for (const choice of subOption.choices) { - if (choice.name && choice.name.length > 110) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} choice ${choice.name} has name longer than 110 chars: "${choice.name}" (${choice.name.length} chars)`); - } - if (choice.value && choice.value.length > 100) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} choice ${choice.name} has value longer than 100 chars: "${choice.value}" (${choice.value.length} chars)`); - } - } - } - } - } - - if (validationErrors.length > 0) { - logger.error('Command validation failed. Errors:'); - validationErrors.forEach((error) => logger.error(` - ${error}`)); - throw new Error(`Command validation failed with ${validationErrors.length} errors`); - } -} - -function prepareCommandsForRegistration(commands) { - if (commands.length >= COMMAND_COUNT_WARN_THRESHOLD) { - logger.warn(`Command count (${commands.length}) is near Discord's ${MAX_COMMANDS} global command limit`); - } - - if (commands.length <= MAX_COMMANDS) { - return commands; - } - - logger.warn(`Command count (${commands.length}) exceeds Discord limit (${MAX_COMMANDS}), truncating...`); - const truncated = commands.slice(0, MAX_COMMANDS); - logger.info(`Truncated to ${truncated.length} commands for registration`); - return truncated; -} +/** + * Clears all globally registered slash commands. + * The bot runs in prefix-only mode, so no slash commands are registered. + */ +export async function clearSlashCommands(client, options = {}) { + const { clientId = null } = options; -async function registerGlobalCommands(client, clientId, commands, totalSubcommands) { if (!clientId) { - throw new Error('CLIENT_ID is required for slash command registration'); + throw new Error('CLIENT_ID is required to clear slash commands'); } if (!client.rest) { - throw new Error('Discord REST client is not available for slash command registration'); - } - - logger.info(`Preparing to register ${totalSubcommands + commands.length} commands globally`); - logger.info('Validating commands before registration...'); - validateCommands(commands); - logger.info('Command validation passed'); - - const commandsToRegister = prepareCommandsForRegistration(commands); - - if (botConfig.commands?.deleteCommands) { - logger.info('Clearing existing global commands before registration...'); - await client.rest.put(`/applications/${clientId}/commands`, { body: [] }); + throw new Error('Discord REST client is not available to clear slash commands'); } - logger.info(`Registering ${commandsToRegister.length} global commands...`); - await client.rest.put(`/applications/${clientId}/commands`, { body: commandsToRegister }); - logger.info(`Successfully registered ${commandsToRegister.length} global commands`); - logger.info('Global commands may take up to an hour to appear in all servers on first deploy'); -} - -export async function registerCommands(client, options = {}) { - const { clientId = null } = options; - - try { - const { commands, totalSubcommands } = collectCommandPayloads(client); - await registerGlobalCommands(client, clientId, commands, totalSubcommands); - } catch (error) { - logger.error('Error registering commands:', error); - throw error; - } + logger.info('Clearing registered global slash commands (prefix-only mode)...'); + await client.rest.put(`/applications/${clientId}/commands`, { body: [] }); + logger.info('Successfully cleared registered global slash commands'); } export async function reloadCommand(client, commandName) { @@ -297,4 +143,4 @@ export async function reloadCommand(client, commandName) { logger.error(`Error reloading command "${commandName}":`, error); return { success: false, message: `Error reloading command: ${error.message}` }; } -} \ No newline at end of file +} diff --git a/src/utils/commandInputValidation.js b/src/utils/commandInputValidation.js deleted file mode 100644 index 3dd278a065..0000000000 --- a/src/utils/commandInputValidation.js +++ /dev/null @@ -1,52 +0,0 @@ -// commandInputValidation.js - -import { z } from 'zod'; -import { createError, ErrorTypes } from './errorHandler.js'; - -const OptionValueSchema = z.union([ - z.string().max(2000), - z.number().finite(), - z.boolean(), - z.null() -]); - -const CommandOptionSchema = z.lazy(() => - z.object({ - name: z.string().min(1).max(32), - type: z.number().int().min(1).max(20), - value: OptionValueSchema.optional(), - options: z.array(CommandOptionSchema).max(25).optional() - }) -); - -const CommandInputSchema = z.object({ - commandName: z.string().min(1).max(32), - options: z.array(CommandOptionSchema).max(25) -}); - -export function validateChatInputPayloadOrThrow(interaction, context = {}) { - const payload = { - commandName: interaction?.commandName, - options: Array.isArray(interaction?.options?.data) ? interaction.options.data : [] - }; - - const parsed = CommandInputSchema.safeParse(payload); - if (parsed.success) { - return parsed.data; - } - - throw createError( - 'Invalid command input payload', - ErrorTypes.VALIDATION, - 'One or more command inputs are invalid. Please review your options and try again.', - { - ...context, - errorCode: 'VALIDATION_FAILED', - issues: parsed.error.issues.map((issue) => ({ - path: issue.path.join('.'), - message: issue.message, - code: issue.code - })) - } - ); -} \ No newline at end of file diff --git a/src/utils/messageAdapter.js b/src/utils/messageAdapter.js index 96916fc711..e2b88c76fd 100644 --- a/src/utils/messageAdapter.js +++ b/src/utils/messageAdapter.js @@ -15,21 +15,6 @@ function getCommandJson(commandData) { return commandData?.toJSON ? commandData.toJSON() : commandData; } -export function resolveSlashAccessKey(interaction) { - const subcommandGroup = interaction.options.getSubcommandGroup(false); - const subcommand = interaction.options.getSubcommand(false); - - if (subcommandGroup && subcommand) { - return `${interaction.commandName} ${subcommandGroup} ${subcommand}`; - } - - if (subcommand) { - return `${interaction.commandName} ${subcommand}`; - } - - return interaction.commandName; -} - export function resolvePrefixAccessKey(commandData, args) { const options = mapArgumentsToOptions(args, commandData); const subcommand = options.getSubcommand(); From 071c8adbaeece067754002e2c71a5c2db5e320bf Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 10:45:13 +0000 Subject: [PATCH 03/14] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/fish.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 5288f048bb..8538af1585 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -5,8 +5,8 @@ import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHan import { InteractionHelper } from '../../utils/interactionHelper.js'; const FISH_COOLDOWN = 45 * 60 * 1000; -const BASE_MIN_REWARD = 300; -const BASE_MAX_REWARD = 900; +const BASE_MIN_REWARD = 1500; +const BASE_MAX_REWARD = 4500; const FISHING_ROD_MULTIPLIER = 1.5; const FISH_TYPES = [ From 31d52859daf34c27d192d68af18b4f8bcb749826 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 11:20:05 +0000 Subject: [PATCH 04/14] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/buy.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index 1a90387315..6b71fa421f 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -28,7 +28,9 @@ export default { ), execute: withErrorHandling(async (interaction, config, client) => { - const deferred = await InteractionHelper.safeDefer(interaction); + const deferred = await InteractionHelper.safeDefer(interaction, { + flags: MessageFlags.Ephemeral, + }); if (!deferred) return; const userId = interaction.user.id; @@ -155,6 +157,21 @@ export default { inline: true, }); - await InteractionHelper.safeEditReply(interaction, { embeds: [embed], flags: [MessageFlags.Ephemeral] }); + if (interaction._isPrefixCommand) { + try { + await interaction.user.send({ embeds: [embed] }); + const sourceMessage = InteractionHelper.getCoordinator(interaction)?.message; + await sourceMessage?.delete().catch(() => {}); + } catch { + const sourceMessage = InteractionHelper.getCoordinator(interaction)?.message; + await sourceMessage?.delete().catch(() => {}); + await interaction.channel.send( + `${interaction.user}, pembelian berhasil, tetapi DM bot kamu tertutup. Aktifkan DM agar struk pembelian bisa dikirim secara pribadi.` + ); + } + return; + } + + await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); }, { command: 'buy' }) -}; \ No newline at end of file +}; From 352e422645150ee312ba49912b14ee7211700f42 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 11:24:50 +0000 Subject: [PATCH 05/14] feat: update workspace Co-authored-by: traeagent --- src/app.js | 16 +-- src/events/interactionCreate.js | 159 ++++++++++++++++++++++++++ src/handlers/loaders/commandLoader.js | 37 ++++-- 3 files changed, 194 insertions(+), 18 deletions(-) diff --git a/src/app.js b/src/app.js index 227c38819c..f4ed84bc85 100644 --- a/src/app.js +++ b/src/app.js @@ -11,7 +11,7 @@ import { getServerCounters, saveServerCounters, updateCounter } from './services import { logger, startupLog, shutdownLog } from './utils/logger.js'; import { checkBirthdays } from './services/birthdayService.js'; import { checkGiveaways } from './services/giveawayService.js'; -import { loadCommands, clearSlashCommands as clearGlobalSlashCommands } from './handlers/loaders/commandLoader.js'; +import { loadCommands, registerCommands as registerGlobalSlashCommands } from './handlers/loaders/commandLoader.js'; import { runSafeTask, handleTaskError, ErrorCodes } from './utils/errorHandler.js'; import { initializeMusic } from './services/music/riffySetup.js'; import { shutdownMusic } from './services/music/playerHandler.js'; @@ -90,9 +90,9 @@ class TitanBot extends Client { await this.login(this.config.bot.token); startupLog('Discord login successful'); - startupLog('Prefix-only mode: clearing registered slash commands...'); - await this.clearSlashCommands(); - startupLog('Slash commands cleared'); + startupLog('Registering slash commands globally...'); + await this.registerCommands(); + startupLog('Slash commands registration complete'); const databaseMode = dbStatus.isDegraded ? 'Optional in-memory mode (data resets after restart)' @@ -322,11 +322,11 @@ class TitanBot extends Client { } } - async clearSlashCommands() { + async registerCommands() { try { - await clearGlobalSlashCommands(this, { clientId: this.config.bot.clientId }); + await registerGlobalSlashCommands(this, { clientId: this.config.bot.clientId }); } catch (error) { - logger.error('Error clearing slash commands:', error); + logger.error('Error registering slash commands:', error); } } @@ -428,4 +428,4 @@ try { process.exit(1); } -export default TitanBot; \ No newline at end of file +export default TitanBot; diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js index a6d26ff605..32b3fd04c8 100644 --- a/src/events/interactionCreate.js +++ b/src/events/interactionCreate.js @@ -1,11 +1,137 @@ import { Events } from 'discord.js'; import { logger } from '../utils/logger.js'; +import { getGuildConfig } from '../services/config/guildConfig.js'; +import { + getBotMessage, + isBotOwner, + isCommandCategoryEnabled, + isMaintenanceMode, +} from '../config/bot.js'; +import botConfig from '../config/bot.js'; import { handleApplicationModal } from '../commands/Community/apply.js'; import { handleInteractionError, createError, ErrorTypes, ErrorCodes } from '../utils/errorHandler.js'; import { InteractionHelper } from '../utils/interactionHelper.js'; import { createInteractionTraceContext, runWithTraceContext } from '../utils/logger.js'; import { isCollectorManagedComponent } from '../utils/collectorComponents.js'; import { ResponseCoordinator } from '../utils/responseCoordinator.js'; +import { enforceAbuseProtection, formatCooldownDuration } from '../utils/abuseProtection.js'; +import { isCommandEnabled } from '../services/commandAccessService.js'; +import { enforceDefaultCommandPermissions } from '../utils/permissionGuard.js'; + +function getSlashAccessKey(interaction) { + const commandName = interaction.commandName; + const subcommandGroup = interaction.options.getSubcommandGroup(false); + const subcommand = interaction.options.getSubcommand(false); + + if (subcommandGroup && subcommand) { + return `${commandName} ${subcommandGroup} ${subcommand}`; + } + return subcommand ? `${commandName} ${subcommand}` : commandName; +} + +async function executeChatInputCommand(interaction, client, traceContext) { + const command = client.commands.get(interaction.commandName); + if (!command) { + throw createError( + `No command matching ${interaction.commandName} was found.`, + ErrorTypes.CONFIGURATION, + 'Sorry, that command does not exist.', + { commandName: interaction.commandName, traceId: traceContext.traceId } + ); + } + + if (isMaintenanceMode() && !isBotOwner(interaction.user.id)) { + throw createError('Bot is in maintenance mode', ErrorTypes.CONFIGURATION, getBotMessage('maintenanceMode')); + } + + if (!isCommandCategoryEnabled(command.category)) { + throw createError( + `Feature disabled for category ${command.category}`, + ErrorTypes.CONFIGURATION, + getBotMessage('commandDisabled') + ); + } + + const defaultCooldownSec = Number(botConfig.commands?.defaultCooldown) || 0; + if (defaultCooldownSec > 0 && !isBotOwner(interaction.user.id)) { + const cooldownKey = `${interaction.user.id}:${interaction.commandName}`; + const expiresAt = client.cooldowns.get(cooldownKey); + if (expiresAt && Date.now() < expiresAt) { + const remainingSec = Math.ceil((expiresAt - Date.now()) / 1000); + throw createError( + `Default command cooldown active for ${interaction.commandName}`, + ErrorTypes.RATE_LIMIT, + getBotMessage('cooldownActive', { time: `${remainingSec}s` }) + ); + } + client.cooldowns.set(cooldownKey, Date.now() + defaultCooldownSec * 1000); + } + + const abuseProtection = await enforceAbuseProtection(interaction, command, interaction.commandName); + if (!abuseProtection.allowed) { + const formattedCooldown = formatCooldownDuration(abuseProtection.remainingMs); + throw createError( + `Risky command cooldown active for ${interaction.commandName}`, + ErrorTypes.RATE_LIMIT, + `This command is on cooldown. Please wait ${formattedCooldown} before trying again.`, + { expected: true, subtype: 'command_cooldown' } + ); + } + + let guildConfig = null; + if (interaction.guildId) { + guildConfig = await getGuildConfig(client, interaction.guildId, traceContext); + const accessKey = getSlashAccessKey(interaction); + if (!(await isCommandEnabled(client, interaction.guildId, accessKey, command.category))) { + throw createError( + `Command ${accessKey} is disabled in this guild`, + ErrorTypes.CONFIGURATION, + 'This command has been disabled for this server.', + { commandName: accessKey, guildId: interaction.guildId } + ); + } + } + + const permissionAllowed = await enforceDefaultCommandPermissions(interaction, command, { + source: 'interactionCreate', + guildConfig, + }); + if (permissionAllowed) { + await command.execute(interaction, guildConfig, client); + } +} + +async function handleAutocomplete(interaction, client) { + const command = client.commands.get(interaction.commandName); + if (command?.autocomplete) { + await command.autocomplete(interaction, client); + return; + } + + const focusedOption = interaction.options.getFocused(true); + if ( + ['apply', 'app-admin'].includes(interaction.commandName) + && focusedOption.name === 'application' + ) { + try { + const { getApplicationRoles } = await import('../utils/database.js'); + const roles = await getApplicationRoles(client, interaction.guildId); + const query = String(focusedOption.value || '').toLowerCase(); + const choices = roles + .filter((role) => interaction.commandName !== 'apply' || role.enabled !== false) + .filter((role) => role.name.toLowerCase().startsWith(query)) + .slice(0, 25) + .map((role) => ({ name: role.name, value: role.name })); + await interaction.respond(choices); + } catch (error) { + logger.error('Error handling application autocomplete:', error); + await interaction.respond([]).catch(() => {}); + } + return; + } + + await interaction.respond([]).catch(() => {}); +} function withTraceContext(context = {}, traceContext = {}) { return { @@ -29,6 +155,39 @@ export default { InteractionHelper.patchInteractionResponses(interaction); ResponseCoordinator.attach(interaction); + if (interaction.isChatInputCommand()) { + try { + logger.info(`Command executed: /${interaction.commandName} by ${interaction.user.tag}`, { + event: 'interaction.command.received', + traceId: interactionTraceContext.traceId, + guildId: interaction.guildId, + userId: interaction.user?.id, + command: interaction.commandName, + }); + await executeChatInputCommand(interaction, client, interactionTraceContext); + } catch (error) { + await handleInteractionError(interaction, error, withTraceContext({ + type: 'command', + commandName: interaction.commandName, + }, interactionTraceContext)); + } + return; + } + + if (interaction.isAutocomplete()) { + try { + await handleAutocomplete(interaction, client); + } catch (error) { + logger.error('Error handling command autocomplete:', { + error: error.message, + guildId: interaction.guildId, + commandName: interaction.commandName, + }); + await interaction.respond([]).catch(() => {}); + } + return; + } + if (interaction.isButton()) { if (interaction.customId.startsWith('shared_todo_')) { const parts = interaction.customId.split('_'); diff --git a/src/handlers/loaders/commandLoader.js b/src/handlers/loaders/commandLoader.js index c481ab3432..c3c83785cb 100644 --- a/src/handlers/loaders/commandLoader.js +++ b/src/handlers/loaders/commandLoader.js @@ -101,24 +101,41 @@ export async function loadCommands(client) { return client.commands; } -/** - * Clears all globally registered slash commands. - * The bot runs in prefix-only mode, so no slash commands are registered. - */ -export async function clearSlashCommands(client, options = {}) { +export async function registerCommands(client, options = {}) { const { clientId = null } = options; if (!clientId) { - throw new Error('CLIENT_ID is required to clear slash commands'); + throw new Error('CLIENT_ID is required for slash command registration'); } if (!client.rest) { - throw new Error('Discord REST client is not available to clear slash commands'); + throw new Error('Discord REST client is not available for slash command registration'); } - logger.info('Clearing registered global slash commands (prefix-only mode)...'); - await client.rest.put(`/applications/${clientId}/commands`, { body: [] }); - logger.info('Successfully cleared registered global slash commands'); + const commands = []; + const registeredNames = new Set(); + + for (const command of client.commands.values()) { + if (!command.data?.toJSON) { + continue; + } + + const commandJson = command.data.toJSON(); + if (registeredNames.has(commandJson.name)) { + continue; + } + + registeredNames.add(commandJson.name); + commands.push(commandJson); + } + + if (commands.length > 100) { + throw new Error(`Cannot register ${commands.length} global slash commands; Discord allows at most 100`); + } + + logger.info(`Registering ${commands.length} global slash commands...`); + await client.rest.put(`/applications/${clientId}/commands`, { body: commands }); + logger.info('Successfully registered global slash commands'); } export async function reloadCommand(client, commandName) { From 40494135463279283582e3027feea1deabdb0fde Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 11:44:58 +0000 Subject: [PATCH 06/14] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/fish.js | 5 +++-- src/config/shop/items.js | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 8538af1585..f4752beae2 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -1,5 +1,6 @@ import { SlashCommandBuilder } from 'discord.js'; import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; +import { getItemById } from '../../config/shop/items.js'; import { getEconomyData, setEconomyData } from '../../utils/economy.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; @@ -7,7 +8,7 @@ import { InteractionHelper } from '../../utils/interactionHelper.js'; const FISH_COOLDOWN = 45 * 60 * 1000; const BASE_MIN_REWARD = 1500; const BASE_MAX_REWARD = 4500; -const FISHING_ROD_MULTIPLIER = 1.5; +const FISHING_ROD_MULTIPLIER = getItemById('fishing_rod')?.effect?.multiplier ?? 1.5; const FISH_TYPES = [ { name: 'Bass', emoji: '🐟', rarity: 'common' }, @@ -129,4 +130,4 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); }, { command: 'fish' }) -}; \ No newline at end of file +}; diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 5f47eaeaf5..9cbc505524 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -66,12 +66,12 @@ roleId: null, id: 'fishing_rod', name: 'šŸŽ£ Fishing Rod', price: 5000, - description: 'Used for fishing commands', + description: 'Increases fishing earnings by 50%.', type: 'tool', durability: 100, effect: { type: 'fishing_yield', - multiplier: 1.0 + multiplier: 1.5 } }, { @@ -200,4 +200,4 @@ export function validatePurchase(itemId, userData) { } return { valid: true }; -} \ No newline at end of file +} From 3892ee46f6629318e46ffab4a2559bcfe5e5c41c Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 11:50:50 +0000 Subject: [PATCH 07/14] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/buy.js | 3 +++ src/commands/Economy/fish.js | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index 6b71fa421f..0e85901207 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -141,6 +141,9 @@ export default { } else if (item.type === "consumable" || item.type === "tool") { userData.inventory[itemId] = (userData.inventory[itemId] || 0) + quantity; + if (itemId === "fishing_rod") { + userData.fishingRodOwned = true; + } if (item.type === "tool") { successDescription += `\n\n**šŸ› ļø ${item.name} added to your inventory!**`; } diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index f4752beae2..25172d7cc8 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -45,7 +45,8 @@ export default { const userData = await getEconomyData(client, guildId, userId); const lastFish = userData.lastFish || 0; - const hasFishingRod = userData.inventory["fishing_rod"] || 0; + const hasFishingRod = userData.fishingRodOwned === true + || Number(userData.inventory?.["fishing_rod"] || 0) > 0; if (now < lastFish + FISH_COOLDOWN) { const remaining = lastFish + FISH_COOLDOWN - now; @@ -98,6 +99,9 @@ export default { userData.wallet += finalEarned; userData.lastFish = now; + if (hasFishingRod) { + userData.fishingRodOwned = true; + } await setEconomyData(client, guildId, userId, userData); From 9dc7788f96ba0fac65a1cd32026ecb4a0ed5f5b3 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 11:55:12 +0000 Subject: [PATCH 08/14] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/buy.js | 9 ++++++--- src/commands/Economy/fish.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index 0e85901207..b58e86479e 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -64,6 +64,8 @@ export default { const PREMIUM_ROLE_ID = guildConfig.premiumRoleId; const userData = await getEconomyData(client, guildId, userId); + const alreadyOwnsFishingRod = userData.fishingRodOwned === true + || Number(userData.inventory?.fishing_rod || 0) > 0; if (userData.wallet < totalCost) { throw createError( @@ -141,14 +143,15 @@ export default { } else if (item.type === "consumable" || item.type === "tool") { userData.inventory[itemId] = (userData.inventory[itemId] || 0) + quantity; - if (itemId === "fishing_rod") { - userData.fishingRodOwned = true; - } if (item.type === "tool") { successDescription += `\n\n**šŸ› ļø ${item.name} added to your inventory!**`; } } + if (alreadyOwnsFishingRod || itemId === "fishing_rod") { + userData.fishingRodOwned = true; + } + await setEconomyData(client, guildId, userId, userData); const embed = successEmbed( diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 25172d7cc8..3040804293 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -90,7 +90,7 @@ export default { let finalEarned = baseEarned; let multiplierMessage = ""; - if (hasFishingRod > 0) { + if (hasFishingRod) { finalEarned = Math.floor(baseEarned * FISHING_ROD_MULTIPLIER); multiplierMessage = `\nšŸŽ£ **Fishing Rod Bonus: +50%**`; } From 00f3bd2986b05e67319790b15a47c9f3b3cc3917 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 12:14:33 +0000 Subject: [PATCH 09/14] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/fish.js | 4 ++-- src/config/shop/items.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 3040804293..c06aa71b5c 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -8,7 +8,7 @@ import { InteractionHelper } from '../../utils/interactionHelper.js'; const FISH_COOLDOWN = 45 * 60 * 1000; const BASE_MIN_REWARD = 1500; const BASE_MAX_REWARD = 4500; -const FISHING_ROD_MULTIPLIER = getItemById('fishing_rod')?.effect?.multiplier ?? 1.5; +const FISHING_ROD_MULTIPLIER = getItemById('fishing_rod')?.effect?.multiplier ?? 1.1; const FISH_TYPES = [ { name: 'Bass', emoji: '🐟', rarity: 'common' }, @@ -92,7 +92,7 @@ export default { if (hasFishingRod) { finalEarned = Math.floor(baseEarned * FISHING_ROD_MULTIPLIER); - multiplierMessage = `\nšŸŽ£ **Fishing Rod Bonus: +50%**`; + multiplierMessage = `\nšŸŽ£ **Fishing Rod Bonus: +10%**`; } const catchMessage = CATCH_MESSAGES[Math.floor(Math.random() * CATCH_MESSAGES.length)]; diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 9cbc505524..bbce032c10 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -66,12 +66,12 @@ roleId: null, id: 'fishing_rod', name: 'šŸŽ£ Fishing Rod', price: 5000, - description: 'Increases fishing earnings by 50%.', + description: 'Increases fishing earnings by 10%.', type: 'tool', durability: 100, effect: { type: 'fishing_yield', - multiplier: 1.5 + multiplier: 1.1 } }, { From 5c7db804936d046bae02e513a05b53c38ea0625f Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 12:16:45 +0000 Subject: [PATCH 10/14] feat: update workspace Co-authored-by: traeagent --- src/utils/economy.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/economy.js b/src/utils/economy.js index c50e9ab1f3..535687b1e3 100644 --- a/src/utils/economy.js +++ b/src/utils/economy.js @@ -356,8 +356,12 @@ export function getShopInventory() { name: 'Fishing Rod', emoji: 'šŸŽ£', price: 500, - description: 'Catch fish to sell for profit!', - type: 'tool' + description: 'Increases fishing earnings by 10%.', + type: 'tool', + effect: { + type: 'fishing_yield', + multiplier: 1.1 + } }, { id: 'hunting_rifle', @@ -396,4 +400,4 @@ export function getShopInventory() { use: 'gamble' } ]; -} \ No newline at end of file +} From 08a20c82b8a99374601bdac87b9cc5a2177fc36c Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 12:25:15 +0000 Subject: [PATCH 11/14] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/buy.js | 10 ++++++++++ src/config/shop/items.js | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index b58e86479e..c10838ca32 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -7,6 +7,8 @@ import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHan import { InteractionHelper } from '../../utils/interactionHelper.js'; const SHOP_ITEMS = shopItems; +const LUCK_POTION_IDS = new Set(['lucky_clover', 'lucky_charm']); +const DEFAULT_LUCK_POTION_DURATION_MS = 5 * 60 * 1000; export default { data: new SlashCommandBuilder() @@ -140,6 +142,14 @@ export default { } else if (item.type === "upgrade") { userData.upgrades[itemId] = true; successDescription += `\n\n**✨ Your upgrade is now active!**`; + } else if (item.type === "consumable" && LUCK_POTION_IDS.has(itemId)) { + const now = Date.now(); + const expiresAt = userData.luckPotionExpiresAt || {}; + const currentExpiry = Math.max(now, Number(expiresAt[itemId]) || 0); + const duration = Number(item.effect?.duration) || DEFAULT_LUCK_POTION_DURATION_MS; + expiresAt[itemId] = currentExpiry + (duration * quantity); + userData.luckPotionExpiresAt = expiresAt; + successDescription += `\n\nšŸ€ **Active for ${Math.ceil((expiresAt[itemId] - now) / 60000)} minutes. Timer started at purchase.**`; } else if (item.type === "consumable" || item.type === "tool") { userData.inventory[itemId] = (userData.inventory[itemId] || 0) + quantity; diff --git a/src/config/shop/items.js b/src/config/shop/items.js index bbce032c10..aac8d84cce 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -53,13 +53,13 @@ roleId: null, id: 'lucky_clover', name: 'Lucky Clover', price: 10000, - description: 'Increases the chance of winning a higher payout on `/gamble` once.', + description: 'Boosts `/gamble` win chance for 5 minutes after purchase.', type: 'consumable', maxQuantity: 10, effect: { type: 'gamble_boost', multiplier: 1.5, - uses: 1 + duration: 300000 } }, { @@ -102,13 +102,13 @@ roleId: null, id: 'lucky_charm', name: 'šŸ€ Lucky Charm', price: 10000, - description: 'Increases luck for gambling. Has 3 uses before being consumed.', + description: 'Boosts `/gamble` win chance for 5 minutes after purchase.', type: 'consumable', maxQuantity: 10, effect: { type: 'gamble_boost', multiplier: 1.3, - uses: 3 + duration: 300000 } }, { From d12808f1b7c4fdb90005e968629ff75ddeda35e2 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 12:27:55 +0000 Subject: [PATCH 12/14] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/fish.js | 76 +++++++++++++++++++++---------- src/commands/Economy/inventory.js | 40 ++++++++-------- src/config/shop/items.js | 12 ++--- 3 files changed, 78 insertions(+), 50 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index c06aa71b5c..7fafee4b14 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -9,6 +9,9 @@ const FISH_COOLDOWN = 45 * 60 * 1000; const BASE_MIN_REWARD = 1500; const BASE_MAX_REWARD = 4500; const FISHING_ROD_MULTIPLIER = getItemById('fishing_rod')?.effect?.multiplier ?? 1.1; +const LUCK_POTION_IDS = ['lucky_clover', 'lucky_charm']; +const LUCK_POTION_FISH_COOLDOWN = 30 * 1000; +const DEFAULT_LUCK_POTION_DURATION = 5 * 60 * 1000; const FISH_TYPES = [ { name: 'Bass', emoji: '🐟', rarity: 'common' }, @@ -30,6 +33,23 @@ const CATCH_MESSAGES = [ "You reel in your catch with expert precision...", ]; +function getFishForLuckBonus(luckBonus = 0) { + const roll = Math.random(); + const commonThreshold = 0.5 - luckBonus; + const uncommonThreshold = 0.75 - (luckBonus * 0.6); + const rareThreshold = 0.9 - (luckBonus * 0.3); + const epicThreshold = 0.98 - (luckBonus * 0.1); + + let rarity = 'legendary'; + if (roll < commonThreshold) rarity = 'common'; + else if (roll < uncommonThreshold) rarity = 'uncommon'; + else if (roll < rareThreshold) rarity = 'rare'; + else if (roll < epicThreshold) rarity = 'epic'; + + const fishOfRarity = FISH_TYPES.filter((fish) => fish.rarity === rarity); + return fishOfRarity[Math.floor(Math.random() * fishOfRarity.length)]; +} + export default { data: new SlashCommandBuilder() .setName('fish') @@ -47,41 +67,45 @@ export default { const lastFish = userData.lastFish || 0; const hasFishingRod = userData.fishingRodOwned === true || Number(userData.inventory?.["fishing_rod"] || 0) > 0; + const inventory = userData.inventory || {}; + const potionExpiries = { ...(userData.luckPotionExpiresAt || {}) }; + + for (const potionId of LUCK_POTION_IDS) { + const legacyQuantity = Number(inventory[potionId] || 0); + if (Number(potionExpiries[potionId]) <= now && legacyQuantity > 0) { + const duration = Number(getItemById(potionId)?.effect?.duration) + || DEFAULT_LUCK_POTION_DURATION; + potionExpiries[potionId] = now + (duration * legacyQuantity); + inventory[potionId] = 0; + } + } - if (now < lastFish + FISH_COOLDOWN) { - const remaining = lastFish + FISH_COOLDOWN - now; + userData.luckPotionExpiresAt = potionExpiries; + const activePotionId = LUCK_POTION_IDS.find( + (potionId) => Number(potionExpiries[potionId]) > now + ) || null; + const luckBonus = activePotionId + ? Number(getItemById(activePotionId)?.effect?.fishingLuckBonus) || 0 + : 0; + const fishCooldown = activePotionId ? LUCK_POTION_FISH_COOLDOWN : FISH_COOLDOWN; + + if (now < lastFish + fishCooldown) { + const remaining = lastFish + fishCooldown - now; const hours = Math.floor(remaining / (1000 * 60 * 60)); const minutes = Math.floor( (remaining % (1000 * 60 * 60)) / (1000 * 60), ); + const seconds = Math.ceil((remaining % (1000 * 60)) / 1000); 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.`, + `You're too tired to fish right now. Rest for **${hours}h ${minutes}m ${seconds}s** 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 fishCaught = getFishForLuckBonus(luckBonus); const baseEarned = Math.floor( Math.random() * (BASE_MAX_REWARD - BASE_MIN_REWARD + 1) @@ -96,6 +120,10 @@ export default { } const catchMessage = CATCH_MESSAGES[Math.floor(Math.random() * CATCH_MESSAGES.length)]; + const luckMessage = activePotionId + ? `\nšŸ€ **${getItemById(activePotionId).name} Active:** +${Math.round(luckBonus * 100)}% chance for higher-rarity fish.` + : ""; + const nextFishingText = activePotionId ? "30 seconds while your potion is active." : "45 minutes."; userData.wallet += finalEarned; userData.lastFish = now; @@ -115,7 +143,7 @@ export default { const embed = createEmbed({ title: 'Fishing Success!', - description: `${catchMessage}\n\nYou caught a **${fishCaught.emoji} ${fishCaught.name}**! You sold it for **$${finalEarned.toLocaleString()}**!${multiplierMessage}`, + description: `${catchMessage}\n\nYou caught a **${fishCaught.emoji} ${fishCaught.name}**! You sold it for **$${finalEarned.toLocaleString()}**!${multiplierMessage}${luckMessage}`, color: rarityColors[fishCaught.rarity] }) .addFields( @@ -130,7 +158,7 @@ export default { inline: true, } ) - .setFooter({ text: `Next fishing trip available in 45 minutes.` }); + .setFooter({ text: `Next fishing trip available in ${nextFishingText}` }); await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); }, { command: 'fish' }) diff --git a/src/commands/Economy/inventory.js b/src/commands/Economy/inventory.js index b7efcfec80..f176397bc7 100644 --- a/src/commands/Economy/inventory.js +++ b/src/commands/Economy/inventory.js @@ -34,25 +34,25 @@ export default { } 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"); - } + const inventoryLines = Object.entries(inventory) + .filter(([itemId, quantity]) => { + const item = SHOP_ITEMS.find(i => i.id === itemId); + return Number(quantity) > 0 && item; + }) + .map(([itemId, quantity]) => { + const item = SHOP_ITEMS.find(i => i.id === itemId); + return `**${item.name}:** ${quantity}x`; + }); + const potionLines = Object.entries(userData.luckPotionExpiresAt || {}) + .filter(([, expiresAt]) => Number(expiresAt) > Date.now()) + .map(([itemId, expiresAt]) => { + const item = SHOP_ITEMS.find(i => i.id === itemId); + const minutesLeft = Math.ceil((Number(expiresAt) - Date.now()) / 60000); + return item ? `šŸ€ **${item.name}:** Active (${minutesLeft}m left)` : null; + }) + .filter(Boolean); + const inventoryDescription = [...inventoryLines, ...potionLines].join("\n") + || "Your inventory is currently empty."; logger.info(`[ECONOMY] Inventory retrieved`, { userId, @@ -67,4 +67,4 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); }, { command: 'inventory' }) -}; \ No newline at end of file +}; diff --git a/src/config/shop/items.js b/src/config/shop/items.js index aac8d84cce..23696e9a78 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -53,12 +53,12 @@ roleId: null, id: 'lucky_clover', name: 'Lucky Clover', price: 10000, - description: 'Boosts `/gamble` win chance for 5 minutes after purchase.', + description: 'Boosts the chance of catching higher-rarity fish for 5 minutes after purchase.', type: 'consumable', maxQuantity: 10, effect: { - type: 'gamble_boost', - multiplier: 1.5, + type: 'fishing_luck', + fishingLuckBonus: 0.1, duration: 300000 } }, @@ -102,12 +102,12 @@ roleId: null, id: 'lucky_charm', name: 'šŸ€ Lucky Charm', price: 10000, - description: 'Boosts `/gamble` win chance for 5 minutes after purchase.', + description: 'Boosts the chance of catching higher-rarity fish for 5 minutes after purchase.', type: 'consumable', maxQuantity: 10, effect: { - type: 'gamble_boost', - multiplier: 1.3, + type: 'fishing_luck', + fishingLuckBonus: 0.08, duration: 300000 } }, From f73f16c46fe87227e2ff9967641ea3a4d6d373d5 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 12:30:29 +0000 Subject: [PATCH 13/14] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/buy.js | 8 +++- src/commands/Economy/fish.js | 8 +++- src/config/shop/items.js | 77 +++++++++++++++++++++++++++++++++--- 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index c10838ca32..153c9fb103 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -7,7 +7,13 @@ import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHan import { InteractionHelper } from '../../utils/interactionHelper.js'; const SHOP_ITEMS = shopItems; -const LUCK_POTION_IDS = new Set(['lucky_clover', 'lucky_charm']); +const LUCK_POTION_IDS = new Set([ + 'luck_potion_1', + 'luck_potion_2', + 'luck_potion_3', + 'luck_potion_4', + 'luck_potion_5', +]); const DEFAULT_LUCK_POTION_DURATION_MS = 5 * 60 * 1000; export default { diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 7fafee4b14..556914488f 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -9,7 +9,13 @@ const FISH_COOLDOWN = 45 * 60 * 1000; const BASE_MIN_REWARD = 1500; const BASE_MAX_REWARD = 4500; const FISHING_ROD_MULTIPLIER = getItemById('fishing_rod')?.effect?.multiplier ?? 1.1; -const LUCK_POTION_IDS = ['lucky_clover', 'lucky_charm']; +const LUCK_POTION_IDS = [ + 'luck_potion_5', + 'luck_potion_4', + 'luck_potion_3', + 'luck_potion_2', + 'luck_potion_1', +]; const LUCK_POTION_FISH_COOLDOWN = 30 * 1000; const DEFAULT_LUCK_POTION_DURATION = 5 * 60 * 1000; diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 23696e9a78..794cdf8846 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -53,13 +53,13 @@ roleId: null, id: 'lucky_clover', name: 'Lucky Clover', price: 10000, - description: 'Boosts the chance of catching higher-rarity fish for 5 minutes after purchase.', + description: 'Increases the chance of winning a higher payout on `/gamble` once.', type: 'consumable', maxQuantity: 10, effect: { - type: 'fishing_luck', - fishingLuckBonus: 0.1, - duration: 300000 + type: 'gamble_boost', + multiplier: 1.5, + uses: 1 } }, { @@ -102,12 +102,77 @@ roleId: null, id: 'lucky_charm', name: 'šŸ€ Lucky Charm', price: 10000, - description: 'Boosts the chance of catching higher-rarity fish for 5 minutes after purchase.', + description: 'Increases luck for gambling. Has 3 uses before being consumed.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'gamble_boost', + multiplier: 1.3, + uses: 3 + } + }, + { + id: 'luck_potion_1', + name: 'Luck Potion I', + price: 10000, + description: 'Boosts higher-rarity fish chances by 5% for 5 minutes after purchase.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + fishingLuckBonus: 0.05, + duration: 300000 + } + }, + { + id: 'luck_potion_2', + name: 'Luck Potion II', + price: 20000, + description: 'Boosts higher-rarity fish chances by 10% for 5 minutes after purchase.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + fishingLuckBonus: 0.1, + duration: 300000 + } + }, + { + id: 'luck_potion_3', + name: 'Luck Potion III', + price: 30000, + description: 'Boosts higher-rarity fish chances by 15% for 5 minutes after purchase.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + fishingLuckBonus: 0.15, + duration: 300000 + } + }, + { + id: 'luck_potion_4', + name: 'Luck Potion IV', + price: 40000, + description: 'Boosts higher-rarity fish chances by 20% for 5 minutes after purchase.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + fishingLuckBonus: 0.2, + duration: 300000 + } + }, + { + id: 'luck_potion_5', + name: 'Luck Potion V', + price: 50000, + description: 'Boosts higher-rarity fish chances by 25% for 5 minutes after purchase.', type: 'consumable', maxQuantity: 10, effect: { type: 'fishing_luck', - fishingLuckBonus: 0.08, + fishingLuckBonus: 0.25, duration: 300000 } }, From a1046d19559c4e29c254eec1852f5137d67e167e Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 12:59:52 +0000 Subject: [PATCH 14/14] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/daily.js | 31 ++++--------------------------- src/commands/Economy/fish.js | 22 +++++++++++++++++----- src/config/shop/items.js | 4 ++-- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/commands/Economy/daily.js b/src/commands/Economy/daily.js index 4045d23ed0..32cf3977a0 100644 --- a/src/commands/Economy/daily.js +++ b/src/commands/Economy/daily.js @@ -1,7 +1,6 @@ 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/config/guildConfig.js'; import { formatDuration } from '../../utils/embeds.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; import { logger } from '../../utils/logger.js'; @@ -10,7 +9,6 @@ import { botConfig } from '../../config/bot.js'; const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; const DAILY_AMOUNT = botConfig.economy?.dailyAmount ?? 100; -const PREMIUM_BONUS_PERCENTAGE = 0.1; export default { data: new SlashCommandBuilder() @@ -50,25 +48,7 @@ export default { ); } - 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; - } + const earned = DAILY_AMOUNT; userData.wallet = (userData.wallet || 0) + earned; userData.lastDaily = now; @@ -80,13 +60,12 @@ export default { 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}` + `You have claimed your daily **$${earned.toLocaleString()}**!` ) .addFields({ name: "New Cash Balance", @@ -94,11 +73,9 @@ export default { inline: true, }) .setFooter({ - text: hasPremiumRole - ? `Next claim in 24 hours. (Premium Active)` - : `Next claim in 24 hours.`, + text: `Next claim in 24 hours.`, }); await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); }, { command: 'daily' }) -}; \ No newline at end of file +}; diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 556914488f..90ed06015f 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -4,11 +4,13 @@ import { getItemById } from '../../config/shop/items.js'; import { getEconomyData, setEconomyData } from '../../utils/economy.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; +import { getGuildConfig } from '../../services/config/guildConfig.js'; const FISH_COOLDOWN = 45 * 60 * 1000; const BASE_MIN_REWARD = 1500; const BASE_MAX_REWARD = 4500; const FISHING_ROD_MULTIPLIER = getItemById('fishing_rod')?.effect?.multiplier ?? 1.1; +const PREMIUM_FISHING_MULTIPLIER = getItemById('premium_role')?.effect?.multiplier ?? 1.1; const LUCK_POTION_IDS = [ 'luck_potion_5', 'luck_potion_4', @@ -73,6 +75,11 @@ export default { const lastFish = userData.lastFish || 0; const hasFishingRod = userData.fishingRodOwned === true || Number(userData.inventory?.["fishing_rod"] || 0) > 0; + const guildConfig = await getGuildConfig(client, guildId); + const premiumRoleId = guildConfig.premiumRoleId; + const hasPremiumRole = Boolean( + premiumRoleId && interaction.member?.roles?.cache?.has(premiumRoleId) + ); const inventory = userData.inventory || {}; const potionExpiries = { ...(userData.luckPotionExpiresAt || {}) }; @@ -117,13 +124,18 @@ export default { Math.random() * (BASE_MAX_REWARD - BASE_MIN_REWARD + 1) ) + BASE_MIN_REWARD; - let finalEarned = baseEarned; - let multiplierMessage = ""; - + let fishingMultiplier = 1; + const bonusMessages = []; if (hasFishingRod) { - finalEarned = Math.floor(baseEarned * FISHING_ROD_MULTIPLIER); - multiplierMessage = `\nšŸŽ£ **Fishing Rod Bonus: +10%**`; + fishingMultiplier *= FISHING_ROD_MULTIPLIER; + bonusMessages.push(`šŸŽ£ **Fishing Rod Bonus: +${Math.round((FISHING_ROD_MULTIPLIER - 1) * 100)}%**`); + } + if (hasPremiumRole) { + fishingMultiplier *= PREMIUM_FISHING_MULTIPLIER; + bonusMessages.push(`✨ **Premium Fishing Bonus: +${Math.round((PREMIUM_FISHING_MULTIPLIER - 1) * 100)}%**`); } + const finalEarned = Math.floor(baseEarned * fishingMultiplier); + const multiplierMessage = bonusMessages.length ? `\n${bonusMessages.join('\n')}` : ""; const catchMessage = CATCH_MESSAGES[Math.floor(Math.random() * CATCH_MESSAGES.length)]; const luckMessage = activePotionId diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 794cdf8846..4668cdbf87 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -41,11 +41,11 @@ cooldown: 86400000, id: 'premium_role', name: 'Premium Server Role', price: 15000, - description: 'A special role granting a fancy color and a 10% daily bonus.', + description: 'A special role granting a fancy color and a 10% fishing earnings bonus.', type: 'role', roleId: null, effect: { - type: 'daily_bonus', + type: 'fishing_yield', multiplier: 1.1 } },