From 467fd8467fb279d6163665095c4c866d15eb277c Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Fri, 12 Dec 2025 23:53:31 +0000 Subject: [PATCH] Add command cooldown management to the dashboard interface Implement new API endpoints and UI elements for managing command cooldowns, including setting, listing, and resetting them. Also, refactor cooldown cache invalidation logic to resolve circular dependency issues. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 10157b12-ee9f-4185-90e0-2ab9db23a6da Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/bakeZwZ Replit-Helium-Checkpoint-Created: true --- aethex-bot/commands/cooldowns.js | 234 ++++++++++++++++++++++++++++ aethex-bot/public/dashboard.html | 141 +++++++++++++++++ aethex-bot/server/webServer.js | 130 ++++++++++++++++ aethex-bot/utils/cooldownManager.js | 145 +++++++++++++++++ 4 files changed, 650 insertions(+) create mode 100644 aethex-bot/commands/cooldowns.js create mode 100644 aethex-bot/utils/cooldownManager.js diff --git a/aethex-bot/commands/cooldowns.js b/aethex-bot/commands/cooldowns.js new file mode 100644 index 0000000..a71e2a8 --- /dev/null +++ b/aethex-bot/commands/cooldowns.js @@ -0,0 +1,234 @@ +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); +const { invalidateCooldownCache, DEFAULT_COOLDOWNS } = require('../utils/cooldownManager'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('cooldowns') + .setDescription('Manage command cooldowns for this server') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand(sub => + sub.setName('set') + .setDescription('Set a custom cooldown for a command') + .addStringOption(opt => + opt.setName('command') + .setDescription('The command to set cooldown for') + .setRequired(true) + .addChoices( + { name: 'work', value: 'work' }, + { name: 'daily', value: 'daily' }, + { name: 'slots', value: 'slots' }, + { name: 'coinflip', value: 'coinflip' }, + { name: 'rep', value: 'rep' }, + { name: 'trivia', value: 'trivia' }, + { name: 'heist', value: 'heist' }, + { name: 'duel', value: 'duel' }, + { name: 'gift', value: 'gift' }, + { name: 'trade', value: 'trade' } + )) + .addIntegerOption(opt => + opt.setName('seconds') + .setDescription('Cooldown in seconds (0 = no cooldown, -1 = reset to default)') + .setRequired(true) + .setMinValue(-1) + .setMaxValue(604800))) + .addSubcommand(sub => + sub.setName('list') + .setDescription('View all custom cooldowns for this server')) + .addSubcommand(sub => + sub.setName('reset') + .setDescription('Reset a command to its default cooldown') + .addStringOption(opt => + opt.setName('command') + .setDescription('The command to reset (or "all" for all commands)') + .setRequired(true) + .addChoices( + { name: 'work', value: 'work' }, + { name: 'daily', value: 'daily' }, + { name: 'slots', value: 'slots' }, + { name: 'coinflip', value: 'coinflip' }, + { name: 'rep', value: 'rep' }, + { name: 'trivia', value: 'trivia' }, + { name: 'heist', value: 'heist' }, + { name: 'duel', value: 'duel' }, + { name: 'gift', value: 'gift' }, + { name: 'trade', value: 'trade' }, + { name: 'All Commands', value: 'all' } + ))), + + async execute(interaction, client, supabase) { + if (!supabase) { + return interaction.reply({ + content: '❌ Database not configured. Command cooldowns require Supabase.', + ephemeral: true + }); + } + + const guildId = interaction.guildId; + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand) { + case 'set': + return handleSet(interaction, supabase, guildId); + case 'list': + return handleList(interaction, supabase, guildId); + case 'reset': + return handleReset(interaction, supabase, guildId); + } + }, + + DEFAULT_COOLDOWNS +}; + +async function handleSet(interaction, supabase, guildId) { + const command = interaction.options.getString('command'); + const seconds = interaction.options.getInteger('seconds'); + + try { + if (seconds === -1) { + const { error } = await supabase + .from('command_cooldowns') + .delete() + .eq('guild_id', guildId) + .eq('command_name', command); + + if (error) throw error; + + invalidateCooldownCache(guildId, command); + + return interaction.reply({ + content: `✅ Reset \`/${command}\` to default cooldown (${formatDuration(DEFAULT_COOLDOWNS[command])}).`, + ephemeral: true + }); + } + + const { error } = await supabase + .from('command_cooldowns') + .upsert({ + guild_id: guildId, + command_name: command, + cooldown_seconds: seconds, + updated_at: new Date().toISOString() + }, { onConflict: 'guild_id,command_name' }); + + if (error) throw error; + + invalidateCooldownCache(guildId, command); + + const message = seconds === 0 + ? `✅ Disabled cooldown for \`/${command}\`.` + : `✅ Set \`/${command}\` cooldown to **${formatDuration(seconds)}**.`; + + return interaction.reply({ content: message, ephemeral: true }); + } catch (error) { + console.error('Failed to set cooldown:', error.message); + return interaction.reply({ + content: '❌ Failed to update cooldown. Please try again.', + ephemeral: true + }); + } +} + +async function handleList(interaction, supabase, guildId) { + try { + const { data: customCooldowns } = await supabase + .from('command_cooldowns') + .select('*') + .eq('guild_id', guildId) + .order('command_name'); + + const customMap = new Map(); + (customCooldowns || []).forEach(c => customMap.set(c.command_name, c.cooldown_seconds)); + + const lines = Object.entries(DEFAULT_COOLDOWNS).map(([cmd, defaultSec]) => { + const customSec = customMap.get(cmd); + const isCustom = customSec !== undefined; + const currentSec = isCustom ? customSec : defaultSec; + + if (currentSec === 0) { + return `⚡ \`/${cmd}\` - **No cooldown** ${isCustom ? '*(custom)*' : ''}`; + } + + const duration = formatDuration(currentSec); + const status = isCustom ? `**${duration}** *(custom)*` : `${duration} *(default)*`; + return `⏱️ \`/${cmd}\` - ${status}`; + }); + + const embed = new EmbedBuilder() + .setTitle('⏱️ Command Cooldowns') + .setColor(0x6366f1) + .setDescription(lines.join('\n')) + .setFooter({ text: 'Use /cooldowns set to customize' }) + .setTimestamp(); + + return interaction.reply({ embeds: [embed] }); + } catch (error) { + console.error('Failed to list cooldowns:', error.message); + return interaction.reply({ + content: '❌ Failed to fetch cooldown settings. Please try again.', + ephemeral: true + }); + } +} + +async function handleReset(interaction, supabase, guildId) { + const command = interaction.options.getString('command'); + + try { + let query = supabase.from('command_cooldowns').delete().eq('guild_id', guildId); + + if (command !== 'all') { + query = query.eq('command_name', command); + } + + const { data, error } = await query.select(); + + if (error) throw error; + + if (command === 'all') { + invalidateCooldownCache(guildId); + } else { + invalidateCooldownCache(guildId, command); + } + + const count = data?.length || 0; + + if (command === 'all') { + return interaction.reply({ + content: `✅ Reset **${count}** command(s) to their default cooldowns.`, + ephemeral: true + }); + } + + if (count === 0) { + return interaction.reply({ + content: `ℹ️ \`/${command}\` was already using the default cooldown.`, + ephemeral: true + }); + } + + return interaction.reply({ + content: `✅ Reset \`/${command}\` to default cooldown (${formatDuration(DEFAULT_COOLDOWNS[command])}).`, + ephemeral: true + }); + } catch (error) { + console.error('Failed to reset cooldown:', error.message); + return interaction.reply({ + content: '❌ Failed to reset cooldown. Please try again.', + ephemeral: true + }); + } +} + +function formatDuration(seconds) { + if (seconds === 0) return 'No cooldown'; + if (seconds < 60) return `${seconds}s`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m`; + if (seconds < 86400) { + const hours = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; + } + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + return hours > 0 ? `${days}d ${hours}h` : `${days}d`; +} diff --git a/aethex-bot/public/dashboard.html b/aethex-bot/public/dashboard.html index 85a4fcb..5ed8710 100644 --- a/aethex-bot/public/dashboard.html +++ b/aethex-bot/public/dashboard.html @@ -1186,6 +1186,9 @@ + @@ -2055,6 +2058,30 @@ + +