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
234 lines
7.6 KiB
JavaScript
234 lines
7.6 KiB
JavaScript
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`;
|
||
}
|