Merge branch 'main' into git-main
This commit is contained in:
@@ -1,2 +1,16 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.env*
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
*.md
|
||||
.vscode
|
||||
.idea
|
||||
coverage
|
||||
.nyc_output
|
||||
.cache
|
||||
logs
|
||||
*.log
|
||||
16
.env.example
16
.env.example
@@ -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
|
||||
149
DEPLOYMENT.md
Normal file
149
DEPLOYMENT.md
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
161
index.js
161
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 {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
});
|
||||
26
package.json
26
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",
|
||||
|
||||
93
scripts/deploy.sh
Executable file
93
scripts/deploy.sh
Executable file
@@ -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}"
|
||||
176
src/Bot.js
176
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 {
|
||||
this.logger.info('Stopping bot...');
|
||||
|
||||
async stop() {
|
||||
try {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
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 {
|
||||
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']
|
||||
});
|
||||
} 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"
|
||||
});
|
||||
}
|
||||
|
||||
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,32 +153,21 @@ 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']
|
||||
});
|
||||
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']
|
||||
content: '❌ You need administrator permissions to register this server.',
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -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;
|
||||
const embeds = EmbedBuilders.createTeamEmbed(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'
|
||||
);
|
||||
|
||||
} 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,12 +554,106 @@ class CommandHandler {
|
||||
}
|
||||
|
||||
async handleSelectMenu(interaction) {
|
||||
// Handle select menu interactions (placeholder for future implementation)
|
||||
this.logger.debug('Select menu interaction received:', {
|
||||
try {
|
||||
// Handle select menu interactions (if needed)
|
||||
this.logger.debug('Select menu interaction:', {
|
||||
customId: interaction.customId,
|
||||
values: interaction.values
|
||||
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}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CommandHandler;
|
||||
@@ -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) {
|
||||
try {
|
||||
const [action, category] = interaction.customId.split('_');
|
||||
|
||||
if (category === 'back') {
|
||||
const setupStatus = await this.checkServerSetupStatus(interaction.guildId);
|
||||
await this.showMainHelp(interaction, setupStatus);
|
||||
await this.showMainHelpButton(interaction, setupStatus);
|
||||
} else {
|
||||
const setupStatus = await this.checkServerSetupStatus(interaction.guildId);
|
||||
await this.showCategoryHelp(interaction, category, setupStatus);
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
80
src/scripts/syncGameChoices.js
Normal file
80
src/scripts/syncGameChoices.js
Normal file
@@ -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;
|
||||
134
src/scripts/testSupabaseConnection.js
Normal file
134
src/scripts/testSupabaseConnection.js
Normal file
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user