Compare commits

...

10 Commits

Author SHA1 Message Date
f999a938a5 Merges pull request #3
Main
2025-07-13 09:19:49 +00:00
VinceC
a8e836fd5b Add help system, deployment docs, and improve setup
Introduces a new interactive help command with button navigation, adds a detailed DEPLOYMENT.md guide, and improves server setup validation and error handling. Updates command registration to include all 9 games, adds version reporting, enhances Docker deployment with a multi-platform script, and removes local .env files from the repo. Also refactors bot startup for better diagnostics and graceful shutdown.
2025-07-13 04:00:39 -05:00
6b904d765d Merges pull request #2
Dev
2025-01-04 18:41:25 +00:00
VinceC
06742518d6 test (#9)
* Dev (#7)

* health check

* Update Dockerfile

* simplifying the deployment

* Dev (#8)

* health check

* Update Dockerfile

* simplifying the deployment

* Update Bot.js

makes the find team command public
2025-01-04 12:37:54 -06:00
VinceC
5b0dd201ca Update Bot.js
makes the find team command public
2025-01-04 12:36:08 -06:00
VinceC
f3b6625527 simplifying the deployment 2025-01-04 12:04:49 -06:00
VinceC
667dc6eddb Update Dockerfile 2025-01-04 11:57:50 -06:00
VinceC
8c7c3fc257 health check 2025-01-04 11:53:48 -06:00
VinceC
e86e5f2778 env update 2025-01-04 11:36:27 -06:00
cd5b0b580c Merges pull request #1
Dev
2025-01-04 17:34:47 +00:00
16 changed files with 1880 additions and 107 deletions

View File

@@ -1,2 +1,16 @@
node_modules node_modules
npm-debug.log npm-debug.log
.env*
.git
.gitignore
README.md
Dockerfile
.dockerignore
*.md
.vscode
.idea
coverage
.nyc_output
.cache
logs
*.log

View File

@@ -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
View 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

View File

@@ -14,9 +14,9 @@ RUN npm ci --only=production
# Bundle app source # Bundle app source
COPY . . COPY . .
# Create a non-root user # Create a non-root user with system UID
RUN addgroup --system --gid 1001 nodejs && \ RUN addgroup --system --gid 999 nodejs && \
adduser --system --uid 1001 --ingroup nodejs nodejs && \ adduser --system --uid 999 --ingroup nodejs --no-create-home nodejs && \
chown -R nodejs:nodejs /usr/src/app chown -R nodejs:nodejs /usr/src/app
USER nodejs USER nodejs
@@ -24,9 +24,9 @@ USER nodejs
# Your app binds to port 3000 # Your app binds to port 3000
EXPOSE 3000 EXPOSE 3000
# Add healthcheck # Add simple healthcheck
HEALTHCHECK --interval=30s --timeout=3s \ HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:3000/health || exit 1 CMD node -e "process.exit(0)"
# Define environment variable # Define environment variable
ENV NODE_ENV=production ENV NODE_ENV=production

View File

@@ -3,8 +3,28 @@ require("dotenv").config();
const commands = [ const commands = [
new SlashCommandBuilder() new SlashCommandBuilder()
.setName("ping") .setName("help")
.setDescription("Replies with Pong!"), .setDescription("Get help with BattleBot commands and features")
.addStringOption((option) =>
option
.setName("category")
.setDescription("Select a help category")
.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("version")
.setDescription("Show bot version and system information"),
new SlashCommandBuilder() new SlashCommandBuilder()
.setName("finduser") .setName("finduser")
.setDescription("Find a user by username") .setDescription("Find a user by username")
@@ -21,6 +41,7 @@ const commands = [
{ name: "Echo Combat", value: "Echo Combat" }, { name: "Echo Combat", value: "Echo Combat" },
{ name: "Gun Raiders", value: "Gun Raiders" }, { name: "Gun Raiders", value: "Gun Raiders" },
{ name: "Nock", value: "Nock" }, { name: "Nock", value: "Nock" },
{ name: "Orion Drift", value: "Orion Drift" },
{ name: "VAIL", value: "VAIL" } { name: "VAIL", value: "VAIL" }
) )
) )
@@ -40,7 +61,21 @@ const commands = [
.setRequired(true) .setRequired(true)
) )
.addStringOption((option) => .addStringOption((option) =>
option.setName("game").setDescription("Filter by game").setRequired(false) 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() new SlashCommandBuilder()
.setName("subscribe") .setName("subscribe")
@@ -59,6 +94,7 @@ const commands = [
{ name: "Echo Combat", value: "Echo Combat" }, { name: "Echo Combat", value: "Echo Combat" },
{ name: "Gun Raiders", value: "Gun Raiders" }, { name: "Gun Raiders", value: "Gun Raiders" },
{ name: "Nock", value: "Nock" }, { name: "Nock", value: "Nock" },
{ name: "Orion Drift", value: "Orion Drift" },
{ name: "VAIL", value: "VAIL" } { name: "VAIL", value: "VAIL" }
) )
) )
@@ -85,6 +121,7 @@ const commands = [
{ name: "Echo Combat", value: "Echo Combat" }, { name: "Echo Combat", value: "Echo Combat" },
{ name: "Gun Raiders", value: "Gun Raiders" }, { name: "Gun Raiders", value: "Gun Raiders" },
{ name: "Nock", value: "Nock" }, { name: "Nock", value: "Nock" },
{ name: "Orion Drift", value: "Orion Drift" },
{ name: "VAIL", value: "VAIL" } { name: "VAIL", value: "VAIL" }
) )
), ),
@@ -112,6 +149,7 @@ const commands = [
{ name: "Echo Combat", value: "Echo Combat" }, { name: "Echo Combat", value: "Echo Combat" },
{ name: "Gun Raiders", value: "Gun Raiders" }, { name: "Gun Raiders", value: "Gun Raiders" },
{ name: "Nock", value: "Nock" }, { name: "Nock", value: "Nock" },
{ name: "Orion Drift", value: "Orion Drift" },
{ name: "VAIL", value: "VAIL" } { name: "VAIL", value: "VAIL" }
) )
) )

145
index.js
View File

@@ -1,6 +1,7 @@
const winston = require('winston'); const winston = require('winston');
const { createClient } = require('@supabase/supabase-js'); const { createClient } = require('@supabase/supabase-js');
const Bot = require('./src/Bot'); const Bot = require('./src/Bot');
const packageInfo = require('./package.json');
require('dotenv').config(); require('dotenv').config();
// Initialize logger // Initialize logger
@@ -20,43 +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 // Initialize Supabase client
logger.info('🔗 Initializing Supabase connection...');
const supabase = createClient( const supabase = createClient(
process.env.SUPABASE_URL, process.env.SUPABASE_URL,
process.env.SUPABASE_KEY process.env.SUPABASE_KEY
); );
// Initialize bot variable at module level
let bot = null;
// Initialize and start bot // Initialize and start bot
const bot = new Bot(process.env.DISCORD_TOKEN, supabase, logger); async function startBot() {
bot.start().catch(error => { try {
console.error('Failed to start bot:', error); // Test Supabase connection
process.exit(1); 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 // Handle process termination
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
console.log('Received SIGINT. Shutting down...'); logger.info('📥 Received SIGINT. Shutting down gracefully...');
try { try {
await bot.stop(); if (bot) {
await bot.stop();
}
logger.info('✅ Bot shutdown completed');
process.exit(0); process.exit(0);
} catch (error) { } catch (error) {
console.error('Error during shutdown:', error); logger.error('Error during shutdown:', {
message: error.message,
stack: error.stack
});
process.exit(1); process.exit(1);
} }
}); });
process.on('SIGTERM', async () => { process.on('SIGTERM', async () => {
console.log('Received SIGTERM. Shutting down...'); logger.info('📥 Received SIGTERM. Shutting down gracefully...');
try { try {
await bot.stop(); if (bot) {
await bot.stop();
}
logger.info('✅ Bot shutdown completed');
process.exit(0); process.exit(0);
} catch (error) { } catch (error) {
console.error('Error during shutdown:', error); logger.error('Error during shutdown:', {
message: error.message,
stack: error.stack
});
process.exit(1); process.exit(1);
} }
});
// Add health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy' });
}); });

View File

@@ -1,18 +1,26 @@
{ {
"name": "discord-bot", "name": "vrbattles-discord-bot",
"version": "1.0.0", "version": "1.2.7",
"description": "VRBattles Discord Bot - Player search, team lookup, match notifications, and more for VR gaming communities",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node -r dotenv/config index.js dotenv_config_path=.env.production", "start": "node -r dotenv/config index.js",
"dev": "nodemon -r dotenv/config index.js dotenv_config_path=.env.development", "start:docker": "node index.js",
"deploy-commands": "node -r dotenv/config deploy-commands.js dotenv_config_path=.env.production", "dev": "nodemon -r dotenv/config index.js",
"deploy-commands:dev": "node -r dotenv/config deploy-commands.js dotenv_config_path=.env.development", "deploy-commands": "node -r dotenv/config deploy-commands.js",
"test:webhook": "node -r dotenv/config src/test/testWebhook.js dotenv_config_path=.env.test" "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": [], "keywords": ["discord", "bot", "vr", "gaming", "vrbattles", "vrchat", "vail"],
"author": "", "author": "VRBattles",
"license": "ISC", "license": "ISC",
"description": "", "repository": {
"type": "git",
"url": "https://github.com/your-repo/vrbattles-discord-bot"
},
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.46.1", "@supabase/supabase-js": "^2.46.1",
"axios": "^1.6.0", "axios": "^1.6.0",

93
scripts/deploy.sh Executable file
View 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}"

View File

@@ -57,6 +57,27 @@ class Bot {
} }
} }
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');
} catch (error) {
this.logger.error('Error stopping bot:', error);
throw error;
}
}
async handleInteraction(interaction) { async handleInteraction(interaction) {
if (!interaction.isCommand() && !interaction.isButton()) { if (!interaction.isCommand() && !interaction.isButton()) {
this.logger.debug('Ignoring non-command/button interaction'); this.logger.debug('Ignoring non-command/button interaction');
@@ -73,11 +94,31 @@ class Bot {
try { try {
if (interaction.isCommand()) { if (interaction.isCommand()) {
// Remove ephemeral flag for finduser and matchhistory commands // Quick defer with timeout handling
const isStatsCommand = ['finduser', 'matchhistory'].includes(interaction.commandName); const isPublicCommand = ['finduser', 'matchhistory', 'findteam'].includes(interaction.commandName);
await interaction.deferReply({
ephemeral: !isStatsCommand 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', { this.logger.debug('Processing command', {
command: interaction.commandName, command: interaction.commandName,
@@ -87,9 +128,12 @@ class Bot {
await this.commandHandler.handleCommand(interaction); await this.commandHandler.handleCommand(interaction);
} else if (interaction.isButton()) { } else if (interaction.isButton()) {
// For buttons, we'll still use deferUpdate to show the loading state try {
await interaction.deferUpdate(); await interaction.deferUpdate();
await this.commandHandler.handleButton(interaction); await this.commandHandler.handleButton(interaction);
} catch (error) {
this.logger.error('Button interaction error:', error);
}
} }
} catch (error) { } catch (error) {
this.logger.error('Command processing error:', { this.logger.error('Command processing error:', {
@@ -100,11 +144,15 @@ class Bot {
}); });
try { try {
if (!interaction.replied) { if (!interaction.replied && !interaction.deferred) {
await interaction.editReply({ await interaction.reply({
content: '❌ An error occurred while processing your request.', content: '❌ An error occurred while processing your request.',
ephemeral: true ephemeral: true
}); });
} else if (interaction.deferred) {
await interaction.editReply({
content: '❌ An error occurred while processing your request.'
});
} }
} catch (replyError) { } catch (replyError) {
this.logger.error('Failed to send error response:', replyError); this.logger.error('Failed to send error response:', replyError);

View File

@@ -1,5 +1,9 @@
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } = require('discord.js'); const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
const EmbedBuilders = require('../utils/embedBuilders'); 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 { class CommandHandler {
constructor(playerService, supabase, logger, serverRegistrationService, subscriptionCommands) { constructor(playerService, supabase, logger, serverRegistrationService, subscriptionCommands) {
@@ -8,20 +12,36 @@ class CommandHandler {
this.logger = logger; this.logger = logger;
this.serverRegistrationService = serverRegistrationService; this.serverRegistrationService = serverRegistrationService;
this.subscriptionCommands = subscriptionCommands; this.subscriptionCommands = subscriptionCommands;
this.helpCommand = new HelpCommand(supabase, logger);
this.paginationManager = new PaginationManager(logger);
this.cooldownManager = new CooldownManager();
} }
async handleCommand(interaction) { async handleCommand(interaction) {
try { try {
// 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) { switch (interaction.commandName) {
case 'register_server': case 'register_server':
await this.handleRegisterServer(interaction); await this.handleRegisterServer(interaction);
break; break;
case 'ping':
await interaction.editReply({ content: 'Pong!', ephemeral: true });
break;
case 'finduser': case 'finduser':
await this.handleFindUser(interaction); await this.handleFindUser(interaction);
break; break;
case 'findteam':
await this.handleFindTeam(interaction);
break;
case 'matchhistory': case 'matchhistory':
await this.handleMatchHistory(interaction); await this.handleMatchHistory(interaction);
break; break;
@@ -34,36 +54,113 @@ class CommandHandler {
case 'list_subscriptions': case 'list_subscriptions':
await this.subscriptionCommands.handleListSubscriptions(interaction); await this.subscriptionCommands.handleListSubscriptions(interaction);
break; break;
case 'findteam': case 'help':
await this.handleFindTeam(interaction); await this.helpCommand.handleHelp(interaction);
break;
case 'version':
await this.handleVersion(interaction);
break; break;
default: default:
await interaction.editReply({ await interaction.editReply({
content: '❌ Unknown command', content: '❌ Unknown command.',
ephemeral: true flags: ['Ephemeral']
}); });
} }
} catch (error) { } catch (error) {
this.logger.error('Command handling error:', { this.logger.error('Error in handleCommand:', {
command: interaction.commandName,
error: error.message, error: error.message,
stack: error.stack commandName: interaction.commandName,
userId: interaction.user.id,
guildId: interaction.guildId,
timestamp: new Date().toISOString()
}); });
try { try {
await interaction.editReply({ if (interaction.replied || interaction.deferred) {
content: '❌ An error occurred while processing your command.', await interaction.editReply({
ephemeral: true content: '❌ An error occurred while processing your command.',
}); });
} catch (followUpError) { } else {
this.logger.error('Failed to send error response:', { await interaction.reply({
error: followUpError.message, content: '❌ An error occurred while processing your command.',
originalError: error.message flags: ['Ephemeral']
}); });
}
} catch (replyError) {
this.logger.error('Failed to send error response:', replyError);
} }
} }
} }
async getServerSubscriptions(guildId) {
try {
const { data: subscriptions, error } = await this.supabase
.from('active_subscriptions')
.select('game_name')
.eq('discord_server_id', guildId);
if (error) {
this.logger.error('Error fetching server subscriptions:', error);
return [];
}
return subscriptions || [];
} catch (error) {
this.logger.error('Error in getServerSubscriptions:', error);
return [];
}
}
async checkSetupStatusWithGuidance(guildId) {
try {
// Check if server is registered
const { data: server, error: serverError } = await this.supabase
.from('servers')
.select('*')
.eq('discord_server_id', guildId)
.single();
const isRegistered = !!server && !serverError;
// Check subscriptions if registered
let subscriptions = [];
if (isRegistered) {
const { data: subs, error: subError } = await this.supabase
.from('active_subscriptions')
.select('game_name')
.eq('discord_server_id', guildId);
if (!subError) {
subscriptions = subs || [];
}
}
return {
isRegistered,
subscriptionCount: subscriptions.length,
setupComplete: isRegistered && subscriptions.length > 0,
getGuidanceMessage() {
if (!isRegistered) {
return "First, run `/register_server` to connect your server to VRBattles.";
} else if (subscriptions.length === 0) {
return "Next, run `/subscribe` to add game notifications.";
}
return "Your server is properly set up!";
}
};
} catch (error) {
this.logger.error('Error checking setup status:', error);
return {
isRegistered: false,
subscriptionCount: 0,
setupComplete: false,
getGuidanceMessage() {
return "Please check your server setup and try again.";
}
};
}
}
async handleRegisterServer(interaction) { async handleRegisterServer(interaction) {
try { try {
// Check for admin permissions // Check for admin permissions
@@ -89,12 +186,37 @@ class CommandHandler {
// Log the result // Log the result
this.logger.debug('Server registration result received', { this.logger.debug('Server registration result received', {
status: result.status, status: result?.status,
serverId: result.server?.id, hasServer: !!result?.server,
serverId: result?.server?.id,
guildId, guildId,
timestamp: new Date().toISOString() 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 // Prepare response message based on status
let message; let message;
switch (result.status) { switch (result.status) {
@@ -120,7 +242,7 @@ class CommandHandler {
this.logger.info('Server registration completed', { this.logger.info('Server registration completed', {
status: result.status, status: result.status,
guildId, guildId,
serverId: result.server.id, serverId: result.server?.id,
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
@@ -151,10 +273,10 @@ class CommandHandler {
const username = interaction.options.getString('username'); const username = interaction.options.getString('username');
const gameFilter = interaction.options.getString('game'); const gameFilter = interaction.options.getString('game');
// Input validation // Input validation with helpful messages
if (!username || typeof username !== 'string') { if (!username || typeof username !== 'string') {
await interaction.editReply({ await interaction.editReply({
content: '❌ Invalid username provided.' 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; return;
} }
@@ -167,7 +289,7 @@ class CommandHandler {
if (!sanitizedUsername) { if (!sanitizedUsername) {
await interaction.editReply({ await interaction.editReply({
content: '❌ Username must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).' content: '❌ **Invalid characters in username.**\n\n✅ **Allowed characters:** Letters, numbers, spaces, hyphens (-), underscores (_), and periods (.)\n🔍 **Example:** Try "PlayerName123" or "Player_Name"'
}); });
return; return;
} }
@@ -385,6 +507,145 @@ class CommandHandler {
} }
} }
async handleButton(interaction) {
try {
// Check if this is a pagination button
if (this.paginationManager.isPaginationButton(interaction.customId)) {
await interaction.deferUpdate();
await this.paginationManager.handlePaginationButton(interaction);
return;
}
// Check if this is a help button
if (interaction.customId.startsWith('help_')) {
await this.helpCommand.handleHelpButton(interaction);
return;
}
// Handle other button interactions here as needed
this.logger.warn('Unhandled button interaction:', {
customId: interaction.customId,
userId: interaction.user.id
});
await interaction.reply({
content: '❌ This button interaction is not implemented yet.',
ephemeral: true
});
} catch (error) {
this.logger.error('Error in handleButton:', {
error: error.message,
customId: interaction.customId,
userId: interaction.user.id
});
try {
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: '❌ An error occurred while processing the button interaction.',
ephemeral: true
});
}
} catch (replyError) {
this.logger.error('Failed to send button error response:', replyError);
}
}
}
async handleSelectMenu(interaction) {
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) { createActionRow(username) {
return new ActionRowBuilder().addComponents( return new ActionRowBuilder().addComponents(
new ButtonBuilder() new ButtonBuilder()

578
src/commands/HelpCommand.js Normal file
View File

@@ -0,0 +1,578 @@
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const packageInfo = require('../../package.json');
class HelpCommand {
constructor(supabase, logger) {
this.supabase = supabase;
this.logger = logger;
}
async handleHelp(interaction) {
try {
const category = interaction.options.getString('category');
const guildId = interaction.guildId;
// Check server setup status
const setupStatus = await this.checkServerSetupStatus(guildId);
if (!category) {
// Show main help menu with setup status
await this.showMainHelp(interaction, setupStatus);
} else {
// Show specific category help
await this.showCategoryHelp(interaction, category, setupStatus);
}
} catch (error) {
this.logger.error('Error in handleHelp:', error);
await interaction.editReply({
content: '❌ An error occurred while loading help. Please try again.'
});
}
}
async checkServerSetupStatus(guildId) {
try {
// Check if server is registered
const { data: server, error: serverError } = await this.supabase
.from('servers')
.select('*')
.eq('discord_server_id', guildId)
.single();
const isRegistered = !!server && !serverError;
// Check subscriptions if registered
let subscriptions = [];
if (isRegistered) {
const { data: subs, error: subError } = await this.supabase
.from('active_subscriptions')
.select('game_name')
.eq('discord_server_id', guildId);
if (!subError) {
subscriptions = subs || [];
}
}
return {
isRegistered,
subscriptionCount: subscriptions.length,
subscriptions: subscriptions.map(s => s.game_name),
setupComplete: isRegistered && subscriptions.length > 0
};
} catch (error) {
this.logger.error('Error checking setup status:', error);
return {
isRegistered: false,
subscriptionCount: 0,
subscriptions: [],
setupComplete: false
};
}
}
async showMainHelp(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.editReply({
embeds: [embed],
components: [row, row2]
});
}
async showCategoryHelp(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.editReply({
embeds: [embed],
components: [backButton]
});
}
getSetupStatusText(setupStatus) {
if (setupStatus.setupComplete) {
return `**✅ Setup Complete!**\n` +
`• Server: Registered\n` +
`• Games: ${setupStatus.subscriptionCount} subscribed\n` +
`• Ready to use all commands!`;
} else if (setupStatus.isRegistered) {
return `**⚠️ Partial Setup**\n` +
`• Server: ✅ Registered\n` +
`• Games: ❌ No subscriptions\n` +
`• Next: Use \`/subscribe\` to add games`;
} else {
return `**❌ Setup Required**\n` +
`• Server: ❌ Not registered\n` +
`• Games: ❌ No subscriptions\n` +
`• Next: Use \`/register_server\` to start`;
}
}
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.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
});
}
}
}
module.exports = HelpCommand;

View File

@@ -16,46 +16,105 @@ async function generateInvite() {
const invite = client.generateInvite({ const invite = client.generateInvite({
scopes: ['bot', 'applications.commands'], scopes: ['bot', 'applications.commands'],
permissions: [ permissions: [
// Basic Discord permissions
PermissionsBitField.Flags.ViewChannel, PermissionsBitField.Flags.ViewChannel,
PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.SendMessagesInThreads,
PermissionsBitField.Flags.EmbedLinks, PermissionsBitField.Flags.EmbedLinks,
PermissionsBitField.Flags.AttachFiles, PermissionsBitField.Flags.AttachFiles,
PermissionsBitField.Flags.ReadMessageHistory,
PermissionsBitField.Flags.UseExternalEmojis,
PermissionsBitField.Flags.AddReactions,
// Application commands (slash commands)
PermissionsBitField.Flags.UseApplicationCommands, PermissionsBitField.Flags.UseApplicationCommands,
// Message management for bot maintenance
PermissionsBitField.Flags.ManageMessages, 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('\n🤖 ===== VRBattles Discord Bot Invite Generator ===== 🤖');
console.log('\nCurrent Bot Status:'); console.log('\n📊 Current Bot Status:');
console.log(`Name: ${client.user.tag}`); console.log(`Name: ${client.user.tag}`);
console.log(`ID: ${client.user.id}`); 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) { if (client.guilds.cache.size === 0) {
console.log('❌ Bot is not in any servers'); console.log('❌ Bot is not in any servers yet');
} else { } else {
console.log(`✅ Bot is active in ${client.guilds.cache.size} server(s):`);
client.guilds.cache.forEach(guild => { 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('\n🔗 ===== Bot Invite Link ===== 🔗');
console.log('Use this link to invite the bot to your server:'); console.log('Copy and share this link to invite VRBattles Bot to any Discord server:');
console.log(invite); console.log('\n' + invite + '\n');
console.log('\n=== Next Steps ==='); console.log('🎯 ===== Bot Features ===== 🎯');
console.log('1. Click the link above'); console.log('✅ Interactive Help System (/help)');
console.log('2. Select your server from the dropdown'); console.log('✅ Player Search (/finduser)');
console.log('3. Keep all permissions checked'); console.log('✅ Team Search (/findteam) with pagination');
console.log('4. Click "Authorize"'); console.log('✅ Match History (/matchhistory)');
console.log('\nAfter adding the bot:'); console.log('✅ Game Subscriptions (/subscribe, /unsubscribe)');
console.log('1. Enable Developer Mode in Discord (User Settings > App Settings > Advanced)'); console.log('✅ Server Registration (/register_server)');
console.log('2. Right-click your notification channel'); console.log('✅ Real-time Match Notifications');
console.log('3. Click "Copy ID"'); console.log('✅ Dynamic Autocomplete');
console.log('4. Update your .env file with the new channel ID');
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) { } 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 { } finally {
client.destroy(); client.destroy();
} }

View 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;

View 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);

View File

@@ -23,6 +23,15 @@ class NotificationService {
console.log(`Notification service listening on port ${port}`); console.log(`Notification service listening on port ${port}`);
resolve(); 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) { } catch (error) {
reject(error); reject(error);
} }

View File

@@ -0,0 +1,205 @@
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } = require('discord.js');
class PaginationManager {
constructor(logger) {
this.logger = logger;
this.activePaginations = new Map(); // Store active paginations by interaction id
}
createPaginatedResponse(items, itemsPerPage = 5, embedBuilder, title = 'Results') {
const totalPages = Math.ceil(items.length / itemsPerPage);
const pages = [];
for (let i = 0; i < totalPages; i++) {
const startIdx = i * itemsPerPage;
const pageItems = items.slice(startIdx, startIdx + itemsPerPage);
const embed = embedBuilder(pageItems, i + 1, totalPages);
pages.push(embed);
}
return {
pages,
totalPages,
currentPage: 0,
totalItems: items.length
};
}
createNavigationButtons(currentPage, totalPages, customId) {
const row = new ActionRowBuilder();
// First page button
row.addComponents(
new ButtonBuilder()
.setCustomId(`${customId}_first`)
.setLabel('⏮️ First')
.setStyle(ButtonStyle.Secondary)
.setDisabled(currentPage === 0)
);
// Previous page button
row.addComponents(
new ButtonBuilder()
.setCustomId(`${customId}_prev`)
.setLabel('◀️ Previous')
.setStyle(ButtonStyle.Primary)
.setDisabled(currentPage === 0)
);
// Page indicator
row.addComponents(
new ButtonBuilder()
.setCustomId(`${customId}_info`)
.setLabel(`${currentPage + 1} / ${totalPages}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true)
);
// Next page button
row.addComponents(
new ButtonBuilder()
.setCustomId(`${customId}_next`)
.setLabel('Next ▶️')
.setStyle(ButtonStyle.Primary)
.setDisabled(currentPage >= totalPages - 1)
);
// Last page button
row.addComponents(
new ButtonBuilder()
.setCustomId(`${customId}_last`)
.setLabel('Last ⏭️')
.setStyle(ButtonStyle.Secondary)
.setDisabled(currentPage >= totalPages - 1)
);
return row;
}
async sendPaginatedMessage(interaction, paginatedData, customId) {
if (paginatedData.totalPages === 0) {
await interaction.editReply({
content: '❌ No results found.',
components: []
});
return;
}
if (paginatedData.totalPages === 1) {
// No pagination needed for single page
await interaction.editReply({
embeds: [paginatedData.pages[0]],
components: []
});
return;
}
// Store pagination data
this.activePaginations.set(interaction.id, {
...paginatedData,
userId: interaction.user.id,
expiresAt: Date.now() + (15 * 60 * 1000) // 15 minutes
});
const navigationRow = this.createNavigationButtons(
paginatedData.currentPage,
paginatedData.totalPages,
customId
);
await interaction.editReply({
embeds: [paginatedData.pages[paginatedData.currentPage]],
components: [navigationRow]
});
// Clean up expired paginations
this.cleanupExpiredPaginations();
}
async handlePaginationButton(interaction) {
try {
const [baseId, action] = interaction.customId.split('_');
const paginationData = this.activePaginations.get(interaction.message.interaction?.id);
if (!paginationData) {
await interaction.reply({
content: '❌ This pagination has expired. Please run the command again.',
flags: ['Ephemeral']
});
return;
}
// Check if user is the one who initiated the command
if (paginationData.userId !== interaction.user.id) {
await interaction.reply({
content: '❌ Only the user who ran the command can navigate the results.',
flags: ['Ephemeral']
});
return;
}
let newPage = paginationData.currentPage;
switch (action) {
case 'first':
newPage = 0;
break;
case 'prev':
newPage = Math.max(0, paginationData.currentPage - 1);
break;
case 'next':
newPage = Math.min(paginationData.totalPages - 1, paginationData.currentPage + 1);
break;
case 'last':
newPage = paginationData.totalPages - 1;
break;
case 'info':
// Info button shouldn't do anything
await interaction.deferUpdate();
return;
}
// Update pagination data
paginationData.currentPage = newPage;
this.activePaginations.set(interaction.message.interaction?.id, paginationData);
// Update the message
const navigationRow = this.createNavigationButtons(
newPage,
paginationData.totalPages,
baseId
);
await interaction.update({
embeds: [paginationData.pages[newPage]],
components: [navigationRow]
});
} catch (error) {
this.logger.error('Error handling pagination button:', error);
await interaction.reply({
content: '❌ An error occurred while navigating. Please try again.',
flags: ['Ephemeral']
});
}
}
cleanupExpiredPaginations() {
const now = Date.now();
for (const [key, data] of this.activePaginations.entries()) {
if (data.expiresAt < now) {
this.activePaginations.delete(key);
}
}
}
isPaginationButton(customId) {
return customId.includes('_first') ||
customId.includes('_prev') ||
customId.includes('_next') ||
customId.includes('_last') ||
customId.includes('_info');
}
}
module.exports = PaginationManager;