Notification subscriptions
This commit is contained in:
219
src/Bot.js
219
src/Bot.js
@@ -5,9 +5,14 @@ const NotificationService = require("./services/NotificationService.js");
|
||||
const MatchCommands = require("./commands/MatchCommands.js");
|
||||
const SupabaseService = require("./services/SupabaseService.js");
|
||||
const SubscriptionCommands = require("./commands/SubscriptionCommands.js");
|
||||
const CooldownManager = require("./utils/CooldownManager.js");
|
||||
const Logger = require("./utils/Logger.js");
|
||||
|
||||
class Bot {
|
||||
constructor() {
|
||||
this.logger = new Logger('Bot');
|
||||
this.cooldownManager = new CooldownManager();
|
||||
|
||||
this.client = new Client({
|
||||
intents: [
|
||||
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.commandHandler = new CommandHandler(this.playerService);
|
||||
this.matchCommands = new MatchCommands(this.playerService);
|
||||
this.notificationService = new NotificationService(this);
|
||||
|
||||
this.supabaseService = new SupabaseService();
|
||||
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.initializeSupabase();
|
||||
}
|
||||
|
||||
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() {
|
||||
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));
|
||||
}
|
||||
|
||||
// In the start method, add Supabase connection test:
|
||||
async start(token) {
|
||||
console.log("Starting bot...");
|
||||
this.logger.info('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");
|
||||
this.logger.info('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}`);
|
||||
this.logger.info(`Notification service started on port ${port}`);
|
||||
} catch (error) {
|
||||
console.error("Startup failed:", error);
|
||||
this.logger.error('Startup failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
console.log("Stopping bot...");
|
||||
this.logger.info('Stopping bot...');
|
||||
try {
|
||||
await this.notificationService.stop();
|
||||
await this.client.destroy();
|
||||
console.log("Bot stopped successfully");
|
||||
this.logger.info('Bot stopped successfully');
|
||||
} catch (error) {
|
||||
console.error("Error stopping bot:", error);
|
||||
this.logger.error('Error stopping bot:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
handleError(error) {
|
||||
console.error("Discord client error:", error);
|
||||
this.logger.error('Discord client error:', error);
|
||||
}
|
||||
|
||||
handleReady() {
|
||||
console.log(`Logged in as ${this.client.user.tag}!`);
|
||||
this.logger.info(`Logged in as ${this.client.user.tag}`);
|
||||
}
|
||||
|
||||
async handleInteraction(interaction) {
|
||||
try {
|
||||
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()) {
|
||||
await this.handleCommand(interaction);
|
||||
this.handleCommand(interaction)
|
||||
.catch(error => this.handleInteractionError(interaction, error));
|
||||
} else if (interaction.isButton()) {
|
||||
await this.handleButton(interaction);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling interaction:", error);
|
||||
await this.handleInteractionError(interaction, error);
|
||||
this.handleButton(interaction)
|
||||
.catch(error => this.handleInteractionError(interaction, error));
|
||||
}
|
||||
}
|
||||
|
||||
async handleCommand(interaction) {
|
||||
try {
|
||||
// Defer the reply immediately for all commands
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
// 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);
|
||||
@@ -113,39 +147,54 @@ async start(token) {
|
||||
await this.commandHandler.handleMatchHistory(interaction);
|
||||
break;
|
||||
case "subscribe":
|
||||
if (this.subscriptionCommands) {
|
||||
await this.subscriptionCommands.handleSubscribe(interaction);
|
||||
} else {
|
||||
await interaction.editReply("Subscription commands are not available.");
|
||||
}
|
||||
break;
|
||||
case "unsubscribe":
|
||||
if (this.subscriptionCommands) {
|
||||
await this.subscriptionCommands.handleUnsubscribe(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.");
|
||||
}
|
||||
case "list_subscriptions":
|
||||
await this.handleSubscriptionCommand(interaction);
|
||||
break;
|
||||
default:
|
||||
await interaction.editReply("Unknown command");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Command error:', error);
|
||||
await this.handleCommandError(interaction, error);
|
||||
await interaction.reply({
|
||||
content: "Unknown command",
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const [action, matchId] = interaction.customId.split("_match_");
|
||||
|
||||
try {
|
||||
this.logger.debug('Processing button interaction', {
|
||||
action,
|
||||
matchId,
|
||||
user: interaction.user.tag
|
||||
});
|
||||
|
||||
switch (action) {
|
||||
case "accept":
|
||||
await this.matchCommands.handleMatchAccept(interaction, matchId);
|
||||
@@ -154,53 +203,61 @@ async start(token) {
|
||||
await this.matchCommands.handleViewDetails(interaction, matchId);
|
||||
break;
|
||||
default:
|
||||
this.logger.warn('Unknown button action', { action });
|
||||
await interaction.reply({
|
||||
content: "Unknown button action",
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling button:", error);
|
||||
this.logger.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;
|
||||
}
|
||||
|
||||
this.logger.error('Command error:', error);
|
||||
try {
|
||||
const message = "An error occurred while processing your command.";
|
||||
if (interaction.deferred) {
|
||||
await interaction.editReply({ content: message });
|
||||
} else if (!interaction.replied) {
|
||||
await interaction.reply({ content: message, ephemeral: true });
|
||||
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) {
|
||||
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) {
|
||||
console.error("Interaction error:", error);
|
||||
|
||||
this.logger.error('Interaction error:', error);
|
||||
try {
|
||||
if (!interaction.replied && !interaction.deferred) {
|
||||
await interaction.reply({
|
||||
content: "An error occurred while processing your request.",
|
||||
ephemeral: true,
|
||||
content: 'An error occurred while processing your request.',
|
||||
ephemeral: true
|
||||
});
|
||||
} else if (interaction.deferred) {
|
||||
} else if (interaction.deferred && !interaction.replied) {
|
||||
await interaction.editReply({
|
||||
content: "An error occurred while processing your request.",
|
||||
content: 'An error occurred while processing your request.'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error while sending error message:", e);
|
||||
this.logger.error('Error sending error message:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,135 +1,94 @@
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const Logger = require('../utils/Logger');
|
||||
|
||||
class SubscriptionCommands {
|
||||
constructor(supabase) {
|
||||
this.supabase = supabase;
|
||||
constructor(supabaseService) {
|
||||
this.supabase = supabaseService.getClient();
|
||||
this.logger = new Logger('SubscriptionCommands');
|
||||
|
||||
if (!this.supabase) {
|
||||
throw new Error('Supabase client not initialized');
|
||||
}
|
||||
}
|
||||
|
||||
async handleSubscribe(interaction) {
|
||||
try {
|
||||
const gameName = interaction.options.getString('game');
|
||||
const gameName = this.sanitizeInput(interaction.options.getString('game'));
|
||||
const channel = interaction.options.getChannel('channel');
|
||||
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
|
||||
.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 });
|
||||
if (!gameName || gameName.length === 0) {
|
||||
await this.safeReply(interaction, 'Invalid game name provided.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get game ID
|
||||
const { data: gameData, error: gameError } = await this.supabase
|
||||
.from('games')
|
||||
.select('id')
|
||||
.eq('name', gameName)
|
||||
.eq('active', true)
|
||||
.single();
|
||||
this.logger.debug('Processing subscription request', {
|
||||
game: gameName,
|
||||
channel: channel.name,
|
||||
guildId,
|
||||
serverName,
|
||||
});
|
||||
|
||||
if (gameError || !gameData) {
|
||||
console.error('Game fetch error:', gameError);
|
||||
await interaction.reply({ content: `Game "${gameName}" not found or not active.`, ephemeral: true });
|
||||
// Get or create the server
|
||||
const serverId = await this.getOrCreateServer(guildId, serverName);
|
||||
if (!serverId) {
|
||||
await this.safeReply(interaction, 'Failed to register or retrieve server. Please try again later.');
|
||||
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 });
|
||||
// Fetch the game
|
||||
const gameData = await this.getGame(gameName);
|
||||
if (!gameData) {
|
||||
await this.safeReply(interaction, `Game "${gameName}" not found or inactive.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.reply({
|
||||
content: `Successfully subscribed to ${gameName} notifications in ${channel}.`,
|
||||
ephemeral: true
|
||||
});
|
||||
// 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) {
|
||||
console.error('Error subscribing:', error);
|
||||
if (!interaction.replied && !interaction.deferred) {
|
||||
await interaction.reply({
|
||||
content: 'An error occurred while processing your subscription.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
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) {
|
||||
try {
|
||||
const gameName = interaction.options.getString('game');
|
||||
const gameName = this.sanitizeInput(interaction.options.getString('game'));
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
// Get server ID
|
||||
const { data: serverData, error: serverError } = await this.supabase
|
||||
.from('servers')
|
||||
.select('id')
|
||||
.eq('discord_server_id', guildId)
|
||||
.single();
|
||||
|
||||
if (serverError) {
|
||||
await interaction.reply({ content: 'Server not found.', ephemeral: true });
|
||||
if (!gameName || gameName.length === 0) {
|
||||
await this.safeReply(interaction, 'Invalid game name provided.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get game ID
|
||||
const { data: gameData, error: gameError } = await this.supabase
|
||||
.from('games')
|
||||
.select('id')
|
||||
.eq('name', gameName)
|
||||
.single();
|
||||
this.logger.debug('Processing unsubscribe request', { game: gameName, guildId });
|
||||
|
||||
if (gameError) {
|
||||
await interaction.reply({ content: 'Game not found.', ephemeral: true });
|
||||
const subscription = await this.fetchSubscription(guildId, gameName);
|
||||
if (!subscription) {
|
||||
await this.safeReply(interaction, `No subscription found for ${gameName}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete subscription
|
||||
const { error: deleteError } = await this.supabase
|
||||
.from('server_game_preferences')
|
||||
.delete()
|
||||
.eq('server_id', serverData.id)
|
||||
.eq('game_id', gameData.id);
|
||||
this.logger.debug('Subscription to delete', { subscription });
|
||||
|
||||
if (deleteError) {
|
||||
await interaction.reply({ content: 'Failed to remove subscription.', ephemeral: true });
|
||||
return;
|
||||
const success = await this.deleteSubscription(subscription);
|
||||
if (success) {
|
||||
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) {
|
||||
console.error('Error unsubscribing:', error);
|
||||
if (!interaction.replied && !interaction.deferred) {
|
||||
await interaction.reply({
|
||||
content: 'An error occurred while processing your unsubscription.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
this.logger.error('Error in handleUnsubscribe:', { error, stack: error.stack });
|
||||
await this.safeReply(interaction, 'An unexpected error occurred. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,53 +96,276 @@ class SubscriptionCommands {
|
||||
try {
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
// Get all subscriptions for this server
|
||||
const { data: subscriptions, error } = await this.supabase
|
||||
this.logger.debug('Fetching subscriptions', { guildId });
|
||||
|
||||
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')
|
||||
.select(`
|
||||
games (name),
|
||||
notification_channel_id,
|
||||
servers!inner (discord_server_id)
|
||||
`)
|
||||
.eq('servers.discord_server_id', guildId);
|
||||
.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) {
|
||||
await interaction.reply({ content: 'Failed to fetch subscriptions.', ephemeral: true });
|
||||
return;
|
||||
this.logger.error('Error fetching subscriptions', { error });
|
||||
await interaction.editReply('Failed to fetch subscriptions. Please try again later.');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!subscriptions || subscriptions.length === 0) {
|
||||
await interaction.reply({
|
||||
content: 'This server has no game subscriptions.',
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
buildSubscriptionsEmbed(subscriptions) {
|
||||
return new EmbedBuilder()
|
||||
.setColor('#0099ff')
|
||||
.setTitle('Game Subscriptions')
|
||||
.setDescription('Current game notification subscriptions for this server:')
|
||||
.setColor('#0099ff');
|
||||
|
||||
subscriptions.forEach(sub => {
|
||||
embed.addFields({
|
||||
name: sub.games.name,
|
||||
.addFields(
|
||||
subscriptions.map(sub => ({
|
||||
name: sub.game_name,
|
||||
value: `<#${sub.notification_channel_id}>`,
|
||||
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) {
|
||||
console.error('Error listing subscriptions:', error);
|
||||
if (!interaction.replied && !interaction.deferred) {
|
||||
await interaction.reply({
|
||||
content: 'An error occurred while fetching subscriptions.',
|
||||
ephemeral: true
|
||||
});
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
52
src/utils/CooldownManager.js
Normal file
52
src/utils/CooldownManager.js
Normal 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
36
src/utils/Logger.js
Normal 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;
|
||||
Reference in New Issue
Block a user