Notification subscriptions

This commit is contained in:
VinceC
2024-11-24 06:04:35 -06:00
parent 3b10e77c82
commit ad6d98d501
4 changed files with 654 additions and 327 deletions

View File

@@ -5,9 +5,14 @@ const NotificationService = require("./services/NotificationService.js");
const MatchCommands = require("./commands/MatchCommands.js"); const MatchCommands = require("./commands/MatchCommands.js");
const SupabaseService = require("./services/SupabaseService.js"); const SupabaseService = require("./services/SupabaseService.js");
const SubscriptionCommands = require("./commands/SubscriptionCommands.js"); const SubscriptionCommands = require("./commands/SubscriptionCommands.js");
const CooldownManager = require("./utils/CooldownManager.js");
const Logger = require("./utils/Logger.js");
class Bot { class Bot {
constructor() { constructor() {
this.logger = new Logger('Bot');
this.cooldownManager = new CooldownManager();
this.client = new Client({ this.client = new Client({
intents: [ intents: [
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds,
@@ -16,92 +21,121 @@ class Bot {
], ],
}); });
this.logger.info('Initializing bot...');
this.initializeServices();
this.setupEventHandlers();
}
initializeServices() {
this.logger.debug('Initializing services...');
this.playerService = new PlayerService(); this.playerService = new PlayerService();
this.commandHandler = new CommandHandler(this.playerService); this.commandHandler = new CommandHandler(this.playerService);
this.matchCommands = new MatchCommands(this.playerService); this.matchCommands = new MatchCommands(this.playerService);
this.notificationService = new NotificationService(this); this.notificationService = new NotificationService(this);
this.supabaseService = new SupabaseService(); this.initializeSupabase();
if (this.supabaseService.supabase) {
this.subscriptionCommands = new SubscriptionCommands(this.supabaseService);
} else {
console.warn('SupabaseService not initialized. Subscription commands will be unavailable.');
this.subscriptionCommands = null;
} }
this.setupEventHandlers(); initializeSupabase() {
this.logger.debug('Initializing Supabase service...');
this.supabaseService = new SupabaseService();
if (this.supabaseService.supabase) {
this.subscriptionCommands = new SubscriptionCommands(this.supabaseService);
this.logger.info('Supabase service initialized successfully');
} else {
this.logger.warn('Supabase initialization failed. Subscription commands unavailable.');
this.subscriptionCommands = null;
}
} }
setupEventHandlers() { setupEventHandlers() {
this.logger.debug('Setting up event handlers...');
this.client.on('error', this.handleError.bind(this)); this.client.on('error', this.handleError.bind(this));
this.client.once('ready', this.handleReady.bind(this)); this.client.once('ready', this.handleReady.bind(this));
this.client.on('interactionCreate', this.handleInteraction.bind(this)); this.client.on('interactionCreate', this.handleInteraction.bind(this));
} }
// In the start method, add Supabase connection test:
async start(token) { async start(token) {
console.log("Starting bot..."); this.logger.info('Starting bot...');
try { try {
// Test Supabase connection first
if (this.supabaseService) {
const connected = await this.supabaseService.testConnection();
if (!connected) {
console.warn('Supabase connection failed. Subscription features will be disabled.');
this.subscriptionCommands = null;
}
}
await this.client.login(token); await this.client.login(token);
console.log("Login successful"); this.logger.info('Login successful');
// Start notification service after bot is logged in
const port = process.env.NOTIFICATION_PORT || 3000; const port = process.env.NOTIFICATION_PORT || 3000;
await this.notificationService.start(port); await this.notificationService.start(port);
console.log(`Notification service started on port ${port}`); this.logger.info(`Notification service started on port ${port}`);
} catch (error) { } catch (error) {
console.error("Startup failed:", error); this.logger.error('Startup failed:', error);
throw error; throw error;
} }
} }
async stop() { async stop() {
console.log("Stopping bot..."); this.logger.info('Stopping bot...');
try { try {
await this.notificationService.stop(); await this.notificationService.stop();
await this.client.destroy(); await this.client.destroy();
console.log("Bot stopped successfully"); this.logger.info('Bot stopped successfully');
} catch (error) { } catch (error) {
console.error("Error stopping bot:", error); this.logger.error('Error stopping bot:', error);
throw error; throw error;
} }
} }
handleError(error) { handleError(error) {
console.error("Discord client error:", error); this.logger.error('Discord client error:', error);
} }
handleReady() { handleReady() {
console.log(`Logged in as ${this.client.user.tag}!`); this.logger.info(`Logged in as ${this.client.user.tag}`);
} }
async handleInteraction(interaction) { handleInteraction(interaction) {
try { this.logger.debug('Received interaction', {
type: interaction.type,
commandName: interaction.commandName,
user: interaction.user.tag,
guild: interaction.guild?.name,
channel: interaction.channel?.name
});
if (interaction.isCommand()) { if (interaction.isCommand()) {
await this.handleCommand(interaction); this.handleCommand(interaction)
.catch(error => this.handleInteractionError(interaction, error));
} else if (interaction.isButton()) { } else if (interaction.isButton()) {
await this.handleButton(interaction); this.handleButton(interaction)
} .catch(error => this.handleInteractionError(interaction, error));
} catch (error) {
console.error("Error handling interaction:", error);
await this.handleInteractionError(interaction, error);
} }
} }
async handleCommand(interaction) { async handleCommand(interaction) {
try { try {
// Defer the reply immediately for all commands // Check cooldown
await interaction.deferReply({ ephemeral: true }); const cooldownTime = this.cooldownManager.checkCooldown(interaction);
if (cooldownTime > 0) {
await interaction.reply({
content: `Please wait ${cooldownTime.toFixed(1)} more seconds before using this command again.`,
ephemeral: true
});
return;
}
this.logger.debug('Processing command', {
command: interaction.commandName,
user: interaction.user.tag
});
await this.processCommand(interaction);
} catch (error) {
this.logger.error('Command error:', error);
await this.handleCommandError(interaction, error);
}
}
async processCommand(interaction) {
switch (interaction.commandName) { switch (interaction.commandName) {
case "ping": case "ping":
await this.commandHandler.handlePing(interaction); await this.commandHandler.handlePing(interaction);
@@ -113,39 +147,54 @@ async start(token) {
await this.commandHandler.handleMatchHistory(interaction); await this.commandHandler.handleMatchHistory(interaction);
break; break;
case "subscribe": case "subscribe":
if (this.subscriptionCommands) {
await this.subscriptionCommands.handleSubscribe(interaction);
} else {
await interaction.editReply("Subscription commands are not available.");
}
break;
case "unsubscribe": case "unsubscribe":
if (this.subscriptionCommands) { case "list_subscriptions":
await this.subscriptionCommands.handleUnsubscribe(interaction); await this.handleSubscriptionCommand(interaction);
} else {
await interaction.editReply("Subscription commands are not available.");
}
break;
case "listsubscriptions":
if (this.subscriptionCommands) {
await this.subscriptionCommands.handleListSubscriptions(interaction);
} else {
await interaction.editReply("Subscription commands are not available.");
}
break; break;
default: default:
await interaction.editReply("Unknown command"); await interaction.reply({
} content: "Unknown command",
} catch (error) { ephemeral: true
console.error('Command error:', error); });
await this.handleCommandError(interaction, error);
} }
} }
async handleSubscriptionCommand(interaction) {
if (!this.subscriptionCommands) {
await this.safeReply(interaction, "Subscription commands are not available.");
return;
}
try {
// Defer the reply immediately
await this.safeReply(interaction, "Processing your request...");
switch (interaction.commandName) {
case "subscribe":
await this.subscriptionCommands.handleSubscribe(interaction);
break;
case "unsubscribe":
await this.subscriptionCommands.handleUnsubscribe(interaction);
break;
case "list_subscriptions":
await this.subscriptionCommands.handleListSubscriptions(interaction);
break;
}
} catch (error) {
this.logger.error('Subscription command error:', error);
await this.safeReply(interaction, 'An error occurred while processing your request.');
}
}
async handleButton(interaction) { async handleButton(interaction) {
const [action, matchId] = interaction.customId.split("_match_"); const [action, matchId] = interaction.customId.split("_match_");
try { try {
this.logger.debug('Processing button interaction', {
action,
matchId,
user: interaction.user.tag
});
switch (action) { switch (action) {
case "accept": case "accept":
await this.matchCommands.handleMatchAccept(interaction, matchId); await this.matchCommands.handleMatchAccept(interaction, matchId);
@@ -154,53 +203,61 @@ async start(token) {
await this.matchCommands.handleViewDetails(interaction, matchId); await this.matchCommands.handleViewDetails(interaction, matchId);
break; break;
default: default:
this.logger.warn('Unknown button action', { action });
await interaction.reply({ await interaction.reply({
content: "Unknown button action", content: "Unknown button action",
ephemeral: true, ephemeral: true,
}); });
} }
} catch (error) { } catch (error) {
console.error("Error handling button:", error); this.logger.error("Error handling button:", error);
await this.handleInteractionError(interaction, error); await this.handleInteractionError(interaction, error);
} }
} }
async handleCommandError(interaction, error) { async handleCommandError(interaction, error) {
console.error("Command error:", error); this.logger.error('Command error:', error);
if (error.code === 10062) {
console.log("Interaction expired");
return;
}
try { try {
const message = "An error occurred while processing your command."; if (!interaction.replied && !interaction.deferred) {
if (interaction.deferred) { await interaction.reply({
await interaction.editReply({ content: message }); content: 'An error occurred while processing your command.',
} else if (!interaction.replied) { ephemeral: true
await interaction.reply({ content: message, ephemeral: true }); });
} else if (interaction.deferred && !interaction.replied) {
await interaction.editReply('An error occurred while processing your command.');
} }
} catch (e) { } catch (e) {
console.error("Error while sending error message:", e); this.logger.error('Error sending error message:', e);
}
}
async safeReply(interaction, content, options = {}) {
try {
if (!interaction.deferred && !interaction.replied) {
await interaction.reply({ content, ephemeral: true, ...options });
} else if (interaction.deferred && !interaction.replied) {
await interaction.editReply({ content, ...options });
}
} catch (error) {
this.logger.error('Error in safeReply:', error);
} }
} }
async handleInteractionError(interaction, error) { async handleInteractionError(interaction, error) {
console.error("Interaction error:", error); this.logger.error('Interaction error:', error);
try { try {
if (!interaction.replied && !interaction.deferred) { if (!interaction.replied && !interaction.deferred) {
await interaction.reply({ await interaction.reply({
content: "An error occurred while processing your request.", content: 'An error occurred while processing your request.',
ephemeral: true, ephemeral: true
}); });
} else if (interaction.deferred) { } else if (interaction.deferred && !interaction.replied) {
await interaction.editReply({ await interaction.editReply({
content: "An error occurred while processing your request.", content: 'An error occurred while processing your request.'
}); });
} }
} catch (e) { } catch (e) {
console.error("Error while sending error message:", e); this.logger.error('Error sending error message:', e);
} }
} }
} }

View File

@@ -1,135 +1,94 @@
const { EmbedBuilder } = require('discord.js'); const { EmbedBuilder } = require('discord.js');
const Logger = require('../utils/Logger');
class SubscriptionCommands { class SubscriptionCommands {
constructor(supabase) { constructor(supabaseService) {
this.supabase = supabase; this.supabase = supabaseService.getClient();
this.logger = new Logger('SubscriptionCommands');
if (!this.supabase) {
throw new Error('Supabase client not initialized');
}
} }
async handleSubscribe(interaction) { async handleSubscribe(interaction) {
try { try {
const gameName = interaction.options.getString('game'); const gameName = this.sanitizeInput(interaction.options.getString('game'));
const channel = interaction.options.getChannel('channel'); const channel = interaction.options.getChannel('channel');
const guildId = interaction.guildId; const guildId = interaction.guildId;
const serverName = this.sanitizeInput(interaction.guild.name);
// First, get or create server record if (!gameName || gameName.length === 0) {
const { data: serverData, error: serverError } = await this.supabase await this.safeReply(interaction, 'Invalid game name provided.');
.from('servers')
.upsert({
discord_server_id: guildId,
server_name: interaction.guild.name
}, {
onConflict: 'discord_server_id',
returning: true
});
if (serverError) {
console.error('Server upsert error:', serverError);
await interaction.reply({ content: 'Failed to process server registration.', ephemeral: true });
return; return;
} }
// Get game ID this.logger.debug('Processing subscription request', {
const { data: gameData, error: gameError } = await this.supabase game: gameName,
.from('games') channel: channel.name,
.select('id') guildId,
.eq('name', gameName) serverName,
.eq('active', true) });
.single();
if (gameError || !gameData) { // Get or create the server
console.error('Game fetch error:', gameError); const serverId = await this.getOrCreateServer(guildId, serverName);
await interaction.reply({ content: `Game "${gameName}" not found or not active.`, ephemeral: true }); if (!serverId) {
await this.safeReply(interaction, 'Failed to register or retrieve server. Please try again later.');
return; return;
} }
// Create subscription // Fetch the game
const { error: prefError } = await this.supabase const gameData = await this.getGame(gameName);
.from('server_game_preferences') if (!gameData) {
.upsert({ await this.safeReply(interaction, `Game "${gameName}" not found or inactive.`);
server_id: serverData[0].id,
game_id: gameData.id,
notification_channel_id: channel.id
}, {
onConflict: '(server_id,game_id)',
returning: true
});
if (prefError) {
console.error('Preference upsert error:', prefError);
await interaction.reply({ content: 'Failed to save subscription.', ephemeral: true });
return; return;
} }
await interaction.reply({ // Create or update subscription
content: `Successfully subscribed to ${gameName} notifications in ${channel}.`, const subscriptionResult = await this.createOrUpdateSubscription(serverId, gameData.id, channel.id);
ephemeral: true if (!subscriptionResult) {
}); await this.safeReply(interaction, 'Failed to save subscription. Please try again later.');
return;
}
// Success response
this.logger.debug('Subscription created successfully', { gameName, channelId: channel.id, guildId: interaction.guildId });
await this.safeReply(interaction, `Successfully subscribed to ${gameName} notifications in ${channel}.`);
} catch (error) { } catch (error) {
console.error('Error subscribing:', error); this.logger.error('Error in handleSubscribe:', { error, stack: error.stack });
if (!interaction.replied && !interaction.deferred) { await this.safeReply(interaction, 'An unexpected error occurred. Please try again later.');
await interaction.reply({
content: 'An error occurred while processing your subscription.',
ephemeral: true
});
}
} }
} }
async handleUnsubscribe(interaction) { async handleUnsubscribe(interaction) {
try { try {
const gameName = interaction.options.getString('game'); const gameName = this.sanitizeInput(interaction.options.getString('game'));
const guildId = interaction.guildId; const guildId = interaction.guildId;
// Get server ID if (!gameName || gameName.length === 0) {
const { data: serverData, error: serverError } = await this.supabase await this.safeReply(interaction, 'Invalid game name provided.');
.from('servers')
.select('id')
.eq('discord_server_id', guildId)
.single();
if (serverError) {
await interaction.reply({ content: 'Server not found.', ephemeral: true });
return; return;
} }
// Get game ID this.logger.debug('Processing unsubscribe request', { game: gameName, guildId });
const { data: gameData, error: gameError } = await this.supabase
.from('games')
.select('id')
.eq('name', gameName)
.single();
if (gameError) { const subscription = await this.fetchSubscription(guildId, gameName);
await interaction.reply({ content: 'Game not found.', ephemeral: true }); if (!subscription) {
await this.safeReply(interaction, `No subscription found for ${gameName}.`);
return; return;
} }
// Delete subscription this.logger.debug('Subscription to delete', { subscription });
const { error: deleteError } = await this.supabase
.from('server_game_preferences')
.delete()
.eq('server_id', serverData.id)
.eq('game_id', gameData.id);
if (deleteError) { const success = await this.deleteSubscription(subscription);
await interaction.reply({ content: 'Failed to remove subscription.', ephemeral: true }); if (success) {
return; await this.safeReply(interaction, `Successfully unsubscribed from ${gameName} notifications.`);
} else {
await this.safeReply(interaction, `Failed to unsubscribe from ${gameName} notifications. Please try again later.`);
} }
await interaction.reply({
content: `Successfully unsubscribed from ${gameName} notifications.`,
ephemeral: true
});
} catch (error) { } catch (error) {
console.error('Error unsubscribing:', error); this.logger.error('Error in handleUnsubscribe:', { error, stack: error.stack });
if (!interaction.replied && !interaction.deferred) { await this.safeReply(interaction, 'An unexpected error occurred. Please try again later.');
await interaction.reply({
content: 'An error occurred while processing your unsubscription.',
ephemeral: true
});
}
} }
} }
@@ -137,53 +96,276 @@ class SubscriptionCommands {
try { try {
const guildId = interaction.guildId; const guildId = interaction.guildId;
// Get all subscriptions for this server this.logger.debug('Fetching subscriptions', { guildId });
const { data: subscriptions, error } = await this.supabase
const subscriptions = await this.getSubscriptions(guildId, interaction);
if (!subscriptions || subscriptions.length === 0) {
await interaction.editReply('This server has no game subscriptions.');
return;
}
const embed = this.buildSubscriptionsEmbed(subscriptions);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
this.handleError('handleListSubscriptions', error, interaction);
}
}
// Helper Methods
async getOrCreateServer(guildId, serverName) {
try {
const { data: existingServer, error: checkError } = await this.supabase
.from('servers')
.select('id')
.eq('discord_server_id', guildId)
.single();
if (checkError) {
this.logger.error('Error fetching server:', { checkError });
return null;
}
if (existingServer) return existingServer.id;
const { data: newServer, error: insertError } = await this.supabase
.from('servers')
.insert({
discord_server_id: guildId,
server_name: serverName,
active: true
})
.select()
.single();
if (insertError) {
this.logger.error('Error creating server:', { insertError });
return null;
}
return newServer.id;
} catch (error) {
this.logger.error('Error in getOrCreateServer:', { error, stack: error.stack });
return null;
}
}
async getGame(gameName) {
try {
const sanitizedGameName = this.sanitizeInput(gameName);
const { data: gameData, error: gameError } = await this.supabase
.from('games')
.select('id, name')
.eq('name', sanitizedGameName)
.eq('active', true)
.single();
if (gameError || !gameData) {
this.logger.error('Error fetching game data:', { gameError, gameName: sanitizedGameName });
return null;
}
return gameData;
} catch (error) {
this.logger.error('Error in getGame:', { error, stack: error.stack });
return null;
}
}
async createOrUpdateSubscription(serverId, gameId, channelId) {
try {
const { data: prefData, error: prefError } = await this.supabase
.from('server_game_preferences') .from('server_game_preferences')
.select(` .upsert(
games (name), { server_id: serverId, game_id: gameId, notification_channel_id: channelId },
notification_channel_id, { onConflict: 'server_id,game_id' }
servers!inner (discord_server_id) )
`) .select();
.eq('servers.discord_server_id', guildId);
if (prefError) {
this.logger.error('Error creating/updating subscription:', { prefError, serverId, gameId, channelId });
return null;
}
return prefData;
} catch (error) {
this.logger.error('Error in createOrUpdateSubscription:', { error, stack: error.stack });
return null;
}
}
async getGame(gameName, interaction) {
try {
const { data: gameData, error: gameError } = await this.supabase
.from('games')
.select('id, name')
.eq('name', gameName)
.eq('active', true)
.single();
if (gameError || !gameData) {
this.logger.error('Error fetching game data:', { gameError, gameName });
return null;
}
return gameData;
} catch (error) {
this.logger.error('Error in getGame:', { error, stack: error.stack });
return null;
}
}
async createOrUpdateSubscription(serverId, gameId, channelId, interaction) {
try {
const { data: prefData, error: prefError } = await this.supabase
.from('server_game_preferences')
.upsert(
{ server_id: serverId, game_id: gameId, notification_channel_id: channelId },
{ onConflict: 'server_id,game_id' }
)
.select();
if (prefError) {
this.logger.error('Error creating/updating subscription:', { prefError, serverId, gameId, channelId });
return null;
}
return prefData;
} catch (error) {
this.logger.error('Error in createOrUpdateSubscription:', { error, stack: error.stack });
return null;
}
}
async fetchSubscription(guildId, gameName) {
// Fetch the subscription data
const { data: subscription, error: subError } = await this.supabase
.from('active_subscriptions')
.select('*')
.eq('discord_server_id', guildId)
.eq('game_name', gameName)
.single();
if (subError || !subscription) {
this.logger.debug('No subscription found', { guildId, gameName });
return null;
}
// Fetch the server_id
const { data: serverData, error: serverError } = await this.supabase
.from('servers')
.select('id')
.eq('discord_server_id', guildId)
.single();
if (serverError) {
this.logger.error('Error fetching server_id', { serverError });
return null;
}
// Fetch the game_id
const { data: gameData, error: gameError } = await this.supabase
.from('games')
.select('id')
.eq('name', gameName)
.single();
if (gameError) {
this.logger.error('Error fetching game_id', { gameError });
return null;
}
this.logger.debug('Subscription fetched', { subscription, serverData, gameData });
return {
server_id: serverData.id,
game_id: gameData.id,
discord_server_id: subscription.discord_server_id,
game_name: subscription.game_name,
notification_channel_id: subscription.notification_channel_id
};
}
async deleteSubscription(subscription) {
if (!subscription || !subscription.server_id || !subscription.game_id) {
this.logger.error('Invalid subscription data', {
subscription,
hasServerId: !!subscription?.server_id,
hasGameId: !!subscription?.game_id
});
return false;
}
const { error: deleteError } = await this.supabase
.from('server_game_preferences')
.delete()
.match({
server_id: subscription.server_id,
game_id: subscription.game_id
});
if (deleteError) {
this.logger.error('Delete error', { deleteError, subscription });
return false;
}
this.logger.debug('Subscription deleted', { subscription });
return true;
}
async getSubscriptions(guildId, interaction) {
const { data: subscriptions, error } = await this.supabase
.from('active_subscriptions')
.select('*')
.eq('discord_server_id', guildId);
if (error) { if (error) {
await interaction.reply({ content: 'Failed to fetch subscriptions.', ephemeral: true }); this.logger.error('Error fetching subscriptions', { error });
return; await interaction.editReply('Failed to fetch subscriptions. Please try again later.');
return null;
} }
if (!subscriptions || subscriptions.length === 0) { return subscriptions;
await interaction.reply({
content: 'This server has no game subscriptions.',
ephemeral: true
});
return;
} }
const embed = new EmbedBuilder() buildSubscriptionsEmbed(subscriptions) {
return new EmbedBuilder()
.setColor('#0099ff')
.setTitle('Game Subscriptions') .setTitle('Game Subscriptions')
.setDescription('Current game notification subscriptions for this server:') .setDescription('Current game notification subscriptions for this server:')
.setColor('#0099ff'); .addFields(
subscriptions.map(sub => ({
subscriptions.forEach(sub => { name: sub.game_name,
embed.addFields({
name: sub.games.name,
value: `<#${sub.notification_channel_id}>`, value: `<#${sub.notification_channel_id}>`,
inline: true inline: true
}); }))
}); )
.setTimestamp()
.setFooter({ text: 'VRBattles Match System' });
}
await interaction.reply({ embeds: [embed], ephemeral: true }); async safeReply(interaction, content, options = {}) {
try {
if (!interaction.deferred && !interaction.replied) {
await interaction.deferReply({ ephemeral: true });
}
await interaction.editReply({ content, ...options });
} catch (error) { } catch (error) {
console.error('Error listing subscriptions:', error); this.logger.error('Error in safeReply:', { error, stack: error.stack });
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: 'An error occurred while fetching subscriptions.',
ephemeral: true
});
} }
} }
sanitizeInput(input) {
if (typeof input !== 'string') {
return '';
}
// Remove any characters that aren't alphanumeric, spaces, or hyphens
return input.replace(/[^a-zA-Z0-9 -]/g, '').trim();
}
handleError(method, error, interaction) {
this.logger.error(`Error in ${method}`, { error, stack: error.stack });
interaction.editReply('An unexpected error occurred. Please try again later.');
} }
} }

View File

@@ -0,0 +1,52 @@
const { Collection } = require('discord.js');
const Logger = require('./Logger');
class CooldownManager {
constructor() {
this.cooldowns = new Collection();
this.cooldownTimes = {
subscribe: 5,
unsubscribe: 5,
list_subscriptions: 5,
finduser: 3,
matchhistory: 3,
ping: 1
};
this.logger = new Logger('CooldownManager');
}
checkCooldown(interaction) {
if (!this.cooldowns.has(interaction.commandName)) {
this.cooldowns.set(interaction.commandName, new Collection());
}
const now = Date.now();
const timestamps = this.cooldowns.get(interaction.commandName);
const cooldownAmount = (this.cooldownTimes[interaction.commandName] || 3) * 1000;
const userKey = `${interaction.user.id}-${interaction.guildId}`;
if (timestamps.has(userKey)) {
const expirationTime = timestamps.get(userKey) + cooldownAmount;
if (now < expirationTime) {
const timeLeft = (expirationTime - now) / 1000;
this.logger.debug('Command on cooldown', {
command: interaction.commandName,
user: interaction.user.tag,
timeLeft
});
return timeLeft;
}
}
timestamps.set(userKey, now);
setTimeout(() => timestamps.delete(userKey), cooldownAmount);
return 0;
}
setCooldown(command, seconds) {
this.cooldownTimes[command] = seconds;
}
}
module.exports = CooldownManager;

36
src/utils/Logger.js Normal file
View File

@@ -0,0 +1,36 @@
class Logger {
constructor(context) {
this.context = context;
}
formatMessage(level, message, data = null) {
const timestamp = new Date().toISOString();
const context = this.context ? `[${this.context}]` : '';
if (data) {
return `[${timestamp}] ${level}${context}: ${message} ${JSON.stringify(data, null, 2)}`;
}
return `[${timestamp}] ${level}${context}: ${message}`;
}
debug(message, data = null) {
console.log(this.formatMessage('DEBUG', message, data));
}
info(message, data = null) {
console.log(this.formatMessage('INFO', message, data));
}
warn(message, data = null) {
console.warn(this.formatMessage('WARN', message, data));
}
error(message, error = null) {
console.error(this.formatMessage('ERROR', message, {
message: error?.message,
stack: error?.stack,
...error
}));
}
}
module.exports = Logger;