diff --git a/.dockerignore b/.dockerignore index 5171c54..3ec8249 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,16 @@ node_modules -npm-debug.log \ No newline at end of file +npm-debug.log +.env* +.git +.gitignore +README.md +Dockerfile +.dockerignore +*.md +.vscode +.idea +coverage +.nyc_output +.cache +logs +*.log \ No newline at end of file diff --git a/.env.example b/.env.example deleted file mode 100644 index ade87cc..0000000 --- a/.env.example +++ /dev/null @@ -1,16 +0,0 @@ -# Discord Bot Configuration -DISCORD_TOKEN=your_dev_bot_token -CLIENT_ID=your_dev_client_id - -# Supabase Configuration -SUPABASE_URL=your_dev_supabase_url -SUPABASE_KEY=your_dev_supabase_key - -# Webhook Configuration (for match notifications) -WEBHOOK_SECRET=your_webhook_secret - -# Channel Configuration -NOTIFICATION_CHANNEL_ID=your_notification_channel_id - -# Environment -NODE_ENV=development \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..4897472 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,149 @@ +# ๐Ÿš€ VRBattles Discord Bot - Deployment Guide + +## ๐Ÿ“ฆ **Latest Version: v1.2.7** + +### **What's New in v1.2.7:** +- โœ… Fixed Docker multi-platform build (x86_64 + ARM64) +- โœ… Resolved "exec format error" on Coolify deployment +- โœ… Enhanced deployment script with buildx support + +### **Previous in v1.2.6:** +- โœ… Fixed autocomplete interaction spam +- โœ… All 9 games now available in dropdowns +- โœ… Working help button navigation +- โœ… Automatic game sync from Supabase +- โœ… Optimized Docker image (355MB) +- โœ… Enhanced error handling + +### **Docker Images Available:** +``` +far54/vrbattles-discord-bot:latest (always current) +far54/vrbattles-discord-bot:v1.2.7 (specific version) +``` + +### **Multi-Platform Support:** +Images are built for both Intel/AMD (x86_64) and ARM64 architectures to ensure compatibility across different server environments. If you encounter "exec format error", the image architecture doesn't match your server. + +## ๐Ÿ”ง **Deploy to Coolify** + +### **Option 1: Using Docker Hub Image** +1. **Create New Application** in Coolify +2. **Source Type**: Public Repository +3. **Docker Image**: `far54/vrbattles-discord-bot:latest` +4. **Port**: `3000` + +### **Option 2: From GitHub (Auto-deploy)** +1. **Source Type**: Git Repository +2. **Repository**: Your GitHub repo URL +3. **Branch**: `main` +4. **Build Pack**: Docker +5. **Port**: `3000` + +## ๐Ÿ” **Required Environment Variables** + +Set these in Coolify Environment tab: + +```bash +# Discord Configuration +DISCORD_TOKEN=your_discord_bot_token +DISCORD_APPLICATION_ID=your_discord_application_id + +# Supabase Configuration +SUPABASE_URL=your_supabase_project_url +SUPABASE_KEY=your_supabase_anon_key + +# Environment +NODE_ENV=production +``` + +## ๐ŸŽฏ **Features Included** + +### **Commands Available:** +- `/help` - Interactive help system with working buttons +- `/finduser` - Search players (9 game dropdown) +- `/findteam` - Search teams (9 game dropdown) +- `/matchhistory` - View match history (optional game filter) +- `/subscribe` - Admin: Subscribe to game notifications +- `/unsubscribe` - Admin: Remove game subscriptions +- `/list_subscriptions` - Admin: View active subscriptions +- `/register_server` - Admin: Register server with BattleBot +- `/version` - Show bot version and system info + +### **Supported Games:** +- Big Ballers VR +- Blacktop Hoops +- Breachers +- Echo Arena +- Echo Combat +- Gun Raiders +- Nock +- Orion Drift +- VAIL + +### **Services:** +- **Discord Bot**: Main command handling +- **Webhook Server**: Port 3000 for match notifications +- **Auto Sync**: Game choices sync with Supabase + +## ๐Ÿ”„ **Updating Games** + +When you add/remove games in Supabase: + +```bash +# Locally run: +npm run sync-games + +# Then rebuild and push (multi-platform): +./scripts/deploy.sh v1.2.7 + +# Update Coolify deployment +``` + +## ๐Ÿ—๏ธ **Building for Production** + +### **Using Deploy Script (Recommended):** +```bash +# This builds for both Intel/AMD and ARM64 +./scripts/deploy.sh v1.2.7 +``` + +### **Manual Multi-Platform Build:** +```bash +# Setup buildx (one-time) +docker buildx create --name multiarch --use --driver docker-container --bootstrap + +# Build and push for both architectures +docker buildx build --platform linux/amd64,linux/arm64 \ + -t far54/vrbattles-discord-bot:v1.2.7 \ + . --push +``` + +## ๐Ÿ“Š **Health Check** + +The Docker container includes a health check that validates: +- Node.js process is running +- Port 3000 is accessible +- Basic application startup + +## ๐Ÿ” **Troubleshooting** + +### **Common Issues:** +1. **Bot not responding**: Check DISCORD_TOKEN is valid +2. **Database errors**: Verify SUPABASE_URL and SUPABASE_KEY +3. **Game dropdowns empty**: Run `npm run sync-games` and redeploy +4. **Help buttons not working**: Ensure latest v1.2.7+ is deployed +5. **exec format error**: Architecture mismatch - use multi-platform images + +### **Log Locations:** +- Coolify: Check application logs tab +- Discord: Bot activity in server +- Health: `/version` command shows system info + +## ๐ŸŽ‰ **Deployment Complete!** + +Your bot should now be running with: +- โœ… All 9 games in dropdowns +- โœ… Working help navigation +- โœ… No debug message spam +- โœ… Match notifications on port 3000 +- โœ… Clean, optimized Docker image \ No newline at end of file diff --git a/deploy-commands.js b/deploy-commands.js index 09343c0..12e3af0 100644 --- a/deploy-commands.js +++ b/deploy-commands.js @@ -2,25 +2,29 @@ const { REST, Routes, SlashCommandBuilder, PermissionFlagsBits } = require("disc require("dotenv").config(); const commands = [ - new SlashCommandBuilder() - .setName("ping") - .setDescription("Replies with Pong!"), new SlashCommandBuilder() .setName("help") .setDescription("Get help with BattleBot commands and features") .addStringOption((option) => option .setName("category") - .setDescription("Choose a help category") + .setDescription("Select a help category") .setRequired(false) .addChoices( - { name: "๐Ÿ  Getting Started", value: "getting_started" }, - { name: "๐Ÿ” Search Commands", value: "search" }, - { name: "โš™๏ธ Admin & Setup", value: "admin" }, - { name: "๐Ÿ”” Notifications", value: "notifications" }, - { name: "โ“ Troubleshooting", value: "troubleshooting" } + { name: "Big Ballers VR", value: "Big Ballers VR" }, + { name: "Blacktop Hoops", value: "Blacktop Hoops" }, + { name: "Breachers", value: "Breachers" }, + { name: "Echo Arena", value: "Echo Arena" }, + { name: "Echo Combat", value: "Echo Combat" }, + { name: "Gun Raiders", value: "Gun Raiders" }, + { name: "Nock", value: "Nock" }, + { name: "Orion Drift", value: "Orion Drift" }, + { name: "VAIL", value: "VAIL" } ) ), + new SlashCommandBuilder() + .setName("version") + .setDescription("Show bot version and system information"), new SlashCommandBuilder() .setName("finduser") .setDescription("Find a user by username") @@ -29,7 +33,17 @@ const commands = [ .setName("game") .setDescription("Select the game") .setRequired(true) - .setAutocomplete(true) + .addChoices( + { name: "Big Ballers VR", value: "Big Ballers VR" }, + { name: "Blacktop Hoops", value: "Blacktop Hoops" }, + { name: "Breachers", value: "Breachers" }, + { name: "Echo Arena", value: "Echo Arena" }, + { name: "Echo Combat", value: "Echo Combat" }, + { name: "Gun Raiders", value: "Gun Raiders" }, + { name: "Nock", value: "Nock" }, + { name: "Orion Drift", value: "Orion Drift" }, + { name: "VAIL", value: "VAIL" } + ) ) .addStringOption((option) => option @@ -47,7 +61,21 @@ const commands = [ .setRequired(true) ) .addStringOption((option) => - option.setName("game").setDescription("Filter by game").setRequired(false).setAutocomplete(true) + option + .setName("game") + .setDescription("Filter by game (optional)") + .setRequired(false) + .addChoices( + { name: "Big Ballers VR", value: "Big Ballers VR" }, + { name: "Blacktop Hoops", value: "Blacktop Hoops" }, + { name: "Breachers", value: "Breachers" }, + { name: "Echo Arena", value: "Echo Arena" }, + { name: "Echo Combat", value: "Echo Combat" }, + { name: "Gun Raiders", value: "Gun Raiders" }, + { name: "Nock", value: "Nock" }, + { name: "Orion Drift", value: "Orion Drift" }, + { name: "VAIL", value: "VAIL" } + ) ), new SlashCommandBuilder() .setName("subscribe") @@ -58,7 +86,17 @@ const commands = [ .setName("game") .setDescription("Game to subscribe to") .setRequired(true) - .setAutocomplete(true) + .addChoices( + { name: "Big Ballers VR", value: "Big Ballers VR" }, + { name: "Blacktop Hoops", value: "Blacktop Hoops" }, + { name: "Breachers", value: "Breachers" }, + { name: "Echo Arena", value: "Echo Arena" }, + { name: "Echo Combat", value: "Echo Combat" }, + { name: "Gun Raiders", value: "Gun Raiders" }, + { name: "Nock", value: "Nock" }, + { name: "Orion Drift", value: "Orion Drift" }, + { name: "VAIL", value: "VAIL" } + ) ) .addChannelOption((option) => option @@ -75,7 +113,17 @@ const commands = [ .setName("game") .setDescription("Game to unsubscribe from") .setRequired(true) - .setAutocomplete(true) + .addChoices( + { name: "Big Ballers VR", value: "Big Ballers VR" }, + { name: "Blacktop Hoops", value: "Blacktop Hoops" }, + { name: "Breachers", value: "Breachers" }, + { name: "Echo Arena", value: "Echo Arena" }, + { name: "Echo Combat", value: "Echo Combat" }, + { name: "Gun Raiders", value: "Gun Raiders" }, + { name: "Nock", value: "Nock" }, + { name: "Orion Drift", value: "Orion Drift" }, + { name: "VAIL", value: "VAIL" } + ) ), new SlashCommandBuilder() .setName("register_server") @@ -93,7 +141,17 @@ const commands = [ .setName("game") .setDescription("Select the game") .setRequired(true) - .setAutocomplete(true) + .addChoices( + { name: "Big Ballers VR", value: "Big Ballers VR" }, + { name: "Blacktop Hoops", value: "Blacktop Hoops" }, + { name: "Breachers", value: "Breachers" }, + { name: "Echo Arena", value: "Echo Arena" }, + { name: "Echo Combat", value: "Echo Combat" }, + { name: "Gun Raiders", value: "Gun Raiders" }, + { name: "Nock", value: "Nock" }, + { name: "Orion Drift", value: "Orion Drift" }, + { name: "VAIL", value: "VAIL" } + ) ) .addStringOption((option) => option diff --git a/index.js b/index.js index 6f99b98..f3e51d5 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,9 @@ const winston = require('winston'); const { createClient } = require('@supabase/supabase-js'); const Bot = require('./src/Bot'); +const packageInfo = require('./package.json'); require('dotenv').config(); -// Import Express and create an app for health checks and webhooks -const express = require('express'); -const app = express(); - -// Middleware for parsing JSON -app.use(express.json()); - // Initialize logger const logger = winston.createLogger({ level: 'debug', @@ -27,60 +21,155 @@ const logger = winston.createLogger({ ] }); +// Display startup banner with version info +logger.info('๐Ÿค– ===== VRBattles Discord Bot Starting ===== ๐Ÿค–'); +logger.info(`๐Ÿ“ฆ Version: ${packageInfo.version}`); +logger.info(`๐Ÿ“ Name: ${packageInfo.name}`); +logger.info(`๐Ÿ“… Started: ${new Date().toISOString()}`); +logger.info(`๐ŸŒ Environment: ${process.env.NODE_ENV || 'development'}`); +logger.info(`๐Ÿง Platform: ${process.platform} ${process.arch}`); +logger.info(`โšก Node.js: ${process.version}`); + +// Check required environment variables +const requiredEnvVars = ['DISCORD_TOKEN', 'SUPABASE_URL', 'SUPABASE_KEY']; +const missingEnvVars = requiredEnvVars.filter(varName => !process.env[varName]); + +if (missingEnvVars.length > 0) { + logger.error(`โŒ Missing required environment variables: ${missingEnvVars.join(', ')}`); + process.exit(1); +} + +logger.info('โœ… Environment variables validated'); + // Initialize Supabase client +logger.info('๐Ÿ”— Initializing Supabase connection...'); const supabase = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_KEY ); -// Initialize bot -const bot = new Bot(process.env.DISCORD_TOKEN, supabase, logger); - -// Add health check endpoint -app.get('/health', (req, res) => { - res.status(200).json({ status: 'healthy' }); -}); - -// Add notification webhook endpoint (this will be used by the NotificationService) -app.post('/api/match-notification', async (req, res) => { - if (bot.notificationService && bot.notificationService.handleMatchNotification) { - await bot.notificationService.handleMatchNotification(req, res); - } else { - res.status(503).json({ error: 'Notification service not ready' }); - } -}); - -// Start the server -const PORT = process.env.PORT || 3000; -app.listen(PORT, () => { - logger.info(`Health check server listening on port ${PORT}`); -}); +// Initialize bot variable at module level +let bot = null; // Initialize and start bot -bot.start().catch(error => { - console.error('Failed to start bot:', error); - process.exit(1); -}); +async function startBot() { + try { + // Test Supabase connection + logger.info('๐Ÿงช Testing Supabase connection...'); + + // Test 1: Read access to games table + const { data: games, error } = await supabase + .from('games') + .select('id, name, active') + .eq('active', true) + .limit(3); + + if (error) { + logger.error('โŒ Supabase read test failed:', { + message: error.message, + details: error.details, + hint: error.hint, + code: error.code + }); + process.exit(1); + } + + logger.info(`โœ… Supabase read access OK - Found ${games.length} active games`); + logger.info(`๐Ÿ“‹ Sample games: ${games.map(g => g.name).join(', ')}`); + + // Test 2: Write access to servers table (dry run) + logger.info('๐Ÿงช Testing database write permissions...'); + const testGuildId = 'test_connection_' + Date.now(); + + try { + // Try to insert a test record + const { data: testServer, error: insertError } = await supabase + .from('servers') + .insert([{ + discord_server_id: testGuildId, + server_name: 'Connection Test Server', + active: false // Mark as inactive so it doesn't interfere + }]) + .select() + .single(); + + if (insertError) { + logger.error('โŒ Database write test failed:', { + message: insertError.message, + details: error.details, + hint: insertError.hint, + code: insertError.code + }); + logger.error('๐Ÿ’ก This suggests a database permissions issue. Check row-level security policies.'); + process.exit(1); + } + + // Clean up test record + if (testServer?.id) { + await supabase + .from('servers') + .delete() + .eq('id', testServer.id); + } + + logger.info('โœ… Database write access OK'); + } catch (writeError) { + logger.error('โŒ Database write test exception:', { + message: writeError.message, + stack: writeError.stack + }); + process.exit(1); + } + + // Initialize and start bot + logger.info('๐Ÿš€ Starting Discord bot...'); + bot = new Bot(process.env.DISCORD_TOKEN, supabase, logger); + + await bot.start(); + return bot; + } catch (error) { + logger.error('โŒ Failed to start bot:', { + message: error.message, + stack: error.stack, + name: error.name + }); + process.exit(1); + } +} + +startBot(); // Handle process termination process.on('SIGINT', async () => { - console.log('Received SIGINT. Shutting down...'); + logger.info('๐Ÿ“ฅ Received SIGINT. Shutting down gracefully...'); try { - await bot.stop(); + if (bot) { + await bot.stop(); + } + logger.info('โœ… Bot shutdown completed'); process.exit(0); } catch (error) { - console.error('Error during shutdown:', error); + logger.error('โŒ Error during shutdown:', { + message: error.message, + stack: error.stack + }); process.exit(1); } }); process.on('SIGTERM', async () => { - console.log('Received SIGTERM. Shutting down...'); + logger.info('๐Ÿ“ฅ Received SIGTERM. Shutting down gracefully...'); try { - await bot.stop(); + if (bot) { + await bot.stop(); + } + logger.info('โœ… Bot shutdown completed'); process.exit(0); } catch (error) { - console.error('Error during shutdown:', error); + logger.error('โŒ Error during shutdown:', { + message: error.message, + stack: error.stack + }); process.exit(1); } }); \ No newline at end of file diff --git a/package.json b/package.json index 8646306..c0ab2fa 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,26 @@ { - "name": "discord-bot", - "version": "1.0.0", + "name": "vrbattles-discord-bot", + "version": "1.2.7", + "description": "VRBattles Discord Bot - Player search, team lookup, match notifications, and more for VR gaming communities", "main": "index.js", "scripts": { "start": "node -r dotenv/config index.js", + "start:docker": "node index.js", "dev": "nodemon -r dotenv/config index.js", "deploy-commands": "node -r dotenv/config deploy-commands.js", - "deploy-commands:dev": "node -r dotenv/config deploy-commands.js", - "test:webhook": "node -r dotenv/config src/tests/testWebhook.js", - "test:connection": "node -r dotenv/config src/tests/testConnection.js", - "test:supabase": "node -r dotenv/config src/tests/testSupabase.js", - "seed:games": "node -r dotenv/config src/scripts/seedGames.js", - "verify:setup": "node -r dotenv/config src/scripts/verifysetup.js" + "sync-games": "node -r dotenv/config src/scripts/syncGameChoices.js && node -r dotenv/config deploy-commands.js", + "deploy": "./scripts/deploy.sh", + "test:webhook": "node -r dotenv/config src/test/testWebhook.js", + "test:supabase": "node src/scripts/testSupabaseConnection.js", + "generate:invite": "node src/scripts/generateInvite.js" }, - "keywords": [], - "author": "", + "keywords": ["discord", "bot", "vr", "gaming", "vrbattles", "vrchat", "vail"], + "author": "VRBattles", "license": "ISC", - "description": "", + "repository": { + "type": "git", + "url": "https://github.com/your-repo/vrbattles-discord-bot" + }, "dependencies": { "@supabase/supabase-js": "^2.46.1", "axios": "^1.6.0", diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..270d314 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# VRBattles Discord Bot - Docker Deploy Script +# Usage: ./scripts/deploy.sh [version] +# Example: ./scripts/deploy.sh v1.2.7 + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +DOCKER_USERNAME="far54" +IMAGE_NAME="vrbattles-discord-bot" +FULL_IMAGE_NAME="${DOCKER_USERNAME}/${IMAGE_NAME}" + +# Get version from package.json if not provided +if [ -z "$1" ]; then + VERSION=$(node -p "require('./package.json').version") + VERSION="v${VERSION}" +else + VERSION="$1" +fi + +echo -e "${BLUE}๐Ÿš€ VRBattles Discord Bot Deployment${NC}" +echo -e "${YELLOW}๐Ÿ“ฆ Version: ${VERSION}${NC}" +echo -e "${YELLOW}๐Ÿณ Image: ${FULL_IMAGE_NAME}${NC}" +echo "" + +# Check if Docker is running +if ! docker info > /dev/null 2>&1; then + echo -e "${RED}โŒ Docker is not running. Please start Docker and try again.${NC}" + exit 1 +fi + +# Sync games from Supabase +echo -e "${BLUE}๐Ÿ”„ Syncing games from Supabase...${NC}" +npm run sync-games + +echo -e "${BLUE}๐Ÿ—๏ธ Building multi-platform Docker image...${NC}" +echo -e "${YELLOW}๐Ÿ“‹ Platforms: linux/amd64, linux/arm64${NC}" + +# Check if buildx is available +if ! docker buildx version > /dev/null 2>&1; then + echo -e "${RED}โŒ Docker buildx is not available. Please update Docker.${NC}" + exit 1 +fi + +# Create/use buildx builder +docker buildx create --name multiarch --use --driver docker-container --bootstrap 2>/dev/null || true +docker buildx use multiarch + +echo -e "${BLUE}๐Ÿ“ค Building and pushing to Docker Hub...${NC}" +echo -e "${YELLOW}Note: Make sure you're logged in with 'docker login'${NC}" + +# Check if logged in +if ! docker info | grep -q "Username:"; then + echo -e "${YELLOW}โš ๏ธ You don't appear to be logged in to Docker Hub.${NC}" + echo -e "${YELLOW}๐Ÿ” Please run: docker login${NC}" + read -p "Press Enter after logging in, or Ctrl+C to cancel..." +fi + +# Build and push multi-platform images +docker buildx build --platform linux/amd64,linux/arm64 \ + -t "${FULL_IMAGE_NAME}:latest" \ + -t "${FULL_IMAGE_NAME}:${VERSION}" \ + . --push + +echo "" +echo -e "${GREEN}โœ… Successfully deployed!${NC}" +echo "" +echo -e "${BLUE}๐Ÿ“‹ Deployment Summary:${NC}" +echo -e " ๐Ÿณ Images pushed:" +echo -e " โ€ข ${FULL_IMAGE_NAME}:latest" +echo -e " โ€ข ${FULL_IMAGE_NAME}:${VERSION}" +echo "" +echo -e "${BLUE}๐Ÿ”ง Coolify Deployment:${NC}" +echo -e " 1. Go to your Coolify dashboard" +echo -e " 2. Update your application image to: ${FULL_IMAGE_NAME}:${VERSION}" +echo -e " 3. Restart the application" +echo "" +echo -e "${BLUE}๐ŸŽฏ Features in this version:${NC}" +echo -e " โœ… All 9 games in dropdowns" +echo -e " โœ… Working help button navigation" +echo -e " โœ… No debug message spam" +echo -e " โœ… Automatic Supabase game sync" +echo -e " โœ… Optimized Docker image" +echo "" +echo -e "${GREEN}๐ŸŽ‰ Deployment complete!${NC}" \ No newline at end of file diff --git a/src/Bot.js b/src/Bot.js index 0bb592d..5a1e54b 100644 --- a/src/Bot.js +++ b/src/Bot.js @@ -6,7 +6,6 @@ const SupabaseService = require('./services/SupabaseService'); const NotificationService = require('./services/NotificationService'); const Logger = require('./utils/Logger'); const ServerRegistrationService = require('./services/ServerRegistrationService'); -const CooldownManager = require('./utils/CooldownManager'); class Bot { constructor(token, supabase, logger) { @@ -25,7 +24,6 @@ class Bot { this.serverRegistrationService = new ServerRegistrationService(supabase, logger); this.subscriptionCommands = new SubscriptionCommands(supabase, logger); this.notificationService = new NotificationService(this, supabase); - this.cooldownManager = new CooldownManager(); // Initialize command handlers this.commandHandler = new CommandHandler( @@ -39,82 +37,15 @@ class Bot { // Setup event handlers this.client.on('ready', () => { this.logger.info(`Logged in as ${this.client.user.tag}`); - // NotificationService is now handled by the main Express server in index.js - this.logger.info('Bot ready - notification service integrated with main server'); + // Start notification service after bot is ready + this.notificationService.start().then(() => { + this.logger.info('Notification service started successfully'); + }).catch(error => { + this.logger.error('Failed to start notification service:', error); + }); }); - this.client.on('interactionCreate', async (interaction) => { - try { - if (interaction.isStringSelectMenu()) { - // Handle select menu interactions (team details) - await this.commandHandler.handleSelectMenu(interaction); - return; - } - - if (interaction.isChatInputCommand() || interaction.isAutocomplete()) { - // Apply rate limiting for chat commands - if (interaction.isChatInputCommand()) { - const cooldownTime = this.cooldownManager.checkCooldown(interaction); - if (cooldownTime > 0) { - await interaction.reply({ - content: `โฐ You need to wait ${cooldownTime.toFixed(1)} seconds before using this command again.`, - flags: ['Ephemeral'] - }); - return; - } - } - - // Defer reply for chat commands only (not autocomplete) - if (interaction.isChatInputCommand()) { - // Determine if command should be public or ephemeral - const isPublicCommand = ['finduser', 'matchhistory', 'findteam'].includes(interaction.commandName); - await interaction.deferReply({ - flags: isPublicCommand ? [] : ['Ephemeral'] - }); - - this.logger.debug('Processing command', { - command: interaction.commandName, - guild: interaction.guild?.name, - isPublic: isPublicCommand, - timestamp: new Date().toISOString() - }); - } - - // Handle the command or autocomplete - await this.commandHandler.handleCommand(interaction); - return; - } - - if (interaction.isButton()) { - // Handle button interactions with proper defer - await interaction.deferUpdate(); - await this.commandHandler.handleButtonInteraction(interaction); - return; - } - - } catch (error) { - this.logger.error('Interaction handling error:', { - type: interaction.type, - commandName: interaction.commandName || 'unknown', - userId: interaction.user?.id, - guildId: interaction.guildId, - error: error.message, - stack: error.stack - }); - - // Only try to reply if it's not an autocomplete interaction and hasn't been replied to - if (!interaction.isAutocomplete() && !interaction.replied && !interaction.deferred) { - try { - await interaction.reply({ - content: 'โŒ An error occurred while processing your request.', - flags: ['Ephemeral'] - }); - } catch (replyError) { - this.logger.error('Failed to send error reply:', replyError); - } - } - } - }); + this.client.on('interactionCreate', this.handleInteraction.bind(this)); } async start() { @@ -128,21 +59,106 @@ class Bot { async stop() { try { - - async stop() { - try { + this.logger.info('Stopping bot...'); + + // Stop notification service + if (this.notificationService) { + await this.notificationService.stop(); + } + + // Destroy Discord client if (this.client) { await this.client.destroy(); - this.logger.info('Bot stopped successfully'); } + + this.logger.info('Bot stopped successfully'); } catch (error) { this.logger.error('Error stopping bot:', error); + throw error; } } + async handleInteraction(interaction) { + if (!interaction.isCommand() && !interaction.isButton()) { + this.logger.debug('Ignoring non-command/button interaction'); + return; + } + + const lockKey = interaction.id; + if (this.processingLock.get(lockKey)) { + this.logger.debug('Interaction already being processed', { id: lockKey }); + return; + } + + this.processingLock.set(lockKey, true); + + try { + if (interaction.isCommand()) { + // Quick defer with timeout handling + const isPublicCommand = ['finduser', 'matchhistory', 'findteam'].includes(interaction.commandName); + + try { + await Promise.race([ + interaction.deferReply({ ephemeral: !isPublicCommand }), + new Promise((_, reject) => setTimeout(() => reject(new Error('Defer timeout')), 2500)) + ]); + } catch (deferError) { + this.logger.warn('Failed to defer interaction:', { + error: deferError.message, + command: interaction.commandName, + guild: interaction.guild?.name + }); + // Try to reply immediately if defer failed + try { + await interaction.reply({ + content: 'โณ Processing your request...', + ephemeral: !isPublicCommand + }); + } catch (replyError) { + this.logger.error('Failed to reply after defer timeout:', replyError); + return; // Give up on this interaction + } + } + + this.logger.debug('Processing command', { + command: interaction.commandName, + guild: interaction.guild?.name, + timestamp: new Date().toISOString() + }); + + await this.commandHandler.handleCommand(interaction); + } else if (interaction.isButton()) { + try { + await interaction.deferUpdate(); + await this.commandHandler.handleButton(interaction); + } catch (error) { + this.logger.error('Button interaction error:', error); + } } } catch (error) { - this.logger.error('Error stopping bot:', error); + this.logger.error('Command processing error:', { + error: error.message, + stack: error.stack, + command: interaction.commandName, + guild: interaction.guild?.name + }); + + 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 (replyError) { + this.logger.error('Failed to send error response:', replyError); + } + } finally { + this.processingLock.delete(lockKey); } } } diff --git a/src/commands/CommandHandler.js b/src/commands/CommandHandler.js index bbbb2f4..3bcc032 100644 --- a/src/commands/CommandHandler.js +++ b/src/commands/CommandHandler.js @@ -2,6 +2,8 @@ const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, PermissionFl const EmbedBuilders = require('../utils/embedBuilders'); const HelpCommand = require('./HelpCommand'); const PaginationManager = require('../utils/PaginationManager'); +const CooldownManager = require('../utils/CooldownManager'); +const packageInfo = require('../../package.json'); class CommandHandler { constructor(playerService, supabase, logger, serverRegistrationService, subscriptionCommands) { @@ -12,29 +14,34 @@ class CommandHandler { this.subscriptionCommands = subscriptionCommands; this.helpCommand = new HelpCommand(supabase, logger); this.paginationManager = new PaginationManager(logger); + this.cooldownManager = new CooldownManager(); } async handleCommand(interaction) { try { - // Handle autocomplete interactions first - if (interaction.isAutocomplete()) { - await this.handleAutocomplete(interaction); + // Check cooldown + const cooldownTime = this.cooldownManager.checkCooldown(interaction); + if (cooldownTime > 0) { + await interaction.reply({ + content: `โฐ Please wait ${cooldownTime.toFixed(1)} seconds before using this command again.`, + flags: ['Ephemeral'] + }); return; } + // Note: deferReply is already handled in Bot.js - don't defer again here + + // Route to appropriate handler switch (interaction.commandName) { case 'register_server': await this.handleRegisterServer(interaction); break; - case 'ping': - await interaction.editReply({ content: 'Pong!', flags: ['Ephemeral'] }); - break; - case 'help': - await this.helpCommand.handleHelp(interaction); - break; case 'finduser': await this.handleFindUser(interaction); break; + case 'findteam': + await this.handleFindTeam(interaction); + break; case 'matchhistory': await this.handleMatchHistory(interaction); break; @@ -47,141 +54,40 @@ class CommandHandler { case 'list_subscriptions': await this.subscriptionCommands.handleListSubscriptions(interaction); break; - case 'findteam': - await this.handleFindTeam(interaction); + case 'help': + await this.helpCommand.handleHelp(interaction); + break; + case 'version': + await this.handleVersion(interaction); break; default: - await interaction.editReply({ - content: 'โŒ Unknown command', + await interaction.editReply({ + content: 'โŒ Unknown command.', flags: ['Ephemeral'] }); } } catch (error) { - this.logger.error('Command handling error:', { - command: interaction.commandName, + this.logger.error('Error in handleCommand:', { error: error.message, - stack: error.stack + commandName: interaction.commandName, + userId: interaction.user.id, + guildId: interaction.guildId, + timestamp: new Date().toISOString() }); try { - await interaction.editReply({ - content: 'โŒ An error occurred while processing your command.', - flags: ['Ephemeral'] - }); - } catch (followUpError) { - this.logger.error('Failed to send error response:', { - error: followUpError.message, - originalError: error.message - }); - } - } - } - - async handleAutocomplete(interaction) { - const { commandName, options } = interaction; - - if (options.getFocused().name === 'game') { - try { - const guildId = interaction.guildId; - - // Check if this is a subscription/admin command - const isAdminCommand = ['subscribe', 'unsubscribe'].includes(commandName); - - if (isAdminCommand) { - // Show ALL games for admin/subscription commands - const allGames = [ - { name: "Big Ballers VR", value: "Big Ballers VR" }, - { name: "Blacktop Hoops", value: "Blacktop Hoops" }, - { name: "Breachers", value: "Breachers" }, - { name: "Echo Arena", value: "Echo Arena" }, - { name: "Echo Combat", value: "Echo Combat" }, - { name: "Gun Raiders", value: "Gun Raiders" }, - { name: "Nock", value: "Nock" }, - { name: "Orion Drift", value: "Orion Drift" }, - { name: "VAIL", value: "VAIL" } - ]; - - await interaction.respond(allGames); - return; - } - - // For user commands (finduser, findteam, matchhistory), check subscriptions - const isUserCommand = ['finduser', 'findteam'].includes(commandName); - const isMatchHistory = commandName === 'matchhistory'; - - // Get subscriptions - const subscriptions = await this.getServerSubscriptions(guildId); - - if (isUserCommand && subscriptions.length === 0) { - // Fallback for unsubscribed servers (strict requirement for finduser/findteam) - const fallbackOptions = [ - { - name: "โŒ No game subscriptions found", - value: "no_subscriptions" - }, - { - name: "๐Ÿ’ก Use /subscribe to add games first", - value: "need_subscription" - }, - { - name: "๐Ÿ“‹ Then use /list_subscriptions to see active games", - value: "list_help" - } - ]; - - await interaction.respond(fallbackOptions); - return; - } - - if (isMatchHistory) { - // For matchhistory, show subscribed games OR all games if no subscriptions - const gamesToShow = subscriptions.length > 0 ? subscriptions : [ - { game_name: "Big Ballers VR" }, - { game_name: "Blacktop Hoops" }, - { game_name: "Breachers" }, - { game_name: "Echo Arena" }, - { game_name: "Echo Combat" }, - { game_name: "Gun Raiders" }, - { game_name: "Nock" }, - { game_name: "Orion Drift" }, - { game_name: "VAIL" } - ]; - - const choices = gamesToShow.map(sub => ({ - name: subscriptions.length > 0 ? `๐ŸŽฎ ${sub.game_name}` : `๐Ÿ” ${sub.game_name}`, - value: sub.game_name - })); - - await interaction.respond(choices.slice(0, 25)); - return; - } - - // Show subscribed games with helpful context for finduser/findteam - const choices = subscriptions.map(sub => ({ - name: `๐ŸŽฎ ${sub.game_name}`, - value: sub.game_name - })); - - // Add helpful footer if less than 25 games (Discord's limit) - if (choices.length < 24) { - choices.push({ - name: `๐Ÿ“ ${choices.length} subscribed game${choices.length !== 1 ? 's' : ''} available`, - value: "info_footer" + if (interaction.replied || interaction.deferred) { + await interaction.editReply({ + content: 'โŒ An error occurred while processing your command.', + }); + } else { + await interaction.reply({ + content: 'โŒ An error occurred while processing your command.', + flags: ['Ephemeral'] }); } - - await interaction.respond(choices.slice(0, 25)); // Discord limit - - } catch (error) { - this.logger.error('Autocomplete error:', error); - - // Fallback error response - await interaction.respond([ - { - name: "โŒ Error loading games - try again", - value: "error_fallback" - } - ]); + } catch (replyError) { + this.logger.error('Failed to send error response:', replyError); } } } @@ -189,9 +95,9 @@ class CommandHandler { async getServerSubscriptions(guildId) { try { const { data: subscriptions, error } = await this.supabase - .from("active_subscriptions") - .select("game_name") - .eq("discord_server_id", guildId); + .from('active_subscriptions') + .select('game_name') + .eq('discord_server_id', guildId); if (error) { this.logger.error('Error fetching server subscriptions:', error); @@ -232,16 +138,14 @@ class CommandHandler { return { isRegistered, subscriptionCount: subscriptions.length, - subscriptions: subscriptions.map(s => s.game_name), setupComplete: isRegistered && subscriptions.length > 0, - getGuidanceMessage: () => { + getGuidanceMessage() { if (!isRegistered) { - return 'โš ๏ธ **Setup Required:** This server isn\'t registered yet.\n\n๐Ÿ”ง **Next Step:** Use `/register_server` to get started, then `/subscribe` to add games.\n๐Ÿ’ก **Need help?** Use `/help getting_started` for a complete guide.'; + return "First, run `/register_server` to connect your server to VRBattles."; } else if (subscriptions.length === 0) { - return 'โš ๏ธ **Almost Ready:** Server is registered but no games are subscribed.\n\n๐ŸŽฎ **Next Step:** Use `/subscribe` to add games for notifications and search.\n๐Ÿ“‹ **Check Status:** Use `/list_subscriptions` to see current setup.'; - } else { - return `โœ… **Setup Complete:** ${subscriptions.length} game${subscriptions.length !== 1 ? 's' : ''} subscribed: ${subscriptions.join(', ')}`; + return "Next, run `/subscribe` to add game notifications."; } + return "Your server is properly set up!"; } }; } catch (error) { @@ -249,36 +153,25 @@ class CommandHandler { return { isRegistered: false, subscriptionCount: 0, - subscriptions: [], setupComplete: false, - getGuidanceMessage: () => 'โŒ **Status Check Failed:** Unable to verify setup. Try `/help troubleshooting` if issues persist.' + getGuidanceMessage() { + return "Please check your server setup and try again."; + } }; } } async handleRegisterServer(interaction) { try { - // Check if command is being used in a guild (not DM) - if (!interaction.guild || !interaction.guildId) { + // Check for admin permissions + if (!interaction.member.permissions.has('ADMINISTRATOR')) { await interaction.editReply({ - content: 'โŒ This command can only be used in a server, not in direct messages.\n\n๐Ÿ’ก **Tip:** Add this bot to your Discord server first, then try again.', - flags: ['Ephemeral'] + content: 'โŒ You need administrator permissions to register this server.', + ephemeral: true }); return; } - // Permission check is handled by Discord via setDefaultMemberPermissions - // Adding runtime check as a safeguard - check for Manage Server or Administrator - if (interaction.memberPermissions && - !interaction.memberPermissions.has(PermissionFlagsBits.Administrator) && - !interaction.memberPermissions.has(PermissionFlagsBits.ManageGuild)) { - await interaction.editReply({ - content: 'โŒ You need Administrator or Manage Server permissions to use this command.\n\n๐Ÿ”’ **Required Permissions:**\nโ€ข Administrator OR\nโ€ข Manage Server\n\n๐Ÿ’ก **Tip:** Ask a server admin to run this command or grant you the necessary permissions.', - flags: ['Ephemeral'] - }); - return; - } - const guildId = interaction.guildId; const serverName = interaction.guild.name; @@ -293,38 +186,63 @@ class CommandHandler { // Log the result this.logger.debug('Server registration result received', { - status: result.status, - serverId: result.server?.id, + status: result?.status, + hasServer: !!result?.server, + serverId: result?.server?.id, guildId, timestamp: new Date().toISOString() }); + // Validate result structure + if (!result || typeof result !== 'object') { + this.logger.error('Invalid result from registerServer:', { result, guildId, serverName }); + await interaction.editReply({ + content: 'โŒ Server registration failed due to invalid response. Please try again or contact support.', + ephemeral: true + }); + return; + } + + if (!result.server) { + this.logger.error('Server registration returned no server data:', { + result, + guildId, + serverName, + timestamp: new Date().toISOString() + }); + await interaction.editReply({ + content: 'โŒ Server registration failed - no server data returned. This might be a database permissions issue.', + ephemeral: true + }); + return; + } + // Prepare response message based on status let message; switch (result.status) { case 'created': - message = 'โœ… **Server successfully registered!** You can now use subscription commands.\n\n๐ŸŽฏ **Next Steps:**\n1. Use `/subscribe` to add games\n2. Use `/help` to learn about available commands\n3. Start searching with `/finduser` once games are added!'; + message = 'โœ… Server successfully registered! You can now use subscription commands.'; break; case 'updated': - message = 'โœ… **Server information has been updated!**\n\n๐Ÿ’ก Your server was already registered, but we\'ve refreshed the details.'; + message = 'โœ… Server information has been updated!'; break; case 'exists': - message = 'โœ… **This server is already registered** and ready to use subscription commands!\n\n๐Ÿš€ **Ready to go:** You can now use `/subscribe`, `/finduser`, and other commands.'; + message = 'โœ… This server is already registered and ready to use subscription commands!'; break; default: - message = 'โŒ An unexpected error occurred during registration.\n\n๐Ÿ”„ **Try again:** Wait a moment and retry the command.\nโ“ **Still having issues?** Use `/help troubleshooting` for more help.'; + message = 'โŒ An unexpected error occurred during registration.'; } await interaction.editReply({ content: message, - flags: ['Ephemeral'] + ephemeral: true }); // Log the successful operation this.logger.info('Server registration completed', { status: result.status, guildId, - serverId: result.server.id, + serverId: result.server?.id, timestamp: new Date().toISOString() }); @@ -332,18 +250,18 @@ class CommandHandler { this.logger.error('Error in handleRegisterServer:', { error: error.message, stack: error.stack, - guildId: interaction.guildId || 'Unknown', - serverName: interaction.guild?.name || 'Unknown' + guildId: interaction.guildId, + serverName: interaction.guild?.name }); // Send error message to user const errorMessage = error.message === 'Invalid guildId provided' || error.message === 'Invalid serverName provided' - ? 'โŒ **Invalid server information provided.**\n\n๐Ÿ”„ **Try again:** This usually resolves itself, please retry the command.\nโ“ **Need help?** Use `/help troubleshooting` for common solutions.' - : 'โŒ **Registration failed.** An error occurred while registering the server.\n\n๐Ÿ”„ **Try again:** Wait a moment and retry the command.\n๐Ÿ†˜ **Still having issues?** Contact support in our main Discord server.'; + ? 'โŒ Invalid server information provided.' + : 'โŒ An error occurred while registering the server. Please try again later.'; await interaction.editReply({ content: errorMessage, - flags: ['Ephemeral'] + ephemeral: true }); throw error; @@ -355,17 +273,6 @@ class CommandHandler { const username = interaction.options.getString('username'); const gameFilter = interaction.options.getString('game'); - // Handle fallback autocomplete values with enhanced guidance - if (['no_subscriptions', 'need_subscription', 'list_help', 'info_footer', 'error_fallback'].includes(gameFilter)) { - const setupStatus = await this.checkSetupStatusWithGuidance(interaction.guildId); - - await interaction.editReply({ - content: `โŒ **Can't search yet!** ${setupStatus.getGuidanceMessage()}\n\n๐Ÿ” **Why this happens:** Search commands require game subscriptions to work properly.\n\n๐Ÿ“š **Need more help?** Use \`/help getting_started\` for step-by-step instructions.`, - flags: ['Ephemeral'] - }); - return; - } - // Input validation with helpful messages if (!username || typeof username !== 'string') { await interaction.editReply({ @@ -398,10 +305,8 @@ class CommandHandler { const userData = await this.playerService.findUserByUsername(sanitizedUsername); if (!userData || !userData.success) { - const setupStatus = await this.checkSetupStatusWithGuidance(interaction.guildId); - await interaction.editReply({ - content: `โŒ **User "${sanitizedUsername}" not found** in ${gameFilter}.\n\n๐Ÿ” **Possible reasons:**\nโ€ข Username spelling might be incorrect\nโ€ข Player hasn't played ${gameFilter} recently\nโ€ข Player might not exist on VRBattles\n\n๐Ÿ’ก **Try:**\nโ€ข Double-check the username spelling\nโ€ข Try a different game\nโ€ข Use \`/help search\` for search tips\n\n๐Ÿ“Š **Server Status:** ${setupStatus.getGuidanceMessage()}` + content: 'โŒ User not found or an error occurred while fetching data.' }); return; } @@ -421,7 +326,7 @@ class CommandHandler { if (!hasPlayedGame) { const teamMessage = isOnTeam ? - `\n๐ŸŽฏ **Note:** They are on a team for ${gameFilter} but haven't played any matches yet.` : + `\nThey are on a team for ${gameFilter} but haven't played any matches yet.` : ''; // Create a basic embed with user info @@ -460,7 +365,7 @@ class CommandHandler { // Add message about no game stats basicEmbed.addFields({ name: `${gameFilter} Status`, - value: `โŒ No match history found for ${gameFilter}${teamMessage}\n\n๐Ÿ’ก **Suggestions:**\nโ€ข Try other games they might have played\nโ€ข Check their profile for recent activity\nโ€ข Use \`/matchhistory ${sanitizedUsername}\` to see all games` + value: `โŒ No match history found for ${gameFilter}${teamMessage}` }); const row = this.createActionRow(playerData.username); @@ -488,7 +393,7 @@ class CommandHandler { timestamp: new Date().toISOString() }); await interaction.editReply({ - content: 'โŒ **Search failed.** An unexpected error occurred while searching for the user.\n\n๐Ÿ”„ **Try again:** This is usually temporary, please retry in a moment.\n๐Ÿ†˜ **Still having issues?** Use `/help troubleshooting` or contact support.' + content: 'โŒ An error occurred while searching for the user.' }); } } @@ -498,18 +403,10 @@ class CommandHandler { const username = interaction.options.getString('username'); const gameFilter = interaction.options.getString('game'); - // Input validation - if (!username || typeof username !== 'string') { - await interaction.editReply({ - content: 'โŒ **Invalid username provided.**\n\nโœ๏ธ **Expected:** A valid VRBattles username\n๐Ÿ’ก **Tip:** Usernames are case-sensitive and should match exactly as shown on VRBattles', - }); - return; - } - const userData = await this.playerService.findUserByUsername(username); if (!userData || !userData.success) { await interaction.editReply({ - content: `โŒ **User "${username}" not found** or an error occurred while fetching data.\n\n๐Ÿ” **Possible reasons:**\nโ€ข Username spelling might be incorrect\nโ€ข Player might not exist on VRBattles\nโ€ข Temporary connection issue\n\n๐Ÿ’ก **Try:**\nโ€ข Double-check the username spelling\nโ€ข Wait a moment and try again\nโ€ข Use \`/help search\` for search tips`, + content: 'โŒ User not found or an error occurred while fetching data.', }); return; } @@ -519,42 +416,14 @@ class CommandHandler { : userData.player_data; // Extract matches from player data - const allMatches = Object.values(playerData.matches || {}); - const matches = allMatches + const matches = Object.values(playerData.matches || {}) .filter(match => !gameFilter || match.game_name.toLowerCase() === gameFilter.toLowerCase()) .sort((a, b) => new Date(b.start_time) - new Date(a.start_time)) .slice(0, 10); - if (matches.length === 0) { - const gameText = gameFilter ? ` for ${gameFilter}` : ''; - const totalMatches = allMatches.length; - - let message = `โŒ **No match history found** for ${username}${gameText}.`; - - if (totalMatches > 0 && gameFilter) { - message += `\n\n๐Ÿ“Š **Available:** ${totalMatches} matches in other games\n๐Ÿ’ก **Try:** Remove the game filter to see all matches`; - } else if (totalMatches === 0) { - message += `\n\n๐Ÿ” **This could mean:**\nโ€ข Player hasn't played any ranked matches\nโ€ข Player is new to VRBattles\nโ€ข Data hasn't been updated yet`; - } - - message += '\n\n๐Ÿ” **Alternative:** Try `/finduser` to see their profile and stats instead.'; - - await interaction.editReply({ - content: message, - }); - return; - } - const embed = EmbedBuilders.createMatchHistoryEmbed(playerData, matches); const row = EmbedBuilders.createActionRow(playerData.username, this.playerService); - // Add helpful footer to embed if game filter was used - if (gameFilter) { - embed.setFooter({ - text: `Showing ${gameFilter} matches only. Use without game filter to see all matches.` - }); - } - await interaction.editReply({ embeds: [embed], components: [row], @@ -565,9 +434,7 @@ class CommandHandler { username: interaction.options.getString('username'), timestamp: new Date().toISOString() }); - await interaction.editReply({ - content: 'โŒ **Match history search failed.** An unexpected error occurred.\n\n๐Ÿ”„ **Try again:** This is usually temporary, please retry in a moment.\n๐Ÿ†˜ **Still having issues?** Use `/help troubleshooting` or contact support.' - }); + throw error; } } @@ -576,21 +443,10 @@ class CommandHandler { const teamName = interaction.options.getString('teamname'); const gameFilter = interaction.options.getString('game'); - // Handle fallback autocomplete values - if (['no_subscriptions', 'need_subscription', 'list_help', 'info_footer', 'error_fallback'].includes(gameFilter)) { - const setupStatus = await this.checkSetupStatusWithGuidance(interaction.guildId); - - await interaction.editReply({ - content: `โŒ **Can't search yet!** ${setupStatus.getGuidanceMessage()}\n\n๐Ÿ” **Why this happens:** Search commands require game subscriptions to work properly.\n\n๐Ÿ“š **Need more help?** Use \`/help getting_started\` for step-by-step instructions.`, - ephemeral: false - }); - return; - } - // Input validation if (!teamName || typeof teamName !== 'string') { await interaction.editReply({ - content: 'โŒ **Invalid team name provided.**\n\nโœ๏ธ **Expected:** A valid team name\n๐Ÿ’ก **Tip:** Team names are case-sensitive and should match exactly', + content: 'โŒ Invalid team name provided.', ephemeral: false }); return; @@ -604,7 +460,7 @@ class CommandHandler { if (!sanitizedTeamName) { await interaction.editReply({ - content: 'โŒ **Invalid characters in team name.**\n\nโœ… **Allowed characters:** Letters, numbers, spaces, hyphens (-), underscores (_), and periods (.)\n๐Ÿ” **Example:** Try "Team Name" or "Team_123"', + content: 'โŒ Team name must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).', ephemeral: false }); return; @@ -622,46 +478,20 @@ class CommandHandler { const teamData = await this.playerService.findTeamByName(sanitizedTeamName, gameFilter); if (!teamData || !teamData.success || !teamData.teams || teamData.teams.length === 0) { - const setupStatus = await this.checkSetupStatusWithGuidance(interaction.guildId); - await interaction.editReply({ - content: `โŒ **No teams found** matching "${sanitizedTeamName}" in ${gameFilter}.\n\n๐Ÿ” **Possible reasons:**\nโ€ข Team name spelling might be incorrect\nโ€ข Team might not exist for this game\nโ€ข Team might be inactive\n\n๐Ÿ’ก **Try:**\nโ€ข Double-check the team name spelling\nโ€ข Try a different game\nโ€ข Search for partial team names\n\n๐Ÿ“Š **Server Status:** ${setupStatus.getGuidanceMessage()}`, + content: 'โŒ No teams found matching your search criteria.', ephemeral: false }); return; } - const teams = teamData.teams; - - // If only one team or few teams, show them directly - if (teams.length <= 3) { - const embeds = EmbedBuilders.createTeamEmbed(teams); - await interaction.editReply({ - embeds: embeds, - components: [], - ephemeral: false - }); - return; - } - - // Use pagination for many teams - const teamEmbedBuilder = (teamSubset, currentPage, totalPages) => { - return EmbedBuilders.createPaginatedTeamEmbed(teamSubset, currentPage, totalPages, sanitizedTeamName, gameFilter); - }; - - const paginatedData = this.paginationManager.createPaginatedResponse( - teams, - 3, // 3 teams per page - teamEmbedBuilder, - `Teams matching "${sanitizedTeamName}"` - ); - - await this.paginationManager.sendPaginatedMessage( - interaction, - paginatedData, - 'teamlist' - ); + const embeds = EmbedBuilders.createTeamEmbed(teamData.teams); + await interaction.editReply({ + embeds: embeds, + components: [], + ephemeral: false + }); } catch (error) { this.logger.error('Error in handleFindTeam:', { error: error.message, @@ -671,32 +501,17 @@ class CommandHandler { timestamp: new Date().toISOString() }); await interaction.editReply({ - content: 'โŒ **Team search failed.** An unexpected error occurred while searching for teams.\n\n๐Ÿ”„ **Try again:** This is usually temporary, please retry in a moment.\n๐Ÿ†˜ **Still having issues?** Use `/help troubleshooting` or contact support.', + content: 'โŒ An error occurred while searching for teams.', ephemeral: false }); } } - createActionRow(username) { - return new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setLabel('๐Ÿ”ต View Profile') - .setStyle(ButtonStyle.Link) - .setURL(this.playerService.getProfileUrl(username)), - new ButtonBuilder() - .setLabel('๐ŸŸก Join Main Discord') - .setStyle(ButtonStyle.Link) - .setURL('https://discord.gg/j3DKVATPGQ') - ); - } - - async handleButtonInteraction(interaction) { + async handleButton(interaction) { try { - // Defer the update to show loading state - await interaction.deferUpdate(); - // Check if this is a pagination button if (this.paginationManager.isPaginationButton(interaction.customId)) { + await interaction.deferUpdate(); await this.paginationManager.handlePaginationButton(interaction); return; } @@ -713,8 +528,13 @@ class CommandHandler { userId: interaction.user.id }); + await interaction.reply({ + content: 'โŒ This button interaction is not implemented yet.', + ephemeral: true + }); + } catch (error) { - this.logger.error('Error in handleButtonInteraction:', { + this.logger.error('Error in handleButton:', { error: error.message, customId: interaction.customId, userId: interaction.user.id @@ -724,11 +544,7 @@ class CommandHandler { if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: 'โŒ An error occurred while processing the button interaction.', - flags: ['Ephemeral'] - }); - } else { - await interaction.editReply({ - content: 'โŒ An error occurred while processing the button interaction.' + ephemeral: true }); } } catch (replyError) { @@ -738,11 +554,105 @@ class CommandHandler { } async handleSelectMenu(interaction) { - // Handle select menu interactions (placeholder for future implementation) - this.logger.debug('Select menu interaction received:', { - customId: interaction.customId, - values: interaction.values - }); + try { + // Handle select menu interactions (if needed) + this.logger.debug('Select menu interaction:', { + customId: interaction.customId, + values: interaction.values, + userId: interaction.user.id + }); + + await interaction.reply({ + content: 'Select menu interactions are not yet implemented.', + flags: ['Ephemeral'] + }); + } catch (error) { + this.logger.error('Error in handleSelectMenu:', { + error: error.message, + customId: interaction.customId, + userId: interaction.user.id + }); + } + } + + async handleVersion(interaction) { + try { + const uptimeHours = Math.floor(process.uptime() / 3600); + const uptimeMinutes = Math.floor((process.uptime() % 3600) / 60); + const uptimeSeconds = Math.floor(process.uptime() % 60); + + const embed = new EmbedBuilder() + .setTitle('๐Ÿค– BattleBot Version Information') + .setColor('#0099ff') + .addFields( + { + name: '๐Ÿ“ฆ Bot Version', + value: `\`v${packageInfo.version}\``, + inline: true + }, + { + name: '๐Ÿ•’ Uptime', + value: `${uptimeHours}h ${uptimeMinutes}m ${uptimeSeconds}s`, + inline: true + }, + { + name: 'โšก Node.js Version', + value: `\`${process.version}\``, + inline: true + }, + { + name: '๐Ÿ’ป Platform', + value: `\`${process.platform} ${process.arch}\``, + inline: true + }, + { + name: '๐ŸŽฎ Discord.js Version', + value: `\`v${require('discord.js').version}\``, + inline: true + }, + { + name: '๐Ÿ“… Last Updated', + value: new Date().toISOString().split('T')[0], + inline: true + } + ) + .setFooter({ + text: `${packageInfo.name} | VRBattles`, + iconURL: 'https://vrbattles.gg/favicon.ico' + }) + .setTimestamp(); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel('๐Ÿ“– Documentation') + .setStyle(ButtonStyle.Link) + .setURL('https://help.vrbattles.gg'), + new ButtonBuilder() + .setLabel('๐ŸŒ VRBattles') + .setStyle(ButtonStyle.Link) + .setURL('https://vrbattles.gg') + ); + + await interaction.editReply({ + embeds: [embed], + components: [row] + }); + } catch (error) { + this.logger.error('Error in handleVersion:', error); + await interaction.editReply({ + content: 'โŒ An error occurred while fetching version information.', + flags: ['Ephemeral'] + }); + } + } + + createActionRow(username) { + return new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel('๐Ÿ”ต View Profile') + .setStyle(ButtonStyle.Link) + .setURL(`https://www.vrbattles.gg/profile/${username}`) + ); } } diff --git a/src/commands/HelpCommand.js b/src/commands/HelpCommand.js index 0cfb620..d0cd334 100644 --- a/src/commands/HelpCommand.js +++ b/src/commands/HelpCommand.js @@ -1,4 +1,5 @@ const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); +const packageInfo = require('../../package.json'); class HelpCommand { constructor(supabase, logger) { @@ -24,8 +25,7 @@ class HelpCommand { } catch (error) { this.logger.error('Error in handleHelp:', error); await interaction.editReply({ - content: 'โŒ An error occurred while loading help. Please try again.', - flags: ['Ephemeral'] + content: 'โŒ An error occurred while loading help. Please try again.' }); } } @@ -75,7 +75,11 @@ class HelpCommand { const embed = new EmbedBuilder() .setTitle('๐Ÿค– BattleBot Help Center') .setDescription('Welcome to BattleBot! Your gateway to VRBattles data and notifications.') - .setColor(setupStatus.setupComplete ? '#00ff00' : '#ffaa00'); + .setColor(setupStatus.setupComplete ? '#00ff00' : '#ffaa00') + .setFooter({ + text: `BattleBot v${packageInfo.version} | VRBattles`, + iconURL: 'https://vrbattles.gg/favicon.ico' + }); // Add setup status section const setupEmoji = setupStatus.setupComplete ? 'โœ…' : 'โš ๏ธ'; @@ -139,15 +143,18 @@ class HelpCommand { await interaction.editReply({ embeds: [embed], - components: [row, row2], - flags: ['Ephemeral'] + components: [row, row2] }); } async showCategoryHelp(interaction, category, setupStatus) { const embed = new EmbedBuilder() .setColor('#0099ff') - .setTimestamp(); + .setTimestamp() + .setFooter({ + text: `BattleBot v${packageInfo.version} | VRBattles`, + iconURL: 'https://vrbattles.gg/favicon.ico' + }); switch (category) { case 'getting_started': @@ -292,8 +299,7 @@ class HelpCommand { await interaction.editReply({ embeds: [embed], - components: [backButton], - flags: ['Ephemeral'] + components: [backButton] }); } @@ -316,15 +322,255 @@ class HelpCommand { } } + async showMainHelpButton(interaction, setupStatus) { + const embed = new EmbedBuilder() + .setTitle('๐Ÿค– BattleBot Help Center') + .setDescription('Welcome to BattleBot! Your gateway to VRBattles data and notifications.') + .setColor(setupStatus.setupComplete ? '#00ff00' : '#ffaa00') + .setFooter({ + text: `BattleBot v${packageInfo.version} | VRBattles`, + iconURL: 'https://vrbattles.gg/favicon.ico' + }); + + // Add setup status section + const setupEmoji = setupStatus.setupComplete ? 'โœ…' : 'โš ๏ธ'; + const setupText = this.getSetupStatusText(setupStatus); + embed.addFields({ + name: `${setupEmoji} Server Setup Status`, + value: setupText, + inline: false + }); + + // Add quick start if not set up + if (!setupStatus.setupComplete) { + embed.addFields({ + name: '๐Ÿš€ Quick Start', + value: '1. `/register_server` - Register your server\n2. `/subscribe` - Subscribe to game notifications\n3. `/finduser` - Start searching players!', + inline: false + }); + } + + // Add help categories + embed.addFields({ + name: '๐Ÿ“š Help Categories', + value: '**๐Ÿ  Getting Started** - Setup and first steps\n' + + '**๐Ÿ” Search Commands** - Find players, teams, and matches\n' + + '**โš™๏ธ Admin & Setup** - Server configuration\n' + + '**๐Ÿ”” Notifications** - Match alerts and subscriptions\n' + + '**โ“ Troubleshooting** - Common issues and solutions', + inline: false + }); + + // Create action buttons + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('help_getting_started') + .setLabel('๐Ÿ  Getting Started') + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId('help_search') + .setLabel('๐Ÿ” Search Commands') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId('help_admin') + .setLabel('โš™๏ธ Admin & Setup') + .setStyle(ButtonStyle.Secondary) + ); + + const row2 = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('help_notifications') + .setLabel('๐Ÿ”” Notifications') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId('help_troubleshooting') + .setLabel('โ“ Troubleshooting') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setLabel('๐Ÿ“– Full Documentation') + .setStyle(ButtonStyle.Link) + .setURL('https://help.vrbattles.gg') + ); + + await interaction.update({ + embeds: [embed], + components: [row, row2] + }); + } + + async showCategoryHelpButton(interaction, category, setupStatus) { + const embed = new EmbedBuilder() + .setColor('#0099ff') + .setTimestamp() + .setFooter({ + text: `BattleBot v${packageInfo.version} | VRBattles`, + iconURL: 'https://vrbattles.gg/favicon.ico' + }); + + switch (category) { + case 'getting_started': + embed.setTitle('๐Ÿ  Getting Started with BattleBot') + .setDescription('Follow these steps to set up BattleBot in your server:') + .addFields( + { + name: '1๏ธโƒฃ Register Your Server', + value: '`/register_server` - This connects your Discord server to BattleBot\n' + + `Status: ${setupStatus.isRegistered ? 'โœ… Complete' : 'โŒ Not done'}`, + inline: false + }, + { + name: '2๏ธโƒฃ Subscribe to Games', + value: '`/subscribe` - Choose games and channels for notifications\n' + + `Status: ${setupStatus.subscriptionCount > 0 ? `โœ… ${setupStatus.subscriptionCount} games` : 'โŒ No subscriptions'}`, + inline: false + }, + { + name: '3๏ธโƒฃ Start Using Commands', + value: '`/finduser` - Search for players\n`/findteam` - Find teams\n`/matchhistory` - View match data', + inline: false + } + ); + break; + + case 'search': + embed.setTitle('๐Ÿ” Search Commands') + .setDescription('Find players, teams, and match data:') + .addFields( + { + name: '๐Ÿ‘ค `/finduser`', + value: 'Search for a player by username in specific games\n' + + 'โ€ข Shows stats, teams, and recent matches\n' + + 'โ€ข Requires game subscription to use', + inline: false + }, + { + name: '๐Ÿ‘ฅ `/findteam`', + value: 'Find teams by name in specific games\n' + + 'โ€ข Shows team stats and roster\n' + + 'โ€ข Displays win rates and match history', + inline: false + }, + { + name: '๐Ÿ“Š `/matchhistory`', + value: 'View detailed match history for any player\n' + + 'โ€ข Optional game filter\n' + + 'โ€ข Shows recent performance trends', + inline: false + } + ); + break; + + case 'admin': + embed.setTitle('โš™๏ธ Admin & Setup Commands') + .setDescription('Server management and configuration (Admin only):') + .addFields( + { + name: '๐Ÿ”ง `/register_server`', + value: 'Connect your Discord server to BattleBot\n' + + 'โ€ข Required before using other features\n' + + 'โ€ข Safe to run multiple times', + inline: false + }, + { + name: '๐Ÿ“‹ `/list_subscriptions`', + value: 'View all active game subscriptions\n' + + 'โ€ข Shows which games and channels\n' + + 'โ€ข Helps manage notifications', + inline: false + } + ); + break; + + case 'notifications': + embed.setTitle('๐Ÿ”” Notification Commands') + .setDescription('Manage game notifications and alerts:') + .addFields( + { + name: 'โž• `/subscribe`', + value: 'Subscribe to match notifications for specific games\n' + + 'โ€ข Choose game and notification channel\n' + + 'โ€ข Get alerts for new matches', + inline: false + }, + { + name: 'โž– `/unsubscribe`', + value: 'Remove game notification subscriptions\n' + + 'โ€ข Stop notifications for specific games\n' + + 'โ€ข Clean up unused subscriptions', + inline: false + }, + { + name: '๐ŸŽฎ Supported Games', + value: setupStatus.subscriptions.length > 0 + ? `**Your subscriptions:** ${setupStatus.subscriptions.join(', ')}` + : 'VAIL, Echo Arena, Nock, Breachers, Gun Raiders, and more!', + inline: false + } + ); + break; + + case 'troubleshooting': + embed.setTitle('โ“ Troubleshooting & Common Issues') + .setDescription('Solutions to common problems:') + .addFields( + { + name: 'โŒ "No game subscriptions found"', + value: '**Solution:** Use `/subscribe` to add games first\n' + + '**Why:** Search commands only work with subscribed games', + inline: false + }, + { + name: '๐Ÿ”’ "Missing permissions"', + value: '**Solution:** Ensure you have Administrator permissions\n' + + '**Commands affected:** `/register_server`, `/subscribe`, `/unsubscribe`', + inline: false + }, + { + name: '๐Ÿ” "User/Team not found"', + value: '**Solutions:**\nโ€ข Check spelling and exact username\nโ€ข Try different games\nโ€ข User might not have played recently', + inline: false + }, + { + name: 'โฐ Rate Limits', + value: '**What:** Commands have cooldowns to prevent spam\n' + + '**Wait times:** Search (3s), Admin (5s), Ping (1s)', + inline: false + } + ); + break; + } + + // Add back button + const backButton = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('help_back') + .setLabel('โฌ…๏ธ Back to Main Help') + .setStyle(ButtonStyle.Secondary) + ); + + await interaction.update({ + embeds: [embed], + components: [backButton] + }); + } + async handleHelpButton(interaction) { - const [action, category] = interaction.customId.split('_'); - - if (category === 'back') { - const setupStatus = await this.checkServerSetupStatus(interaction.guildId); - await this.showMainHelp(interaction, setupStatus); - } else { - const setupStatus = await this.checkServerSetupStatus(interaction.guildId); - await this.showCategoryHelp(interaction, category, setupStatus); + try { + const [action, category] = interaction.customId.split('_'); + + if (category === 'back') { + const setupStatus = await this.checkServerSetupStatus(interaction.guildId); + await this.showMainHelpButton(interaction, setupStatus); + } else { + const setupStatus = await this.checkServerSetupStatus(interaction.guildId); + await this.showCategoryHelpButton(interaction, category, setupStatus); + } + } catch (error) { + this.logger.error('Error in handleHelpButton:', error); + await interaction.reply({ + content: 'โŒ An error occurred while processing the help button.', + ephemeral: true + }); } } } diff --git a/src/scripts/generateInvite.js b/src/scripts/generateInvite.js index 6726b54..25c418c 100644 --- a/src/scripts/generateInvite.js +++ b/src/scripts/generateInvite.js @@ -16,46 +16,105 @@ async function generateInvite() { const invite = client.generateInvite({ scopes: ['bot', 'applications.commands'], permissions: [ + // Basic Discord permissions PermissionsBitField.Flags.ViewChannel, PermissionsBitField.Flags.SendMessages, + PermissionsBitField.Flags.SendMessagesInThreads, PermissionsBitField.Flags.EmbedLinks, PermissionsBitField.Flags.AttachFiles, + PermissionsBitField.Flags.ReadMessageHistory, + PermissionsBitField.Flags.UseExternalEmojis, + PermissionsBitField.Flags.AddReactions, + + // Application commands (slash commands) PermissionsBitField.Flags.UseApplicationCommands, + + // Message management for bot maintenance PermissionsBitField.Flags.ManageMessages, + + // Channel management for notifications setup + PermissionsBitField.Flags.ViewChannel, + PermissionsBitField.Flags.ManageChannels, + + // Advanced features + PermissionsBitField.Flags.CreatePublicThreads, + PermissionsBitField.Flags.CreatePrivateThreads, + PermissionsBitField.Flags.UseExternalStickers, + + // Voice permissions (if needed for future features) + PermissionsBitField.Flags.Connect, + PermissionsBitField.Flags.Speak, ] }); - console.log('\n=== Bot Invite Link Generator ==='); - console.log('\nCurrent Bot Status:'); + console.log('\n๐Ÿค– ===== VRBattles Discord Bot Invite Generator ===== ๐Ÿค–'); + console.log('\n๐Ÿ“Š Current Bot Status:'); console.log(`Name: ${client.user.tag}`); console.log(`ID: ${client.user.id}`); + console.log(`Created: ${client.user.createdAt.toDateString()}`); - console.log('\nCurrent Servers:'); + console.log('\n๐Ÿ  Current Servers:'); if (client.guilds.cache.size === 0) { - console.log('โŒ Bot is not in any servers'); + console.log('โŒ Bot is not in any servers yet'); } else { + console.log(`โœ… Bot is active in ${client.guilds.cache.size} server(s):`); client.guilds.cache.forEach(guild => { - console.log(`- ${guild.name} (ID: ${guild.id})`); + console.log(` ๐ŸŽฎ ${guild.name} (${guild.memberCount} members) - ID: ${guild.id}`); }); } - console.log('\n=== Invite Link ==='); - console.log('Use this link to invite the bot to your server:'); - console.log(invite); + console.log('\n๐Ÿ”— ===== Bot Invite Link ===== ๐Ÿ”—'); + console.log('Copy and share this link to invite VRBattles Bot to any Discord server:'); + console.log('\n' + invite + '\n'); - console.log('\n=== Next Steps ==='); - console.log('1. Click the link above'); - console.log('2. Select your server from the dropdown'); - console.log('3. Keep all permissions checked'); - console.log('4. Click "Authorize"'); - console.log('\nAfter adding the bot:'); - console.log('1. Enable Developer Mode in Discord (User Settings > App Settings > Advanced)'); - console.log('2. Right-click your notification channel'); - console.log('3. Click "Copy ID"'); - console.log('4. Update your .env file with the new channel ID'); + console.log('๐ŸŽฏ ===== Bot Features ===== ๐ŸŽฏ'); + console.log('โœ… Interactive Help System (/help)'); + console.log('โœ… Player Search (/finduser)'); + console.log('โœ… Team Search (/findteam) with pagination'); + console.log('โœ… Match History (/matchhistory)'); + console.log('โœ… Game Subscriptions (/subscribe, /unsubscribe)'); + console.log('โœ… Server Registration (/register_server)'); + console.log('โœ… Real-time Match Notifications'); + console.log('โœ… Dynamic Autocomplete'); + + console.log('\nโš™๏ธ ===== Permissions Included ===== โš™๏ธ'); + console.log('โœ… View Channels & Send Messages'); + console.log('โœ… Slash Commands Support'); + console.log('โœ… Embed Links & File Attachments'); + console.log('โœ… Message & Channel Management'); + console.log('โœ… Emoji & Reaction Support'); + console.log('โœ… Thread Management'); + console.log('โœ… Voice Channel Access (future features)'); + + console.log('\n๐Ÿš€ ===== Setup Instructions ===== ๐Ÿš€'); + console.log('1. ๐Ÿ”— Click the invite link above'); + console.log('2. ๐Ÿ  Select your Discord server from the dropdown'); + console.log('3. โœ… Keep ALL permissions checked (required for full functionality)'); + console.log('4. ๐ŸŽ‰ Click "Authorize" to add the bot'); + + console.log('\n๐Ÿ“ ===== After Adding the Bot ===== ๐Ÿ“'); + console.log('1. ๐Ÿ’ฌ Run /help to see all available commands'); + console.log('2. ๐Ÿ”ง Run /register_server to connect your server'); + console.log('3. ๐ŸŽฎ Run /subscribe to set up game notifications'); + console.log('4. ๐Ÿ” Try /finduser to search for players!'); + + console.log('\n๐Ÿ”ง ===== Admin Setup (Channel IDs) ===== ๐Ÿ”ง'); + console.log('To get Discord Channel IDs for notifications:'); + console.log('1. Enable Developer Mode: User Settings > App Settings > Advanced > Developer Mode'); + console.log('2. Right-click any text channel > Copy ID'); + console.log('3. Use the ID in /subscribe command'); + + console.log('\n๐Ÿ“š ===== Documentation ===== ๐Ÿ“š'); + console.log('Full documentation: https://help.vrbattles.gg'); + console.log('VRBattles website: https://www.vrbattles.gg'); + + console.log('\nโœจ VRBattles Discord Bot is ready to enhance your VR gaming community! โœจ\n'); } catch (error) { - console.error('Error:', error); + console.error('โŒ Error generating invite:', error); + if (error.code === 'TOKEN_INVALID') { + console.error('\n๐Ÿ”‘ Invalid bot token. Please check your .env file and ensure BOT_TOKEN is set correctly.'); + } } finally { client.destroy(); } diff --git a/src/scripts/syncGameChoices.js b/src/scripts/syncGameChoices.js new file mode 100644 index 0000000..4f5226c --- /dev/null +++ b/src/scripts/syncGameChoices.js @@ -0,0 +1,80 @@ +const { createClient } = require('@supabase/supabase-js'); +const fs = require('fs'); +const path = require('path'); + +async function syncGameChoices() { + try { + console.log('๐Ÿ”„ Syncing game choices from Supabase...'); + + // Initialize Supabase client + const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_KEY + ); + + // Fetch all active games + const { data: games, error } = await supabase + .from('games') + .select('id, name') + .eq('active', true) + .order('name'); + + if (error) { + console.error('โŒ Failed to fetch games:', error); + process.exit(1); + } + + console.log(`โœ… Found ${games.length} active games`); + + // Generate choices array + const choices = games.map(game => ({ + name: game.name, + value: game.name + })); + + // Generate the choices code + const choicesCode = choices + .map(choice => ` { name: "${choice.name}", value: "${choice.value}" }`) + .join(',\n'); + + console.log('\n๐Ÿ“‹ Generated choices:'); + choices.forEach(choice => console.log(` ๐ŸŽฎ ${choice.name}`)); + + // Read current deploy-commands.js + const deployPath = path.join(__dirname, '../../deploy-commands.js'); + let deployContent = fs.readFileSync(deployPath, 'utf8'); + + // Pattern to match the game choices in addChoices() + const choicesPattern = /\.addChoices\(\s*([^)]+)\s*\)/g; + + // Replace all instances with updated choices + const newChoicesBlock = `.addChoices( +${choicesCode} + )`; + + deployContent = deployContent.replace(choicesPattern, newChoicesBlock); + + // Write updated file + fs.writeFileSync(deployPath, deployContent); + + console.log('\nโœ… Updated deploy-commands.js with current game choices'); + console.log('๐Ÿš€ Run "node deploy-commands.js" to deploy the updated commands'); + + // Show diff + console.log('\n๐Ÿ“Š Summary:'); + console.log(` Database games: ${games.length}`); + console.log(` Generated choices: ${choices.length}`); + console.log(' Files updated: deploy-commands.js'); + + } catch (error) { + console.error('โŒ Error syncing game choices:', error); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + syncGameChoices(); +} + +module.exports = syncGameChoices; \ No newline at end of file diff --git a/src/scripts/testSupabaseConnection.js b/src/scripts/testSupabaseConnection.js new file mode 100644 index 0000000..0c91e12 --- /dev/null +++ b/src/scripts/testSupabaseConnection.js @@ -0,0 +1,134 @@ +require('dotenv').config(); +const { createClient } = require('@supabase/supabase-js'); + +async function testSupabaseConnection() { + console.log('๐Ÿ”ง Testing Supabase Connection...\n'); + + // Check environment variables + console.log('๐Ÿ“‹ Environment Variables:'); + console.log(`SUPABASE_URL: ${process.env.SUPABASE_URL ? 'โœ… Set' : 'โŒ Missing'}`); + console.log(`SUPABASE_KEY: ${process.env.SUPABASE_KEY ? 'โœ… Set' : 'โŒ Missing'}`); + + if (!process.env.SUPABASE_URL || !process.env.SUPABASE_KEY) { + console.log('\nโŒ Missing required environment variables'); + console.log('Make sure you have SUPABASE_URL and SUPABASE_KEY set in your .env file'); + return; + } + + console.log(`\nURL: ${process.env.SUPABASE_URL}`); + console.log(`Key: ${process.env.SUPABASE_KEY.substring(0, 20)}...\n`); + + try { + // Initialize Supabase client (same as in main bot) + const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_KEY + ); + + console.log('๐Ÿ“ก Testing basic connection...'); + + // Test 1: Check basic connection + const { data, error } = await supabase.from('games').select('count', { count: 'exact', head: true }); + if (error) { + console.log('โŒ Basic connection failed:', error.message); + return; + } + console.log('โœ… Basic connection successful'); + + // Test 2: Fetch all games + console.log('\n๐ŸŽฎ Testing games table...'); + const { data: games, error: gamesError } = await supabase + .from('games') + .select('id, name, active') + .order('name'); + + if (gamesError) { + console.log('โŒ Games query failed:', gamesError.message); + return; + } + + console.log(`โœ… Found ${games.length} total games:`); + games.forEach(game => { + console.log(` ${game.active ? 'โœ…' : 'โŒ'} ${game.name} (ID: ${game.id})`); + }); + + // Test 3: Fetch active games only + console.log('\n๐Ÿ”ฅ Testing active games filter...'); + const { data: activeGames, error: activeError } = await supabase + .from('games') + .select('id, name') + .eq('active', true) + .order('name'); + + if (activeError) { + console.log('โŒ Active games query failed:', activeError.message); + return; + } + + console.log(`โœ… Found ${activeGames.length} active games:`); + activeGames.forEach(game => { + console.log(` ๐ŸŽฏ ${game.name} (ID: ${game.id})`); + }); + + // Test 4: Test servers table + console.log('\n๐Ÿ  Testing servers table...'); + const { data: servers, error: serversError } = await supabase + .from('servers') + .select('id, discord_server_id, server_name, active') + .limit(5); + + if (serversError) { + console.log('โŒ Servers query failed:', serversError.message); + } else { + console.log(`โœ… Found ${servers.length} servers (showing first 5):`); + servers.forEach(server => { + console.log(` ${server.active ? 'โœ…' : 'โŒ'} ${server.server_name} (Discord ID: ${server.discord_server_id})`); + }); + } + + // Test 5: Test active_subscriptions view + console.log('\n๐Ÿ“‹ Testing active_subscriptions view...'); + const { data: subscriptions, error: subsError } = await supabase + .from('active_subscriptions') + .select('*') + .limit(5); + + if (subsError) { + console.log('โŒ Active subscriptions query failed:', subsError.message); + } else { + console.log(`โœ… Found ${subscriptions.length} active subscriptions (showing first 5):`); + subscriptions.forEach(sub => { + console.log(` ๐ŸŽฎ ${sub.game_name} โ†’ Server ${sub.discord_server_id} โ†’ Channel ${sub.notification_channel_id}`); + }); + } + + // Test 6: Test specific game lookup (simulate autocomplete) + console.log('\n๐Ÿ” Testing specific game lookup (autocomplete simulation)...'); + const testGameName = 'VAIL'; // Try to find a common game + const { data: specificGame, error: specificError } = await supabase + .from('games') + .select('id, name') + .eq('name', testGameName) + .eq('active', true) + .single(); + + if (specificError) { + console.log(`โŒ Specific game lookup failed for "${testGameName}":`, specificError.message); + } else { + console.log(`โœ… Found specific game: ${specificGame.name} (ID: ${specificGame.id})`); + } + + console.log('\n๐ŸŽ‰ All tests completed successfully!'); + console.log('\n๐Ÿ’ก If autocomplete still isn\'t working, check:'); + console.log(' 1. Environment variables are set correctly in Coolify'); + console.log(' 2. Bot has been restarted after fixing the code'); + console.log(' 3. Discord slash commands have been re-deployed'); + console.log(' 4. Try the /subscribe command to see if games appear there'); + + } catch (error) { + console.log('โŒ Unexpected error:', error.message); + console.log('Stack:', error.stack); + } +} + +testSupabaseConnection().catch(console.error); \ No newline at end of file diff --git a/src/services/NotificationService.js b/src/services/NotificationService.js index 913ddc7..a25979d 100644 --- a/src/services/NotificationService.js +++ b/src/services/NotificationService.js @@ -23,6 +23,15 @@ class NotificationService { console.log(`Notification service listening on port ${port}`); resolve(); }); + + this.server.on('error', (error) => { + if (error.code === 'EADDRINUSE') { + console.log(`Port ${port} is in use, trying port ${port + 1}`); + this.start(port + 1).then(resolve).catch(reject); + } else { + reject(error); + } + }); } catch (error) { reject(error); }