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
This commit is contained in:
parent
3e26b99d65
commit
467fd8467f
4 changed files with 650 additions and 0 deletions
234
aethex-bot/commands/cooldowns.js
Normal file
234
aethex-bot/commands/cooldowns.js
Normal file
|
|
@ -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`;
|
||||
}
|
||||
|
|
@ -1186,6 +1186,9 @@
|
|||
<div class="nav-item" data-page="activity-roles">
|
||||
<span class="nav-icon">🎯</span> Activity Roles
|
||||
</div>
|
||||
<div class="nav-item" data-page="cooldowns">
|
||||
<span class="nav-icon">⏱️</span> Cooldowns
|
||||
</div>
|
||||
<div class="nav-item" data-page="federation">
|
||||
<span class="nav-icon">🌐</span> Federation
|
||||
</div>
|
||||
|
|
@ -2055,6 +2058,30 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-cooldowns" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Command <span class="text-gradient">Cooldowns</span></h1>
|
||||
<p class="page-subtitle">Customize how often members can use specific commands</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Cooldown Settings</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p style="color:var(--muted);margin-bottom:1.5rem;font-size:0.9rem">
|
||||
Set custom cooldowns for commands. Use 0 for no cooldown, or click Reset to use the default value.
|
||||
</p>
|
||||
<div id="cooldownsList">
|
||||
<div class="empty-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p style="margin-top:1rem">Loading cooldowns...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-federation" class="page hidden">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title"><span class="text-gradient">Federation</span> Management</h1>
|
||||
|
|
@ -2345,6 +2372,7 @@
|
|||
case 'moderation': loadModerationData(); break;
|
||||
case 'analytics': loadAnalyticsData(); break;
|
||||
case 'activity-roles': loadActivityRoles(); break;
|
||||
case 'cooldowns': loadCooldowns(); break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3881,6 +3909,119 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Command Cooldowns Functions
|
||||
async function loadCooldowns() {
|
||||
if (!currentGuild) return;
|
||||
|
||||
const container = document.getElementById('cooldownsList');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guild/' + currentGuild + '/cooldowns');
|
||||
if (!res.ok) throw new Error('Failed to load');
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.cooldowns || data.cooldowns.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">No cooldown data available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const cmdEmojis = {
|
||||
work: '💼', daily: '📅', slots: '🎰', coinflip: '🪙',
|
||||
rep: '⭐', trivia: '🧠', heist: '💰', duel: '⚔️',
|
||||
gift: '🎁', trade: '🔄'
|
||||
};
|
||||
|
||||
container.innerHTML = data.cooldowns.map(c => {
|
||||
const currentSec = c.is_custom ? c.custom_seconds : c.default_seconds;
|
||||
const displayTime = formatCooldownTime(currentSec);
|
||||
const defaultTime = formatCooldownTime(c.default_seconds);
|
||||
|
||||
return `
|
||||
<div class="item-row" style="margin-bottom:0.75rem">
|
||||
<div style="width:40px;height:40px;background:linear-gradient(135deg,var(--gradient-1),var(--gradient-2));border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.25rem">${cmdEmojis[c.command] || '⏱️'}</div>
|
||||
<div class="item-info" style="flex:1">
|
||||
<div class="item-name">
|
||||
/${c.command}
|
||||
${c.is_custom ? '<span class="item-badge active">Custom</span>' : '<span class="item-badge">Default</span>'}
|
||||
</div>
|
||||
<div class="item-desc">Current: ${displayTime} ${c.is_custom ? '(Default: ' + defaultTime + ')' : ''}</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:0.5rem">
|
||||
<input type="number" id="cooldown-${c.command}" class="form-input" style="width:80px"
|
||||
value="${currentSec}" min="0" max="604800" placeholder="seconds">
|
||||
<button class="btn btn-secondary" style="padding:0.5rem 0.75rem" onclick="saveCooldown('${c.command}')">Save</button>
|
||||
${c.is_custom ? `<button class="btn-icon danger" onclick="resetCooldown('${c.command}')" title="Reset to default">↺</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
container.innerHTML = '<div class="empty-state">Failed to load cooldowns</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function formatCooldownTime(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 h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
return m > 0 ? h + 'h ' + m + 'm' : h + 'h';
|
||||
}
|
||||
const d = Math.floor(seconds / 86400);
|
||||
const h = Math.floor((seconds % 86400) / 3600);
|
||||
return h > 0 ? d + 'd ' + h + 'h' : d + 'd';
|
||||
}
|
||||
|
||||
async function saveCooldown(command) {
|
||||
if (!currentGuild) return;
|
||||
|
||||
const input = document.getElementById('cooldown-' + command);
|
||||
const seconds = parseInt(input.value, 10);
|
||||
|
||||
if (isNaN(seconds) || seconds < 0 || seconds > 604800) {
|
||||
alert('Please enter a valid value between 0 and 604800 (7 days)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guild/' + currentGuild + '/cooldowns', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command, seconds })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
await loadCooldowns();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert(err.error || 'Failed to save cooldown');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to save cooldown');
|
||||
}
|
||||
}
|
||||
|
||||
async function resetCooldown(command) {
|
||||
if (!confirm('Reset /' + command + ' to default cooldown?')) return;
|
||||
if (!currentGuild) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guild/' + currentGuild + '/cooldowns/' + command, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
await loadCooldowns();
|
||||
} else {
|
||||
alert('Failed to reset cooldown');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to reset cooldown');
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const cors = require('cors');
|
|||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const Stripe = require('stripe');
|
||||
const { invalidateCooldownCache } = require('../utils/cooldownManager');
|
||||
|
||||
function createWebServer(discordClient, supabase, options = {}) {
|
||||
const app = express();
|
||||
|
|
@ -2269,6 +2270,135 @@ function createWebServer(discordClient, supabase, options = {}) {
|
|||
}
|
||||
});
|
||||
|
||||
// Command Cooldowns API endpoints
|
||||
const DEFAULT_COOLDOWNS = {
|
||||
work: 3600,
|
||||
daily: 72000,
|
||||
slots: 60,
|
||||
coinflip: 30,
|
||||
rep: 43200,
|
||||
trivia: 30,
|
||||
heist: 1800,
|
||||
duel: 300,
|
||||
gift: 60,
|
||||
trade: 60
|
||||
};
|
||||
|
||||
app.get('/api/guild/:guildId/cooldowns', async (req, res) => {
|
||||
if (!supabase) return res.status(503).json({ error: 'Database not available' });
|
||||
|
||||
const userId = req.session.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
|
||||
|
||||
const { guildId } = req.params;
|
||||
|
||||
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
||||
if (!userGuild || !userGuild.isAdmin) {
|
||||
return res.status(403).json({ error: 'No admin access' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: customCooldowns } = await supabase
|
||||
.from('command_cooldowns')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.order('command_name');
|
||||
|
||||
const customMap = {};
|
||||
(customCooldowns || []).forEach(c => {
|
||||
customMap[c.command_name] = c.cooldown_seconds;
|
||||
});
|
||||
|
||||
const cooldowns = Object.entries(DEFAULT_COOLDOWNS).map(([cmd, defaultSec]) => ({
|
||||
command: cmd,
|
||||
default_seconds: defaultSec,
|
||||
custom_seconds: customMap[cmd] ?? null,
|
||||
is_custom: customMap[cmd] !== undefined
|
||||
}));
|
||||
|
||||
res.json({ cooldowns });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch cooldowns:', error);
|
||||
res.status(500).json({ error: 'Failed to load cooldowns' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/guild/:guildId/cooldowns', async (req, res) => {
|
||||
if (!supabase) return res.status(503).json({ error: 'Database not available' });
|
||||
|
||||
const userId = req.session.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
|
||||
|
||||
const { guildId } = req.params;
|
||||
|
||||
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
||||
if (!userGuild || !userGuild.isAdmin) {
|
||||
return res.status(403).json({ error: 'No admin access' });
|
||||
}
|
||||
|
||||
const { command, seconds } = req.body;
|
||||
|
||||
if (!command || !DEFAULT_COOLDOWNS.hasOwnProperty(command)) {
|
||||
return res.status(400).json({ error: 'Invalid command' });
|
||||
}
|
||||
|
||||
const parsedSeconds = parseInt(seconds, 10);
|
||||
if (isNaN(parsedSeconds) || parsedSeconds < 0 || parsedSeconds > 604800) {
|
||||
return res.status(400).json({ error: 'Seconds must be between 0 and 604800 (7 days)' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('command_cooldowns')
|
||||
.upsert({
|
||||
guild_id: guildId,
|
||||
command_name: command,
|
||||
cooldown_seconds: parsedSeconds,
|
||||
updated_at: new Date().toISOString()
|
||||
}, { onConflict: 'guild_id,command_name' });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
invalidateCooldownCache(guildId, command);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to set cooldown:', error);
|
||||
res.status(500).json({ error: 'Failed to save cooldown' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/guild/:guildId/cooldowns/:command', async (req, res) => {
|
||||
if (!supabase) return res.status(503).json({ error: 'Database not available' });
|
||||
|
||||
const userId = req.session.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
|
||||
|
||||
const { guildId, command } = req.params;
|
||||
|
||||
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
||||
if (!userGuild || !userGuild.isAdmin) {
|
||||
return res.status(403).json({ error: 'No admin access' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('command_cooldowns')
|
||||
.delete()
|
||||
.eq('guild_id', guildId)
|
||||
.eq('command_name', command);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
invalidateCooldownCache(guildId, command);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to reset cooldown:', error);
|
||||
res.status(500).json({ error: 'Failed to reset cooldown' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
|
|
|
|||
145
aethex-bot/utils/cooldownManager.js
Normal file
145
aethex-bot/utils/cooldownManager.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
const DEFAULT_COOLDOWNS = {
|
||||
work: 3600,
|
||||
daily: 72000,
|
||||
slots: 60,
|
||||
coinflip: 30,
|
||||
rep: 43200,
|
||||
trivia: 30,
|
||||
heist: 1800,
|
||||
duel: 300,
|
||||
gift: 60,
|
||||
trade: 60
|
||||
};
|
||||
|
||||
const userCooldowns = new Map();
|
||||
|
||||
const cooldownCache = new Map();
|
||||
const CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
async function getCommandCooldown(supabase, guildId, commandName) {
|
||||
if (!supabase) {
|
||||
return DEFAULT_COOLDOWNS[commandName] || 0;
|
||||
}
|
||||
|
||||
const cacheKey = `${guildId}-${commandName}`;
|
||||
const cached = cooldownCache.get(cacheKey);
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('command_cooldowns')
|
||||
.select('cooldown_seconds')
|
||||
.eq('guild_id', guildId)
|
||||
.eq('command_name', commandName)
|
||||
.maybeSingle();
|
||||
|
||||
const cooldown = data?.cooldown_seconds ?? DEFAULT_COOLDOWNS[commandName] ?? 0;
|
||||
|
||||
cooldownCache.set(cacheKey, { value: cooldown, timestamp: Date.now() });
|
||||
|
||||
return cooldown;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch cooldown:', error.message);
|
||||
return DEFAULT_COOLDOWNS[commandName] || 0;
|
||||
}
|
||||
}
|
||||
|
||||
function getCooldownKey(guildId, userId, commandName) {
|
||||
return `${guildId}-${userId}-${commandName}`;
|
||||
}
|
||||
|
||||
function getUserCooldown(guildId, userId, commandName) {
|
||||
const key = getCooldownKey(guildId, userId, commandName);
|
||||
return userCooldowns.get(key);
|
||||
}
|
||||
|
||||
function setUserCooldown(guildId, userId, commandName) {
|
||||
const key = getCooldownKey(guildId, userId, commandName);
|
||||
userCooldowns.set(key, Date.now());
|
||||
}
|
||||
|
||||
function clearUserCooldown(guildId, userId, commandName) {
|
||||
const key = getCooldownKey(guildId, userId, commandName);
|
||||
userCooldowns.delete(key);
|
||||
}
|
||||
|
||||
async function checkCooldown(supabase, guildId, userId, commandName) {
|
||||
const cooldownSeconds = await getCommandCooldown(supabase, guildId, commandName);
|
||||
|
||||
if (cooldownSeconds === 0) {
|
||||
return { onCooldown: false, cooldownSeconds: 0 };
|
||||
}
|
||||
|
||||
const cooldownMs = cooldownSeconds * 1000;
|
||||
const lastUsed = getUserCooldown(guildId, userId, commandName);
|
||||
|
||||
if (!lastUsed) {
|
||||
return { onCooldown: false, cooldownSeconds };
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - lastUsed;
|
||||
const remaining = cooldownMs - elapsed;
|
||||
|
||||
if (remaining <= 0) {
|
||||
return { onCooldown: false, cooldownSeconds };
|
||||
}
|
||||
|
||||
return {
|
||||
onCooldown: true,
|
||||
remaining: remaining,
|
||||
remainingSeconds: Math.ceil(remaining / 1000),
|
||||
cooldownSeconds
|
||||
};
|
||||
}
|
||||
|
||||
function formatCooldownRemaining(remainingMs) {
|
||||
const seconds = Math.ceil(remainingMs / 1000);
|
||||
|
||||
if (seconds < 60) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
if (minutes < 60) {
|
||||
return secs > 0 ? `${minutes}m ${secs}s` : `${minutes}m`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
|
||||
if (hours < 24) {
|
||||
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
||||
}
|
||||
|
||||
const days = Math.floor(hours / 24);
|
||||
const hrs = hours % 24;
|
||||
return hrs > 0 ? `${days}d ${hrs}h` : `${days}d`;
|
||||
}
|
||||
|
||||
function invalidateCooldownCache(guildId, commandName = null) {
|
||||
if (commandName) {
|
||||
cooldownCache.delete(`${guildId}-${commandName}`);
|
||||
} else {
|
||||
for (const key of cooldownCache.keys()) {
|
||||
if (key.startsWith(`${guildId}-`)) {
|
||||
cooldownCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCommandCooldown,
|
||||
getUserCooldown,
|
||||
setUserCooldown,
|
||||
clearUserCooldown,
|
||||
checkCooldown,
|
||||
formatCooldownRemaining,
|
||||
invalidateCooldownCache,
|
||||
DEFAULT_COOLDOWNS
|
||||
};
|
||||
Loading…
Reference in a new issue