Compare commits

..

12 Commits

Author SHA1 Message Date
VinceC
d0dc7bf4ef Merge git-main into main: v1.2.11 tournament features 2025-10-02 06:32:02 -05:00
VinceC
dd8aa456d9 v1.2.11: Add tournaments command with filtering and improve error handling
Features:
- Add /tournaments command with status and game filters
- Add tournament API integration to PlayerService
- Add beautiful tournament embed builders with prize pools
- Protect sync-games from overwriting manual customizations
- Improve error handling with proper MessageFlags

Changes:
- Add getTournamentsData() and getTournamentData() to PlayerService
- Add tournament command handler with pagination support
- Add tournament embed builders with rich formatting
- Update deployment docs for v1.2.10 features
- Add issue tracker documentation
- Add tournament testing script
- Fix ephemeral flags throughout CommandHandler
- Improve permission checks with PermissionFlagsBits

Version: 1.2.11
2025-10-02 06:24:54 -05:00
VinceC
546127f91c Merge branch 'main' into git-main 2025-07-13 04:26:54 -05: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
18 changed files with 1797 additions and 510 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

172
DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,172 @@
# 🚀 VRBattles Discord Bot - Deployment Guide
## 📦 **Latest Version: v1.2.10**
### **What's New in v1.2.10:**
- ✅ MAJOR FIX: Tournament status filter now works correctly and permanently
- ✅ Fixed sync-games script overwriting tournament status choices
- ✅ Tournament status uses actual API values (Published, In-progress, Ended)
- ✅ Excluded Unpublished status (admin-only tournaments)
- ✅ Protected manual choice customizations from future overwrites
- ✅ Added emojis to status filter choices for better UX
### **Previous in v1.2.9:**
- ✅ FIXED: Tournament status filter now works correctly
- ✅ Tournament status filter uses proper values (Published, In-progress, Ended)
- ✅ Removed Unpublished status from filter options
- ✅ Added emojis to status filter choices for better UX
### **Previous in v1.2.8:**
- ✅ NEW: `/tournaments` command with beautiful formatting
- ✅ Filter tournaments by status (Published, In-progress, Ended, Unpublished)
- ✅ Filter tournaments by game (VAIL, Echo Arena, Blacktop Hoops, etc.)
- ✅ Smart date formatting with relative times
- ✅ Rich embeds with prize pools, entry fees, and team sizes
- ✅ Enhanced PlayerService with tournaments API integration
### **Previous 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.10 (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)
- `/tournaments` - **NEW!** View all tournaments with filters
- `/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

@@ -2,25 +2,29 @@ const { REST, Routes, SlashCommandBuilder, PermissionFlagsBits } = require("disc
require("dotenv").config(); require("dotenv").config();
const commands = [ const commands = [
new SlashCommandBuilder()
.setName("ping")
.setDescription("Replies with Pong!"),
new SlashCommandBuilder() new SlashCommandBuilder()
.setName("help") .setName("help")
.setDescription("Get help with BattleBot commands and features") .setDescription("Get help with BattleBot commands and features")
.addStringOption((option) => .addStringOption((option) =>
option option
.setName("category") .setName("category")
.setDescription("Choose a help category") .setDescription("Select a help category")
.setRequired(false) .setRequired(false)
.addChoices( .addChoices(
{ name: "🏠 Getting Started", value: "getting_started" }, { name: "Big Ballers VR", value: "Big Ballers VR" },
{ name: "🔍 Search Commands", value: "search" }, { name: "Blacktop Hoops", value: "Blacktop Hoops" },
{ name: "⚙️ Admin & Setup", value: "admin" }, { name: "Breachers", value: "Breachers" },
{ name: "🔔 Notifications", value: "notifications" }, { name: "Echo Arena", value: "Echo Arena" },
{ name: "❓ Troubleshooting", value: "troubleshooting" } { 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")
@@ -29,7 +33,17 @@ const commands = [
.setName("game") .setName("game")
.setDescription("Select the game") .setDescription("Select the game")
.setRequired(true) .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) => .addStringOption((option) =>
option option
@@ -47,7 +61,21 @@ const commands = [
.setRequired(true) .setRequired(true)
) )
.addStringOption((option) => .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() new SlashCommandBuilder()
.setName("subscribe") .setName("subscribe")
@@ -58,7 +86,17 @@ const commands = [
.setName("game") .setName("game")
.setDescription("Game to subscribe to") .setDescription("Game to subscribe to")
.setRequired(true) .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) => .addChannelOption((option) =>
option option
@@ -75,7 +113,17 @@ const commands = [
.setName("game") .setName("game")
.setDescription("Game to unsubscribe from") .setDescription("Game to unsubscribe from")
.setRequired(true) .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() new SlashCommandBuilder()
.setName("register_server") .setName("register_server")
@@ -93,7 +141,17 @@ const commands = [
.setName("game") .setName("game")
.setDescription("Select the game") .setDescription("Select the game")
.setRequired(true) .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) => .addStringOption((option) =>
option option
@@ -101,6 +159,37 @@ const commands = [
.setDescription("The team name to search for") .setDescription("The team name to search for")
.setRequired(true) .setRequired(true)
), ),
new SlashCommandBuilder()
.setName("tournaments")
.setDescription("View all available tournaments")
.addStringOption((option) =>
option
.setName("status")
.setDescription("Filter tournaments by status (optional)")
.setRequired(false)
.addChoices(
{ name: "📢 Published", value: "Published" },
{ name: "⚡ In-progress", value: "In-progress" },
{ name: "🏁 Ended", value: "Ended" }
)
)
.addStringOption((option) =>
option
.setName("game")
.setDescription("Filter tournaments 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" }
)
),
]; ];
const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN);

49
docs/issue-tracker.md Normal file
View File

@@ -0,0 +1,49 @@
# VRBattles Discord Bot Issue Tracker
Use this checklist to track code-quality fixes and consistency updates. Check items off as completed and commit this file with your changes.
## How to use
- Edit this file, change "[ ]" to "[x]" for completed items, and include a short note or PR link if helpful.
## Checklist
- [ ] Replace admin permission check with PermissionFlagsBits.Administrator
- Location: `src/commands/CommandHandler.js` (in `handleRegisterServer`)
- Desired: `interaction.member.permissions.has(PermissionFlagsBits.Administrator)`
- [ ] Replace legacy `flags` ephemeral usage with `ephemeral: true`
- Location: various replies in `src/commands/CommandHandler.js` and `src/commands/SubscriptionCommands.js`
- Notes: decide ephemerality at `reply`/`deferReply`; it cannot be changed via `editReply`
- [ ] Switch `interaction.isCommand()` to `interaction.isChatInputCommand()`
- Location: `src/Bot.js` (in `handleInteraction`)
- [ ] Use `PermissionFlagsBits` for channel permission checks
- Location: `src/services/NotificationService.js`
- Replace string permissions (e.g., 'SendMessages', 'ViewChannel') with `PermissionFlagsBits.*`
- [ ] Clean up `SubscriptionCommands` duplicates and constructor mismatch
- Location: `src/commands/SubscriptionCommands.js`
- Remove duplicated methods (`getGame`, `createOrUpdateSubscription`, `safeReply`), fix constructor to accept `(supabase, logger)` and use the injected logger
- [ ] Fix `PlayerService` logging
- Location: `src/services/PlayerService.js`
- Inject a logger or use a consistent logging strategy; remove `this.logger` references if not injected
- [ ] Align `/help` category choices with handler expectations
- Locations:
- `deploy-commands.js` (slash command choices for `help`)
- `src/commands/HelpCommand.js` (expects categories like `getting_started`, `search`, etc.)
- Decide whether to offer topic categories or game names and make both sides consistent
- [ ] Add guild-scoped slash command deployment for development
- Location: `deploy-commands.js` (or add a new `deploy-commands.dev.js`)
- Use `Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID)` for fast iteration in dev
- [ ] Standardize interaction handlers and checks
- Ensure consistent use of `isChatInputCommand`, `isButton`, and ephemerality decisions across commands and buttons
## Notes
- Keep PRs small and focused (one checklist item per PR when possible).
- Update this file with any extra context, follow-ups, or links.

167
index.js
View File

@@ -1,15 +1,9 @@
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();
// 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 // Initialize logger
const logger = winston.createLogger({ const logger = winston.createLogger({
level: 'debug', 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 // 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 // Initialize bot variable at module level
const bot = new Bot(process.env.DISCORD_TOKEN, supabase, logger); let bot = null;
// 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 and start bot // Initialize and start bot
bot.start().catch(error => { async function startBot() {
console.error('Failed to start bot:', error); try {
process.exit(1); // 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 // 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);
} }
}); });

View File

@@ -1,22 +1,26 @@
{ {
"name": "discord-bot", "name": "vrbattles-discord-bot",
"version": "1.0.0", "version": "1.2.11",
"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", "start": "node -r dotenv/config index.js",
"start:docker": "node index.js",
"dev": "nodemon -r dotenv/config index.js", "dev": "nodemon -r dotenv/config index.js",
"deploy-commands": "node -r dotenv/config deploy-commands.js", "deploy-commands": "node -r dotenv/config deploy-commands.js",
"deploy-commands:dev": "node -r dotenv/config deploy-commands.js", "sync-games": "node -r dotenv/config src/scripts/syncGameChoices.js && node -r dotenv/config deploy-commands.js",
"test:webhook": "node -r dotenv/config src/tests/testWebhook.js", "deploy": "./scripts/deploy.sh",
"test:connection": "node -r dotenv/config src/tests/testConnection.js", "test:webhook": "node -r dotenv/config src/test/testWebhook.js",
"test:supabase": "node -r dotenv/config src/tests/testSupabase.js", "test:supabase": "node src/scripts/testSupabaseConnection.js",
"seed:games": "node -r dotenv/config src/scripts/seedGames.js", "generate:invite": "node src/scripts/generateInvite.js"
"verify:setup": "node -r dotenv/config src/scripts/verifysetup.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",

94
scripts/deploy.sh Executable file
View File

@@ -0,0 +1,94 @@
#!/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 " ✅ NEW: /tournaments command with filtering"
echo -e " ✅ Tournament status & game filters"
echo -e " ✅ Beautiful tournament embeds with prizes"
echo -e " ✅ All 9 games in dropdowns"
echo -e " ✅ Working help button navigation"
echo -e " ✅ Automatic Supabase game sync"
echo ""
echo -e "${GREEN}🎉 Deployment complete!${NC}"

View File

@@ -1,4 +1,4 @@
const { Client, GatewayIntentBits } = require('discord.js'); const { Client, GatewayIntentBits, MessageFlags } = require('discord.js');
const CommandHandler = require('./commands/CommandHandler'); const CommandHandler = require('./commands/CommandHandler');
const SubscriptionCommands = require('./commands/SubscriptionCommands'); const SubscriptionCommands = require('./commands/SubscriptionCommands');
const PlayerService = require('./services/PlayerService'); const PlayerService = require('./services/PlayerService');
@@ -6,7 +6,6 @@ const SupabaseService = require('./services/SupabaseService');
const NotificationService = require('./services/NotificationService'); const NotificationService = require('./services/NotificationService');
const Logger = require('./utils/Logger'); const Logger = require('./utils/Logger');
const ServerRegistrationService = require('./services/ServerRegistrationService'); const ServerRegistrationService = require('./services/ServerRegistrationService');
const CooldownManager = require('./utils/CooldownManager');
class Bot { class Bot {
constructor(token, supabase, logger) { constructor(token, supabase, logger) {
@@ -25,7 +24,6 @@ class Bot {
this.serverRegistrationService = new ServerRegistrationService(supabase, logger); this.serverRegistrationService = new ServerRegistrationService(supabase, logger);
this.subscriptionCommands = new SubscriptionCommands(supabase, logger); this.subscriptionCommands = new SubscriptionCommands(supabase, logger);
this.notificationService = new NotificationService(this, supabase); this.notificationService = new NotificationService(this, supabase);
this.cooldownManager = new CooldownManager();
// Initialize command handlers // Initialize command handlers
this.commandHandler = new CommandHandler( this.commandHandler = new CommandHandler(
@@ -39,82 +37,15 @@ class Bot {
// Setup event handlers // Setup event handlers
this.client.on('ready', () => { this.client.on('ready', () => {
this.logger.info(`Logged in as ${this.client.user.tag}`); this.logger.info(`Logged in as ${this.client.user.tag}`);
// NotificationService is now handled by the main Express server in index.js // Start notification service after bot is ready
this.logger.info('Bot ready - notification service integrated with main server'); 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) => { this.client.on('interactionCreate', this.handleInteraction.bind(this));
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);
}
}
}
});
} }
async start() { async start() {
@@ -128,21 +59,94 @@ class Bot {
async stop() { async stop() {
try { try {
this.logger.info('Stopping bot...');
async stop() { // Stop notification service
try { if (this.notificationService) {
await this.notificationService.stop();
}
// Destroy Discord client
if (this.client) { if (this.client) {
await this.client.destroy(); await this.client.destroy();
this.logger.info('Bot stopped successfully');
} }
this.logger.info('Bot stopped successfully');
} catch (error) { } catch (error) {
this.logger.error('Error stopping bot:', error); this.logger.error('Error stopping bot:', error);
throw error;
} }
} }
async handleInteraction(interaction) {
if (!interaction.isChatInputCommand() && !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.isChatInputCommand()) {
// Defer once to acknowledge within the 3s window
const isPublicCommand = ['finduser', 'matchhistory', 'findteam'].includes(interaction.commandName);
try {
await interaction.deferReply(!isPublicCommand ? { flags: MessageFlags.Ephemeral } : {});
} catch (deferError) {
this.logger.warn('Failed to defer interaction:', {
error: deferError.message,
command: interaction.commandName,
guild: interaction.guild?.name
});
// If we failed to acknowledge, do not attempt another response
return;
}
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) { } 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);
} }
} }
} }

View File

@@ -1,7 +1,9 @@
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, PermissionFlagsBits, MessageFlags } = require('discord.js');
const EmbedBuilders = require('../utils/embedBuilders'); const EmbedBuilders = require('../utils/embedBuilders');
const HelpCommand = require('./HelpCommand'); const HelpCommand = require('./HelpCommand');
const PaginationManager = require('../utils/PaginationManager'); 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) {
@@ -12,29 +14,33 @@ class CommandHandler {
this.subscriptionCommands = subscriptionCommands; this.subscriptionCommands = subscriptionCommands;
this.helpCommand = new HelpCommand(supabase, logger); this.helpCommand = new HelpCommand(supabase, logger);
this.paginationManager = new PaginationManager(logger); this.paginationManager = new PaginationManager(logger);
this.cooldownManager = new CooldownManager();
} }
async handleCommand(interaction) { async handleCommand(interaction) {
try { try {
// Handle autocomplete interactions first // Check cooldown
if (interaction.isAutocomplete()) { const cooldownTime = this.cooldownManager.checkCooldown(interaction);
await this.handleAutocomplete(interaction); if (cooldownTime > 0) {
await interaction.editReply({
content: `⏰ Please wait ${cooldownTime.toFixed(1)} seconds before using this command again.`
});
return; 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!', flags: ['Ephemeral'] });
break;
case 'help':
await this.helpCommand.handleHelp(interaction);
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;
@@ -47,141 +53,42 @@ 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;
case 'tournaments':
await this.handleTournaments(interaction);
break; break;
default: default:
await interaction.editReply({ await interaction.editReply({
content: '❌ Unknown command', content: '❌ Unknown command.'
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({
flags: ['Ephemeral'] 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: MessageFlags.Ephemeral
});
}
}
}
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"
}); });
} }
} catch (replyError) {
await interaction.respond(choices.slice(0, 25)); // Discord limit this.logger.error('Failed to send error response:', replyError);
} catch (error) {
this.logger.error('Autocomplete error:', error);
// Fallback error response
await interaction.respond([
{
name: "❌ Error loading games - try again",
value: "error_fallback"
}
]);
} }
} }
} }
@@ -189,9 +96,9 @@ class CommandHandler {
async getServerSubscriptions(guildId) { async getServerSubscriptions(guildId) {
try { try {
const { data: subscriptions, error } = await this.supabase const { data: subscriptions, error } = await this.supabase
.from("active_subscriptions") .from('active_subscriptions')
.select("game_name") .select('game_name')
.eq("discord_server_id", guildId); .eq('discord_server_id', guildId);
if (error) { if (error) {
this.logger.error('Error fetching server subscriptions:', error); this.logger.error('Error fetching server subscriptions:', error);
@@ -232,16 +139,14 @@ class CommandHandler {
return { return {
isRegistered, isRegistered,
subscriptionCount: subscriptions.length, subscriptionCount: subscriptions.length,
subscriptions: subscriptions.map(s => s.game_name),
setupComplete: isRegistered && subscriptions.length > 0, setupComplete: isRegistered && subscriptions.length > 0,
getGuidanceMessage: () => { getGuidanceMessage() {
if (!isRegistered) { 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) { } 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.'; return "Next, run `/subscribe` to add game notifications.";
} else {
return `✅ **Setup Complete:** ${subscriptions.length} game${subscriptions.length !== 1 ? 's' : ''} subscribed: ${subscriptions.join(', ')}`;
} }
return "Your server is properly set up!";
} }
}; };
} catch (error) { } catch (error) {
@@ -249,32 +154,20 @@ class CommandHandler {
return { return {
isRegistered: false, isRegistered: false,
subscriptionCount: 0, subscriptionCount: 0,
subscriptions: [],
setupComplete: false, 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) { async handleRegisterServer(interaction) {
try { try {
// Check if command is being used in a guild (not DM) // Check for admin permissions
if (!interaction.guild || !interaction.guildId) { if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
await interaction.editReply({ 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.', content: '❌ You need administrator permissions to register this server.'
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']
}); });
return; return;
} }
@@ -293,38 +186,60 @@ 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.'
});
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.'
});
return;
}
// Prepare response message based on status // Prepare response message based on status
let message; let message;
switch (result.status) { switch (result.status) {
case 'created': 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; break;
case 'updated': 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; break;
case 'exists': 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; break;
default: 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({ await interaction.editReply({
content: message, content: message
flags: ['Ephemeral']
}); });
// Log the successful operation // Log the successful operation
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()
}); });
@@ -332,18 +247,17 @@ class CommandHandler {
this.logger.error('Error in handleRegisterServer:', { this.logger.error('Error in handleRegisterServer:', {
error: error.message, error: error.message,
stack: error.stack, stack: error.stack,
guildId: interaction.guildId || 'Unknown', guildId: interaction.guildId,
serverName: interaction.guild?.name || 'Unknown' serverName: interaction.guild?.name
}); });
// Send error message to user // Send error message to user
const errorMessage = error.message === 'Invalid guildId provided' || error.message === 'Invalid serverName provided' 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.' ? '❌ Invalid server information provided.'
: '❌ **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.'; : '❌ An error occurred while registering the server. Please try again later.';
await interaction.editReply({ await interaction.editReply({
content: errorMessage, content: errorMessage
flags: ['Ephemeral']
}); });
throw error; throw error;
@@ -355,17 +269,6 @@ 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');
// 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 // Input validation with helpful messages
if (!username || typeof username !== 'string') { if (!username || typeof username !== 'string') {
await interaction.editReply({ await interaction.editReply({
@@ -398,10 +301,8 @@ class CommandHandler {
const userData = await this.playerService.findUserByUsername(sanitizedUsername); const userData = await this.playerService.findUserByUsername(sanitizedUsername);
if (!userData || !userData.success) { if (!userData || !userData.success) {
const setupStatus = await this.checkSetupStatusWithGuidance(interaction.guildId);
await interaction.editReply({ 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; return;
} }
@@ -421,7 +322,7 @@ class CommandHandler {
if (!hasPlayedGame) { if (!hasPlayedGame) {
const teamMessage = isOnTeam ? 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 // Create a basic embed with user info
@@ -460,7 +361,7 @@ class CommandHandler {
// Add message about no game stats // Add message about no game stats
basicEmbed.addFields({ basicEmbed.addFields({
name: `${gameFilter} Status`, 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); const row = this.createActionRow(playerData.username);
@@ -488,7 +389,7 @@ class CommandHandler {
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
await interaction.editReply({ 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 +399,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
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); const userData = await this.playerService.findUserByUsername(username);
if (!userData || !userData.success) { if (!userData || !userData.success) {
await interaction.editReply({ 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; return;
} }
@@ -519,42 +412,14 @@ class CommandHandler {
: userData.player_data; : userData.player_data;
// Extract matches from player data // Extract matches from player data
const allMatches = Object.values(playerData.matches || {}); const matches = Object.values(playerData.matches || {})
const matches = allMatches
.filter(match => !gameFilter || match.game_name.toLowerCase() === gameFilter.toLowerCase()) .filter(match => !gameFilter || match.game_name.toLowerCase() === gameFilter.toLowerCase())
.sort((a, b) => new Date(b.start_time) - new Date(a.start_time)) .sort((a, b) => new Date(b.start_time) - new Date(a.start_time))
.slice(0, 10); .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 embed = EmbedBuilders.createMatchHistoryEmbed(playerData, matches);
const row = EmbedBuilders.createActionRow(playerData.username, this.playerService); 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({ await interaction.editReply({
embeds: [embed], embeds: [embed],
components: [row], components: [row],
@@ -565,9 +430,7 @@ class CommandHandler {
username: interaction.options.getString('username'), username: interaction.options.getString('username'),
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
await interaction.editReply({ throw error;
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.'
});
} }
} }
@@ -576,22 +439,10 @@ class CommandHandler {
const teamName = interaction.options.getString('teamname'); const teamName = interaction.options.getString('teamname');
const gameFilter = interaction.options.getString('game'); 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 // Input validation
if (!teamName || typeof teamName !== 'string') { if (!teamName || typeof teamName !== 'string') {
await interaction.editReply({ 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; return;
} }
@@ -604,8 +455,7 @@ class CommandHandler {
if (!sanitizedTeamName) { if (!sanitizedTeamName) {
await interaction.editReply({ 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; return;
} }
@@ -622,46 +472,18 @@ class CommandHandler {
const teamData = await this.playerService.findTeamByName(sanitizedTeamName, gameFilter); const teamData = await this.playerService.findTeamByName(sanitizedTeamName, gameFilter);
if (!teamData || !teamData.success || !teamData.teams || teamData.teams.length === 0) { if (!teamData || !teamData.success || !teamData.teams || teamData.teams.length === 0) {
const setupStatus = await this.checkSetupStatusWithGuidance(interaction.guildId);
await interaction.editReply({ 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; 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'
);
await interaction.editReply({
embeds: embeds,
components: []
});
} catch (error) { } catch (error) {
this.logger.error('Error in handleFindTeam:', { this.logger.error('Error in handleFindTeam:', {
error: error.message, error: error.message,
@@ -671,32 +493,17 @@ class CommandHandler {
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
await interaction.editReply({ 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 ephemeral: false
}); });
} }
} }
createActionRow(username) { async handleButton(interaction) {
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) {
try { try {
// Defer the update to show loading state
await interaction.deferUpdate();
// Check if this is a pagination button // Check if this is a pagination button
if (this.paginationManager.isPaginationButton(interaction.customId)) { if (this.paginationManager.isPaginationButton(interaction.customId)) {
await interaction.deferUpdate();
await this.paginationManager.handlePaginationButton(interaction); await this.paginationManager.handlePaginationButton(interaction);
return; return;
} }
@@ -713,8 +520,13 @@ class CommandHandler {
userId: interaction.user.id userId: interaction.user.id
}); });
await interaction.reply({
content: '❌ This button interaction is not implemented yet.',
ephemeral: true
});
} catch (error) { } catch (error) {
this.logger.error('Error in handleButtonInteraction:', { this.logger.error('Error in handleButton:', {
error: error.message, error: error.message,
customId: interaction.customId, customId: interaction.customId,
userId: interaction.user.id userId: interaction.user.id
@@ -724,11 +536,7 @@ class CommandHandler {
if (!interaction.replied && !interaction.deferred) { if (!interaction.replied && !interaction.deferred) {
await interaction.reply({ await interaction.reply({
content: '❌ An error occurred while processing the button interaction.', content: '❌ An error occurred while processing the button interaction.',
flags: ['Ephemeral'] ephemeral: true
});
} else {
await interaction.editReply({
content: '❌ An error occurred while processing the button interaction.'
}); });
} }
} catch (replyError) { } catch (replyError) {
@@ -738,11 +546,195 @@ class CommandHandler {
} }
async handleSelectMenu(interaction) { async handleSelectMenu(interaction) {
// Handle select menu interactions (placeholder for future implementation) try {
this.logger.debug('Select menu interaction received:', { // Handle select menu interactions (if needed)
customId: interaction.customId, this.logger.debug('Select menu interaction:', {
values: interaction.values customId: interaction.customId,
}); values: interaction.values,
userId: interaction.user.id
});
await interaction.reply({
content: 'Select menu interactions are not yet implemented.',
flags: MessageFlags.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.'
});
}
}
createActionRow(username) {
return new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel('🔵 View Profile')
.setStyle(ButtonStyle.Link)
.setURL(`https://www.vrbattles.gg/profile/${username}`)
);
}
async handleTournaments(interaction) {
try {
this.logger.info('Processing tournaments command', {
userId: interaction.user.id,
guildId: interaction.guildId
});
// Get filter options
const statusFilter = interaction.options.getString('status');
const gameFilter = interaction.options.getString('game');
// Fetch tournaments data
const tournamentsData = await this.playerService.getTournamentsData();
if (!tournamentsData || tournamentsData.success === false) {
await interaction.editReply({
content: '❌ Failed to fetch tournaments data. Please try again later.',
});
return;
}
let tournaments = tournamentsData.data || tournamentsData;
if (!Array.isArray(tournaments)) {
await interaction.editReply({
content: '❌ Invalid tournaments data received from API.',
});
return;
}
// Apply filters
if (statusFilter) {
tournaments = tournaments.filter(t => t.tournament_status === statusFilter);
}
if (gameFilter) {
tournaments = tournaments.filter(t => t.name === gameFilter);
}
if (tournaments.length === 0) {
const filterText = [];
if (statusFilter) filterText.push(`status: ${statusFilter}`);
if (gameFilter) filterText.push(`game: ${gameFilter}`);
const filterMsg = filterText.length > 0 ? ` matching ${filterText.join(', ')}` : '';
await interaction.editReply({
content: `🏆 No tournaments found${filterMsg}.`,
});
return;
}
// Sort tournaments by date (upcoming first, then recent)
tournaments.sort((a, b) => {
const dateA = new Date(a.date_start);
const dateB = new Date(b.date_start);
const now = new Date();
// Upcoming tournaments first
if (dateA > now && dateB > now) {
return dateA - dateB; // Soonest first
}
// Past tournaments last (most recent first)
if (dateA < now && dateB < now) {
return dateB - dateA; // Most recent first
}
// Mix: upcoming before past
if (dateA > now && dateB < now) return -1;
if (dateA < now && dateB > now) return 1;
return dateA - dateB;
});
// Create tournament embed
const embed = EmbedBuilders.createTournamentsEmbed(tournaments, statusFilter, gameFilter);
await interaction.editReply({
embeds: [embed]
});
} catch (error) {
this.logger.error('Error in handleTournaments:', {
error: error.message,
stack: error.stack,
userId: interaction.user.id,
guildId: interaction.guildId
});
await interaction.editReply({
content: '❌ An error occurred while fetching tournaments.',
});
}
} }
} }

View File

@@ -1,4 +1,5 @@
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const packageInfo = require('../../package.json');
class HelpCommand { class HelpCommand {
constructor(supabase, logger) { constructor(supabase, logger) {
@@ -24,8 +25,7 @@ class HelpCommand {
} catch (error) { } catch (error) {
this.logger.error('Error in handleHelp:', error); this.logger.error('Error in handleHelp:', error);
await interaction.editReply({ await interaction.editReply({
content: '❌ An error occurred while loading help. Please try again.', content: '❌ An error occurred while loading help. Please try again.'
flags: ['Ephemeral']
}); });
} }
} }
@@ -75,7 +75,11 @@ class HelpCommand {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle('🤖 BattleBot Help Center') .setTitle('🤖 BattleBot Help Center')
.setDescription('Welcome to BattleBot! Your gateway to VRBattles data and notifications.') .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 // Add setup status section
const setupEmoji = setupStatus.setupComplete ? '✅' : '⚠️'; const setupEmoji = setupStatus.setupComplete ? '✅' : '⚠️';
@@ -139,15 +143,18 @@ class HelpCommand {
await interaction.editReply({ await interaction.editReply({
embeds: [embed], embeds: [embed],
components: [row, row2], components: [row, row2]
flags: ['Ephemeral']
}); });
} }
async showCategoryHelp(interaction, category, setupStatus) { async showCategoryHelp(interaction, category, setupStatus) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor('#0099ff') .setColor('#0099ff')
.setTimestamp(); .setTimestamp()
.setFooter({
text: `BattleBot v${packageInfo.version} | VRBattles`,
iconURL: 'https://vrbattles.gg/favicon.ico'
});
switch (category) { switch (category) {
case 'getting_started': case 'getting_started':
@@ -292,8 +299,7 @@ class HelpCommand {
await interaction.editReply({ await interaction.editReply({
embeds: [embed], embeds: [embed],
components: [backButton], components: [backButton]
flags: ['Ephemeral']
}); });
} }
@@ -316,15 +322,255 @@ class HelpCommand {
} }
} }
async handleHelpButton(interaction) { async showMainHelpButton(interaction, setupStatus) {
const [action, category] = interaction.customId.split('_'); 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'
});
if (category === 'back') { // Add setup status section
const setupStatus = await this.checkServerSetupStatus(interaction.guildId); const setupEmoji = setupStatus.setupComplete ? '✅' : '⚠️';
await this.showMainHelp(interaction, setupStatus); const setupText = this.getSetupStatusText(setupStatus);
} else { embed.addFields({
const setupStatus = await this.checkServerSetupStatus(interaction.guildId); name: `${setupEmoji} Server Setup Status`,
await this.showCategoryHelp(interaction, category, setupStatus); 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
});
} }
} }
} }

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,88 @@
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 ONLY game choice blocks (avoid tournament status choices)
// Look for game choice patterns in finduser, findteam, matchhistory, subscribe, unsubscribe commands
const gameChoiceCommands = ['finduser', 'findteam', 'matchhistory', 'subscribe', 'unsubscribe'];
gameChoiceCommands.forEach(commandName => {
// Match the specific pattern for each game command
const commandPattern = new RegExp(
`(\\.setName\\("${commandName}"\\)[\\s\\S]*?\\.setName\\("game"\\)[\\s\\S]*?)\\.addChoices\\(\\s*([^)]+)\\s*\\)`,
'g'
);
const newChoicesBlock = `.addChoices(
${choicesCode}
)`;
deployContent = deployContent.replace(commandPattern, `$1${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

@@ -76,6 +76,41 @@ class PlayerService {
return `${this.baseUrl}/profile/${username}/stats`; return `${this.baseUrl}/profile/${username}/stats`;
} }
async getTournamentsData() {
try {
console.log('Fetching tournaments data...');
const url = `${this.baseUrl}/api/fetch/tournaments`;
console.log(`API URL: ${url}`);
const response = await axios.get(url, {
timeout: 5000,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
validateStatus: function (status) {
return status >= 200 && status < 300;
}
});
console.log('Tournaments API Response:', JSON.stringify(response.data, null, 2));
return response.data;
} catch (error) {
console.error('Error fetching tournaments data:', {
message: error.message,
response: error.response?.data,
status: error.response?.status
});
return { success: false, error: 'Failed to fetch tournaments data' };
}
}
async getTournamentData(tournamentId) {
const url = `${this.baseUrl}/api/get_tournament_data/${tournamentId}`;
const response = await axios.get(url);
return response.data;
}
async findTeamByName(teamName, gameFilter) { async findTeamByName(teamName, gameFilter) {
try { try {
// Double-check sanitization here as well for defense in depth // Double-check sanitization here as well for defense in depth

View File

@@ -0,0 +1,90 @@
// Test script for tournaments functionality
require('dotenv').config();
const PlayerService = require('../services/PlayerService');
async function testTournamentsAPI() {
console.log('🏆 Testing Tournaments API Functions...\n');
const playerService = new PlayerService();
try {
// Test 1: Get all tournaments
console.log('📋 Test 1: Fetching all tournaments...');
console.log('=' .repeat(50));
const tournamentsData = await playerService.getTournamentsData();
if (tournamentsData && tournamentsData.success !== false) {
console.log('✅ Successfully fetched tournaments data!');
console.log(`📊 Response structure:`, Object.keys(tournamentsData));
// Show some basic info about the tournaments
if (Array.isArray(tournamentsData.tournaments)) {
console.log(`🎯 Found ${tournamentsData.tournaments.length} tournaments`);
// Show first few tournaments as examples
const sampleSize = Math.min(3, tournamentsData.tournaments.length);
console.log(`\n📖 Sample tournaments (showing ${sampleSize}):`);
for (let i = 0; i < sampleSize; i++) {
const tournament = tournamentsData.tournaments[i];
console.log(` ${i + 1}. ${tournament.name || tournament.title || 'Unknown'} (ID: ${tournament.id || 'N/A'})`);
}
} else if (Array.isArray(tournamentsData)) {
console.log(`🎯 Found ${tournamentsData.length} tournaments (direct array)`);
const sampleSize = Math.min(3, tournamentsData.length);
console.log(`\n📖 Sample tournaments (showing ${sampleSize}):`);
for (let i = 0; i < sampleSize; i++) {
const tournament = tournamentsData[i];
console.log(` ${i + 1}. ${tournament.name || tournament.title || 'Unknown'} (ID: ${tournament.id || 'N/A'})`);
}
} else {
console.log('📊 Response data:', JSON.stringify(tournamentsData, null, 2));
}
} else {
console.log('❌ Failed to fetch tournaments data');
console.log('Error:', tournamentsData?.error || 'Unknown error');
}
console.log('\n' + '=' .repeat(50));
// Test 2: Try to get specific tournament data (if we have any tournament IDs)
console.log('\n🎯 Test 2: Testing single tournament fetch...');
console.log('=' .repeat(50));
// Try with a sample tournament ID (you might need to adjust this based on actual data)
const testTournamentId = '1'; // Start with ID 1 as a common starting point
console.log(`📋 Fetching tournament with ID: ${testTournamentId}`);
const singleTournament = await playerService.getTournamentData(testTournamentId);
if (singleTournament && singleTournament.success !== false) {
console.log('✅ Successfully fetched single tournament data!');
console.log('📊 Tournament details:');
console.log(` Name: ${singleTournament.name || singleTournament.title || 'Unknown'}`);
console.log(` Game: ${singleTournament.game || singleTournament.game_name || 'Unknown'}`);
console.log(` Status: ${singleTournament.status || 'Unknown'}`);
} else {
console.log(`❌ Failed to fetch tournament with ID: ${testTournamentId}`);
console.log('Error:', singleTournament?.error || 'Unknown error');
console.log('💡 This might be normal if tournament ID 1 doesn\'t exist');
}
} catch (error) {
console.error('❌ Test failed with error:', {
message: error.message,
stack: error.stack
});
}
console.log('\n🏁 Tournament API testing completed!');
}
// Run the test
if (require.main === module) {
testTournamentsAPI().catch(console.error);
}
module.exports = { testTournamentsAPI };

View File

@@ -353,6 +353,141 @@ class EmbedBuilders {
return embed; return embed;
} }
static createTournamentsEmbed(tournaments, statusFilter, gameFilter) {
const embed = new EmbedBuilder()
.setTitle('🏆 VRBattles Tournaments')
.setColor('#FFD700') // Gold color for tournaments
.setTimestamp();
// Add filter info in description if filters are applied
let description = '';
const filterInfo = [];
if (statusFilter) filterInfo.push(`**Status:** ${statusFilter}`);
if (gameFilter) filterInfo.push(`**Game:** ${gameFilter}`);
if (filterInfo.length > 0) {
description = `*Filtered by: ${filterInfo.join(' | ')}*\n\n`;
}
description += `Found **${tournaments.length}** tournament${tournaments.length !== 1 ? 's' : ''}`;
embed.setDescription(description);
// Group tournaments by status for better organization
const grouped = {
'Published': [],
'In-progress': [],
'Ended': [],
'Unpublished': []
};
tournaments.forEach(tournament => {
const status = tournament.tournament_status || 'Unknown';
if (!grouped[status]) grouped[status] = [];
grouped[status].push(tournament);
});
// Add tournaments by status (show active ones first)
const statusOrder = ['Published', 'In-progress', 'Ended', 'Unpublished'];
statusOrder.forEach(status => {
if (grouped[status].length === 0) return;
// Status emoji mapping
const statusEmoji = {
'Published': '📢',
'In-progress': '⚡',
'Ended': '🏁',
'Unpublished': '📝'
};
const emoji = statusEmoji[status] || '🎯';
embed.addFields({
name: `${emoji} ${status} Tournaments (${grouped[status].length})`,
value: '\u200B',
inline: false
});
// Add each tournament in this status
grouped[status].slice(0, 10).forEach(tournament => {
const tournamentName = tournament.tournament_name || 'Unknown Tournament';
const gameName = tournament.name || 'Unknown Game';
const prize = tournament.prize ? `$${tournament.prize}` : 'No prize';
const entryFee = tournament.entry_cost === '0' ? 'Free' : `$${tournament.entry_cost}`;
const teamSize = tournament.team_size_local || tournament.team_size || 'Unknown';
const maxTeams = tournament.max_teams || 'Unknown';
const tournamentType = tournament.type || 'Unknown';
// Format date
let dateInfo = '';
if (tournament.date_start) {
const startDate = new Date(tournament.date_start);
const now = new Date();
if (startDate > now) {
dateInfo = `🗓️ Starts: <t:${Math.floor(startDate.getTime() / 1000)}:R>`;
} else {
dateInfo = `🗓️ Started: <t:${Math.floor(startDate.getTime() / 1000)}:R>`;
}
}
// Build tournament info
const tournamentInfo = [
`🎮 **${gameName}**`,
`💰 Prize: **${prize}** | Entry: **${entryFee}**`,
`👥 **${teamSize}** (Max: ${maxTeams}) | Type: **${tournamentType}**`,
dateInfo
].filter(info => info).join('\n');
embed.addFields({
name: `**${tournamentName}** (ID: ${tournament.id})`,
value: tournamentInfo,
inline: true
});
});
// Add a separator if there are more statuses coming
const currentIndex = statusOrder.indexOf(status);
const hasMoreStatuses = statusOrder.slice(currentIndex + 1).some(s => grouped[s].length > 0);
if (hasMoreStatuses) {
embed.addFields({ name: '\u200B', value: '▬▬▬▬▬▬▬▬▬▬', inline: false });
}
});
// Add footer with helpful info
const totalTournaments = tournaments.length;
if (totalTournaments > 30) {
embed.setFooter({
text: `Showing first 30 tournaments. Total: ${totalTournaments} | Use filters to narrow results`
});
} else {
embed.setFooter({
text: 'Use /tournaments status:<filter> or /tournaments game:<filter> to filter results'
});
}
// Set thumbnail based on game filter or use default
if (gameFilter) {
// You could add game-specific thumbnails here
switch (gameFilter) {
case 'VAIL':
embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/--%20Logos/VAIL_Icon_Color.png');
break;
case 'Echo Arena':
embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/echovr.png');
break;
case 'Blacktop Hoops':
embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/Blacktop%20Hoops.png');
break;
default:
embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/--%20Logos/VAIL_Icon_Color.png');
}
} else {
embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/--%20Logos/VAIL_Icon_Color.png');
}
return embed;
}
} }
module.exports = EmbedBuilders; module.exports = EmbedBuilders;