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,204 +5,261 @@ 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.client = new Client({ this.logger = new Logger('Bot');
intents: [ this.cooldownManager = new CooldownManager();
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages, this.client = new Client({
GatewayIntentBits.MessageContent, intents: [
], GatewayIntentBits.Guilds,
}); GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
this.playerService = new PlayerService(); ],
this.commandHandler = new CommandHandler(this.playerService); });
this.matchCommands = new MatchCommands(this.playerService);
this.notificationService = new NotificationService(this); this.logger.info('Initializing bot...');
this.initializeServices();
this.setupEventHandlers();
}
initializeServices() {
this.logger.debug('Initializing services...');
this.playerService = new PlayerService();
this.commandHandler = new CommandHandler(this.playerService);
this.matchCommands = new MatchCommands(this.playerService);
this.notificationService = new NotificationService(this);
this.initializeSupabase();
}
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() {
this.logger.debug('Setting up event handlers...');
this.client.on('error', this.handleError.bind(this));
this.client.once('ready', this.handleReady.bind(this));
this.client.on('interactionCreate', this.handleInteraction.bind(this));
}
async start(token) {
this.logger.info('Starting bot...');
try {
await this.client.login(token);
this.logger.info('Login successful');
const port = process.env.NOTIFICATION_PORT || 3000;
await this.notificationService.start(port);
this.logger.info(`Notification service started on port ${port}`);
} catch (error) {
this.logger.error('Startup failed:', error);
throw error;
}
}
async stop() {
this.logger.info('Stopping bot...');
try {
await this.notificationService.stop();
await this.client.destroy();
this.logger.info('Bot stopped successfully');
} catch (error) {
this.logger.error('Error stopping bot:', error);
throw error;
}
}
handleError(error) {
this.logger.error('Discord client error:', error);
}
handleReady() {
this.logger.info(`Logged in as ${this.client.user.tag}`);
}
handleInteraction(interaction) {
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()) {
this.handleCommand(interaction)
.catch(error => this.handleInteractionError(interaction, error));
} else if (interaction.isButton()) {
this.handleButton(interaction)
.catch(error => this.handleInteractionError(interaction, error));
}
}
async handleCommand(interaction) {
try {
// Check cooldown
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) {
case "ping":
await this.commandHandler.handlePing(interaction);
break;
case "finduser":
await this.commandHandler.handleFindUser(interaction);
break;
case "matchhistory":
await this.commandHandler.handleMatchHistory(interaction);
break;
case "subscribe":
case "unsubscribe":
case "list_subscriptions":
await this.handleSubscriptionCommand(interaction);
break;
default:
await interaction.reply({
content: "Unknown command",
ephemeral: true
});
}
}
async handleSubscriptionCommand(interaction) {
if (!this.subscriptionCommands) {
await this.safeReply(interaction, "Subscription commands are not available.");
return;
}
this.supabaseService = new SupabaseService(); try {
if (this.supabaseService.supabase) { // Defer the reply immediately
this.subscriptionCommands = new SubscriptionCommands(this.supabaseService); await this.safeReply(interaction, "Processing your request...");
} else {
console.warn('SupabaseService not initialized. Subscription commands will be unavailable.'); switch (interaction.commandName) {
this.subscriptionCommands = null; case "subscribe":
}
this.setupEventHandlers();
}
setupEventHandlers() {
this.client.on('error', this.handleError.bind(this));
this.client.once('ready', this.handleReady.bind(this));
this.client.on('interactionCreate', this.handleInteraction.bind(this));
}
// In the start method, add Supabase connection test:
async start(token) {
console.log("Starting bot...");
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);
console.log("Login successful");
// Start notification service after bot is logged in
const port = process.env.NOTIFICATION_PORT || 3000;
await this.notificationService.start(port);
console.log(`Notification service started on port ${port}`);
} catch (error) {
console.error("Startup failed:", error);
throw error;
}
}
async stop() {
console.log("Stopping bot...");
try {
await this.notificationService.stop();
await this.client.destroy();
console.log("Bot stopped successfully");
} catch (error) {
console.error("Error stopping bot:", error);
throw error;
}
}
handleError(error) {
console.error("Discord client error:", error);
}
handleReady() {
console.log(`Logged in as ${this.client.user.tag}!`);
}
async handleInteraction(interaction) {
try {
if (interaction.isCommand()) {
await this.handleCommand(interaction);
} else if (interaction.isButton()) {
await this.handleButton(interaction);
}
} catch (error) {
console.error("Error handling interaction:", error);
await this.handleInteractionError(interaction, error);
}
}
async handleCommand(interaction) {
try {
// Defer the reply immediately for all commands
await interaction.deferReply({ ephemeral: true });
switch (interaction.commandName) {
case "ping":
await this.commandHandler.handlePing(interaction);
break;
case "finduser":
await this.commandHandler.handleFindUser(interaction);
break;
case "matchhistory":
await this.commandHandler.handleMatchHistory(interaction);
break;
case "subscribe":
if (this.subscriptionCommands) {
await this.subscriptionCommands.handleSubscribe(interaction); await this.subscriptionCommands.handleSubscribe(interaction);
} else { break;
await interaction.editReply("Subscription commands are not available."); case "unsubscribe":
}
break;
case "unsubscribe":
if (this.subscriptionCommands) {
await this.subscriptionCommands.handleUnsubscribe(interaction); await this.subscriptionCommands.handleUnsubscribe(interaction);
} else { break;
await interaction.editReply("Subscription commands are not available."); case "list_subscriptions":
}
break;
case "listsubscriptions":
if (this.subscriptionCommands) {
await this.subscriptionCommands.handleListSubscriptions(interaction); await this.subscriptionCommands.handleListSubscriptions(interaction);
} else { break;
await interaction.editReply("Subscription commands are not available."); }
} } catch (error) {
break; this.logger.error('Subscription command error:', error);
default: await this.safeReply(interaction, 'An error occurred while processing your request.');
await interaction.editReply("Unknown command");
} }
} catch (error) {
console.error('Command error:', error);
await this.handleCommandError(interaction, error);
} }
} async handleButton(interaction) {
const [action, matchId] = interaction.customId.split("_match_");
async handleButton(interaction) { try {
const [action, matchId] = interaction.customId.split("_match_"); this.logger.debug('Processing button interaction', {
action,
matchId,
user: interaction.user.tag
});
try { switch (action) {
switch (action) { case "accept":
case "accept": await this.matchCommands.handleMatchAccept(interaction, matchId);
await this.matchCommands.handleMatchAccept(interaction, matchId); break;
break; case "view":
case "view": 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) {
this.logger.error("Error handling button:", error);
await this.handleInteractionError(interaction, error);
}
}
async handleCommandError(interaction, error) {
this.logger.error('Command error:', error);
try {
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: 'An error occurred while processing your command.',
ephemeral: true
});
} else if (interaction.deferred && !interaction.replied) {
await interaction.editReply('An error occurred while processing your command.');
}
} catch (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);
} }
} catch (error) {
console.error("Error handling button:", error);
await this.handleInteractionError(interaction, error);
}
}
async handleCommandError(interaction, error) {
console.error("Command error:", error);
if (error.code === 10062) {
console.log("Interaction expired");
return;
} }
try { async handleInteractionError(interaction, error) {
const message = "An error occurred while processing your command."; this.logger.error('Interaction error:', error);
if (interaction.deferred) { try {
await interaction.editReply({ content: message }); if (!interaction.replied && !interaction.deferred) {
} else if (!interaction.replied) { await interaction.reply({
await interaction.reply({ content: message, ephemeral: true }); content: 'An error occurred while processing your request.',
} ephemeral: true
} catch (e) { });
console.error("Error while sending error message:", e); } else if (interaction.deferred && !interaction.replied) {
await interaction.editReply({
content: 'An error occurred while processing your request.'
});
}
} catch (e) {
this.logger.error('Error sending error message:', e);
}
} }
}
async handleInteractionError(interaction, error) {
console.error("Interaction error:", error);
try {
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: "An error occurred while processing your request.",
ephemeral: true,
});
} else if (interaction.deferred) {
await interaction.editReply({
content: "An error occurred while processing your request.",
});
}
} catch (e) {
console.error("Error while sending error message:", e);
}
}
} }
module.exports = Bot; module.exports = Bot;

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
const { data: serverData, error: serverError } = await this.supabase if (!gameName || gameName.length === 0) {
.from('servers') await this.safeReply(interaction, 'Invalid game name provided.');
.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) {
console.error('Game fetch error:', gameError);
await interaction.reply({ content: `Game "${gameName}" not found or not active.`, ephemeral: true });
return;
}
// Create subscription
const { error: prefError } = await this.supabase
.from('server_game_preferences')
.upsert({
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;
}
await interaction.reply({
content: `Successfully subscribed to ${gameName} notifications in ${channel}.`,
ephemeral: true
}); });
} catch (error) { // Get or create the server
console.error('Error subscribing:', error); const serverId = await this.getOrCreateServer(guildId, serverName);
if (!interaction.replied && !interaction.deferred) { if (!serverId) {
await interaction.reply({ await this.safeReply(interaction, 'Failed to register or retrieve server. Please try again later.');
content: 'An error occurred while processing your subscription.', return;
ephemeral: true
});
} }
// Fetch the game
const gameData = await this.getGame(gameName);
if (!gameData) {
await this.safeReply(interaction, `Game "${gameName}" not found or inactive.`);
return;
}
// Create or update subscription
const subscriptionResult = await this.createOrUpdateSubscription(serverId, gameData.id, channel.id);
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) {
this.logger.error('Error in handleSubscribe:', { error, stack: error.stack });
await this.safeReply(interaction, 'An unexpected error occurred. Please try again later.');
} }
} }
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') const subscription = await this.fetchSubscription(guildId, gameName);
.select('id') if (!subscription) {
.eq('name', gameName) await this.safeReply(interaction, `No subscription found for ${gameName}.`);
.single();
if (gameError) {
await interaction.reply({ content: 'Game not found.', ephemeral: true });
return; return;
} }
// Delete subscription this.logger.debug('Subscription to delete', { subscription });
const { error: deleteError } = await this.supabase
.from('server_game_preferences') const success = await this.deleteSubscription(subscription);
.delete() if (success) {
.eq('server_id', serverData.id) await this.safeReply(interaction, `Successfully unsubscribed from ${gameName} notifications.`);
.eq('game_id', gameData.id); } else {
await this.safeReply(interaction, `Failed to unsubscribe from ${gameName} notifications. Please try again later.`);
if (deleteError) {
await interaction.reply({ content: 'Failed to remove subscription.', ephemeral: true });
return;
} }
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,54 +96,277 @@ 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
.from('server_game_preferences')
.select(`
games (name),
notification_channel_id,
servers!inner (discord_server_id)
`)
.eq('servers.discord_server_id', guildId);
if (error) {
await interaction.reply({ content: 'Failed to fetch subscriptions.', ephemeral: true });
return;
}
const subscriptions = await this.getSubscriptions(guildId, interaction);
if (!subscriptions || subscriptions.length === 0) { if (!subscriptions || subscriptions.length === 0) {
await interaction.reply({ await interaction.editReply('This server has no game subscriptions.');
content: 'This server has no game subscriptions.',
ephemeral: true
});
return; return;
} }
const embed = new EmbedBuilder() const embed = this.buildSubscriptionsEmbed(subscriptions);
.setTitle('Game Subscriptions') await interaction.editReply({ embeds: [embed] });
.setDescription('Current game notification subscriptions for this server:') } catch (error) {
.setColor('#0099ff'); this.handleError('handleListSubscriptions', error, interaction);
}
}
subscriptions.forEach(sub => { // Helper Methods
embed.addFields({
name: sub.games.name, 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')
.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 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) {
this.logger.error('Error fetching subscriptions', { error });
await interaction.editReply('Failed to fetch subscriptions. Please try again later.');
return null;
}
return subscriptions;
}
buildSubscriptionsEmbed(subscriptions) {
return new EmbedBuilder()
.setColor('#0099ff')
.setTitle('Game Subscriptions')
.setDescription('Current game notification subscriptions for this server:')
.addFields(
subscriptions.map(sub => ({
name: sub.game_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 {
} catch (error) { if (!interaction.deferred && !interaction.replied) {
console.error('Error listing subscriptions:', error); await interaction.deferReply({ ephemeral: true });
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: 'An error occurred while fetching subscriptions.',
ephemeral: true
});
} }
await interaction.editReply({ content, ...options });
} catch (error) {
this.logger.error('Error in safeReply:', { error, stack: error.stack });
} }
} }
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.');
}
} }
module.exports = SubscriptionCommands; module.exports = SubscriptionCommands;

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;