Compare commits
12 Commits
3453be6947
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0dc7bf4ef | ||
|
|
dd8aa456d9 | ||
|
|
546127f91c | ||
|
|
a8e836fd5b | ||
| 6b904d765d | |||
|
|
06742518d6 | ||
|
|
5b0dd201ca | ||
|
|
f3b6625527 | ||
|
|
667dc6eddb | ||
|
|
8c7c3fc257 | ||
|
|
e86e5f2778 | ||
| cd5b0b580c |
@@ -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
|
||||||
16
.env.example
16
.env.example
@@ -1,16 +0,0 @@
|
|||||||
# Discord Bot Configuration
|
|
||||||
DISCORD_TOKEN=your_dev_bot_token
|
|
||||||
CLIENT_ID=your_dev_client_id
|
|
||||||
|
|
||||||
# Supabase Configuration
|
|
||||||
SUPABASE_URL=your_dev_supabase_url
|
|
||||||
SUPABASE_KEY=your_dev_supabase_key
|
|
||||||
|
|
||||||
# Webhook Configuration (for match notifications)
|
|
||||||
WEBHOOK_SECRET=your_webhook_secret
|
|
||||||
|
|
||||||
# Channel Configuration
|
|
||||||
NOTIFICATION_CHANNEL_ID=your_notification_channel_id
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
NODE_ENV=development
|
|
||||||
172
DEPLOYMENT.md
Normal file
172
DEPLOYMENT.md
Normal 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
|
||||||
@@ -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
49
docs/issue-tracker.md
Normal 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.
|
||||||
|
|
||||||
161
index.js
161
index.js
@@ -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 {
|
||||||
|
if (bot) {
|
||||||
await bot.stop();
|
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 {
|
||||||
|
if (bot) {
|
||||||
await bot.stop();
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
26
package.json
26
package.json
@@ -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
94
scripts/deploy.sh
Executable 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}"
|
||||||
166
src/Bot.js
166
src/Bot.js
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
if (interaction.replied || interaction.deferred) {
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: '❌ An error occurred while processing your command.',
|
content: '❌ An error occurred while processing your command.',
|
||||||
flags: ['Ephemeral']
|
|
||||||
});
|
});
|
||||||
} 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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
} catch (replyError) {
|
||||||
}
|
this.logger.error('Failed to send error response:', replyError);
|
||||||
|
|
||||||
async handleAutocomplete(interaction) {
|
|
||||||
const { commandName, options } = interaction;
|
|
||||||
|
|
||||||
if (options.getFocused().name === 'game') {
|
|
||||||
try {
|
|
||||||
const guildId = interaction.guildId;
|
|
||||||
|
|
||||||
// Check if this is a subscription/admin command
|
|
||||||
const isAdminCommand = ['subscribe', 'unsubscribe'].includes(commandName);
|
|
||||||
|
|
||||||
if (isAdminCommand) {
|
|
||||||
// Show ALL games for admin/subscription commands
|
|
||||||
const allGames = [
|
|
||||||
{ name: "Big Ballers VR", value: "Big Ballers VR" },
|
|
||||||
{ name: "Blacktop Hoops", value: "Blacktop Hoops" },
|
|
||||||
{ name: "Breachers", value: "Breachers" },
|
|
||||||
{ name: "Echo Arena", value: "Echo Arena" },
|
|
||||||
{ name: "Echo Combat", value: "Echo Combat" },
|
|
||||||
{ name: "Gun Raiders", value: "Gun Raiders" },
|
|
||||||
{ name: "Nock", value: "Nock" },
|
|
||||||
{ name: "Orion Drift", value: "Orion Drift" },
|
|
||||||
{ name: "VAIL", value: "VAIL" }
|
|
||||||
];
|
|
||||||
|
|
||||||
await interaction.respond(allGames);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For user commands (finduser, findteam, matchhistory), check subscriptions
|
|
||||||
const isUserCommand = ['finduser', 'findteam'].includes(commandName);
|
|
||||||
const isMatchHistory = commandName === 'matchhistory';
|
|
||||||
|
|
||||||
// Get subscriptions
|
|
||||||
const subscriptions = await this.getServerSubscriptions(guildId);
|
|
||||||
|
|
||||||
if (isUserCommand && subscriptions.length === 0) {
|
|
||||||
// Fallback for unsubscribed servers (strict requirement for finduser/findteam)
|
|
||||||
const fallbackOptions = [
|
|
||||||
{
|
|
||||||
name: "❌ No game subscriptions found",
|
|
||||||
value: "no_subscriptions"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "💡 Use /subscribe to add games first",
|
|
||||||
value: "need_subscription"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "📋 Then use /list_subscriptions to see active games",
|
|
||||||
value: "list_help"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
await interaction.respond(fallbackOptions);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMatchHistory) {
|
|
||||||
// For matchhistory, show subscribed games OR all games if no subscriptions
|
|
||||||
const gamesToShow = subscriptions.length > 0 ? subscriptions : [
|
|
||||||
{ game_name: "Big Ballers VR" },
|
|
||||||
{ game_name: "Blacktop Hoops" },
|
|
||||||
{ game_name: "Breachers" },
|
|
||||||
{ game_name: "Echo Arena" },
|
|
||||||
{ game_name: "Echo Combat" },
|
|
||||||
{ game_name: "Gun Raiders" },
|
|
||||||
{ game_name: "Nock" },
|
|
||||||
{ game_name: "Orion Drift" },
|
|
||||||
{ game_name: "VAIL" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const choices = gamesToShow.map(sub => ({
|
|
||||||
name: subscriptions.length > 0 ? `🎮 ${sub.game_name}` : `🔍 ${sub.game_name}`,
|
|
||||||
value: sub.game_name
|
|
||||||
}));
|
|
||||||
|
|
||||||
await interaction.respond(choices.slice(0, 25));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show subscribed games with helpful context for finduser/findteam
|
|
||||||
const choices = subscriptions.map(sub => ({
|
|
||||||
name: `🎮 ${sub.game_name}`,
|
|
||||||
value: sub.game_name
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add helpful footer if less than 25 games (Discord's limit)
|
|
||||||
if (choices.length < 24) {
|
|
||||||
choices.push({
|
|
||||||
name: `📝 ${choices.length} subscribed game${choices.length !== 1 ? 's' : ''} available`,
|
|
||||||
value: "info_footer"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await interaction.respond(choices.slice(0, 25)); // Discord limit
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Autocomplete error:', error);
|
|
||||||
|
|
||||||
// Fallback error response
|
|
||||||
await interaction.respond([
|
|
||||||
{
|
|
||||||
name: "❌ Error loading games - try again",
|
|
||||||
value: "error_fallback"
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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({
|
await interaction.editReply({
|
||||||
embeds: embeds,
|
embeds: embeds,
|
||||||
components: [],
|
components: []
|
||||||
ephemeral: false
|
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use pagination for many teams
|
|
||||||
const teamEmbedBuilder = (teamSubset, currentPage, totalPages) => {
|
|
||||||
return EmbedBuilders.createPaginatedTeamEmbed(teamSubset, currentPage, totalPages, sanitizedTeamName, gameFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
const paginatedData = this.paginationManager.createPaginatedResponse(
|
|
||||||
teams,
|
|
||||||
3, // 3 teams per page
|
|
||||||
teamEmbedBuilder,
|
|
||||||
`Teams matching "${sanitizedTeamName}"`
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.paginationManager.sendPaginatedMessage(
|
|
||||||
interaction,
|
|
||||||
paginatedData,
|
|
||||||
'teamlist'
|
|
||||||
);
|
|
||||||
|
|
||||||
} catch (error) {
|
} 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,12 +546,196 @@ 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)
|
||||||
|
this.logger.debug('Select menu interaction:', {
|
||||||
customId: interaction.customId,
|
customId: interaction.customId,
|
||||||
values: interaction.values
|
values: interaction.values,
|
||||||
|
userId: interaction.user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'Select menu interactions are not yet implemented.',
|
||||||
|
flags: 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.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = CommandHandler;
|
module.exports = CommandHandler;
|
||||||
@@ -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 showMainHelpButton(interaction, setupStatus) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle('🤖 BattleBot Help Center')
|
||||||
|
.setDescription('Welcome to BattleBot! Your gateway to VRBattles data and notifications.')
|
||||||
|
.setColor(setupStatus.setupComplete ? '#00ff00' : '#ffaa00')
|
||||||
|
.setFooter({
|
||||||
|
text: `BattleBot v${packageInfo.version} | VRBattles`,
|
||||||
|
iconURL: 'https://vrbattles.gg/favicon.ico'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add setup status section
|
||||||
|
const setupEmoji = setupStatus.setupComplete ? '✅' : '⚠️';
|
||||||
|
const setupText = this.getSetupStatusText(setupStatus);
|
||||||
|
embed.addFields({
|
||||||
|
name: `${setupEmoji} Server Setup Status`,
|
||||||
|
value: setupText,
|
||||||
|
inline: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add quick start if not set up
|
||||||
|
if (!setupStatus.setupComplete) {
|
||||||
|
embed.addFields({
|
||||||
|
name: '🚀 Quick Start',
|
||||||
|
value: '1. `/register_server` - Register your server\n2. `/subscribe` - Subscribe to game notifications\n3. `/finduser` - Start searching players!',
|
||||||
|
inline: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add help categories
|
||||||
|
embed.addFields({
|
||||||
|
name: '📚 Help Categories',
|
||||||
|
value: '**🏠 Getting Started** - Setup and first steps\n' +
|
||||||
|
'**🔍 Search Commands** - Find players, teams, and matches\n' +
|
||||||
|
'**⚙️ Admin & Setup** - Server configuration\n' +
|
||||||
|
'**🔔 Notifications** - Match alerts and subscriptions\n' +
|
||||||
|
'**❓ Troubleshooting** - Common issues and solutions',
|
||||||
|
inline: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create action buttons
|
||||||
|
const row = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('help_getting_started')
|
||||||
|
.setLabel('🏠 Getting Started')
|
||||||
|
.setStyle(ButtonStyle.Primary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('help_search')
|
||||||
|
.setLabel('🔍 Search Commands')
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('help_admin')
|
||||||
|
.setLabel('⚙️ Admin & Setup')
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
);
|
||||||
|
|
||||||
|
const row2 = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('help_notifications')
|
||||||
|
.setLabel('🔔 Notifications')
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('help_troubleshooting')
|
||||||
|
.setLabel('❓ Troubleshooting')
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setLabel('📖 Full Documentation')
|
||||||
|
.setStyle(ButtonStyle.Link)
|
||||||
|
.setURL('https://help.vrbattles.gg')
|
||||||
|
);
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [embed],
|
||||||
|
components: [row, row2]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async showCategoryHelpButton(interaction, category, setupStatus) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor('#0099ff')
|
||||||
|
.setTimestamp()
|
||||||
|
.setFooter({
|
||||||
|
text: `BattleBot v${packageInfo.version} | VRBattles`,
|
||||||
|
iconURL: 'https://vrbattles.gg/favicon.ico'
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (category) {
|
||||||
|
case 'getting_started':
|
||||||
|
embed.setTitle('🏠 Getting Started with BattleBot')
|
||||||
|
.setDescription('Follow these steps to set up BattleBot in your server:')
|
||||||
|
.addFields(
|
||||||
|
{
|
||||||
|
name: '1️⃣ Register Your Server',
|
||||||
|
value: '`/register_server` - This connects your Discord server to BattleBot\n' +
|
||||||
|
`Status: ${setupStatus.isRegistered ? '✅ Complete' : '❌ Not done'}`,
|
||||||
|
inline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '2️⃣ Subscribe to Games',
|
||||||
|
value: '`/subscribe` - Choose games and channels for notifications\n' +
|
||||||
|
`Status: ${setupStatus.subscriptionCount > 0 ? `✅ ${setupStatus.subscriptionCount} games` : '❌ No subscriptions'}`,
|
||||||
|
inline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '3️⃣ Start Using Commands',
|
||||||
|
value: '`/finduser` - Search for players\n`/findteam` - Find teams\n`/matchhistory` - View match data',
|
||||||
|
inline: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'search':
|
||||||
|
embed.setTitle('🔍 Search Commands')
|
||||||
|
.setDescription('Find players, teams, and match data:')
|
||||||
|
.addFields(
|
||||||
|
{
|
||||||
|
name: '👤 `/finduser`',
|
||||||
|
value: 'Search for a player by username in specific games\n' +
|
||||||
|
'• Shows stats, teams, and recent matches\n' +
|
||||||
|
'• Requires game subscription to use',
|
||||||
|
inline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '👥 `/findteam`',
|
||||||
|
value: 'Find teams by name in specific games\n' +
|
||||||
|
'• Shows team stats and roster\n' +
|
||||||
|
'• Displays win rates and match history',
|
||||||
|
inline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '📊 `/matchhistory`',
|
||||||
|
value: 'View detailed match history for any player\n' +
|
||||||
|
'• Optional game filter\n' +
|
||||||
|
'• Shows recent performance trends',
|
||||||
|
inline: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'admin':
|
||||||
|
embed.setTitle('⚙️ Admin & Setup Commands')
|
||||||
|
.setDescription('Server management and configuration (Admin only):')
|
||||||
|
.addFields(
|
||||||
|
{
|
||||||
|
name: '🔧 `/register_server`',
|
||||||
|
value: 'Connect your Discord server to BattleBot\n' +
|
||||||
|
'• Required before using other features\n' +
|
||||||
|
'• Safe to run multiple times',
|
||||||
|
inline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '📋 `/list_subscriptions`',
|
||||||
|
value: 'View all active game subscriptions\n' +
|
||||||
|
'• Shows which games and channels\n' +
|
||||||
|
'• Helps manage notifications',
|
||||||
|
inline: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'notifications':
|
||||||
|
embed.setTitle('🔔 Notification Commands')
|
||||||
|
.setDescription('Manage game notifications and alerts:')
|
||||||
|
.addFields(
|
||||||
|
{
|
||||||
|
name: '➕ `/subscribe`',
|
||||||
|
value: 'Subscribe to match notifications for specific games\n' +
|
||||||
|
'• Choose game and notification channel\n' +
|
||||||
|
'• Get alerts for new matches',
|
||||||
|
inline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '➖ `/unsubscribe`',
|
||||||
|
value: 'Remove game notification subscriptions\n' +
|
||||||
|
'• Stop notifications for specific games\n' +
|
||||||
|
'• Clean up unused subscriptions',
|
||||||
|
inline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '🎮 Supported Games',
|
||||||
|
value: setupStatus.subscriptions.length > 0
|
||||||
|
? `**Your subscriptions:** ${setupStatus.subscriptions.join(', ')}`
|
||||||
|
: 'VAIL, Echo Arena, Nock, Breachers, Gun Raiders, and more!',
|
||||||
|
inline: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'troubleshooting':
|
||||||
|
embed.setTitle('❓ Troubleshooting & Common Issues')
|
||||||
|
.setDescription('Solutions to common problems:')
|
||||||
|
.addFields(
|
||||||
|
{
|
||||||
|
name: '❌ "No game subscriptions found"',
|
||||||
|
value: '**Solution:** Use `/subscribe` to add games first\n' +
|
||||||
|
'**Why:** Search commands only work with subscribed games',
|
||||||
|
inline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '🔒 "Missing permissions"',
|
||||||
|
value: '**Solution:** Ensure you have Administrator permissions\n' +
|
||||||
|
'**Commands affected:** `/register_server`, `/subscribe`, `/unsubscribe`',
|
||||||
|
inline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '🔍 "User/Team not found"',
|
||||||
|
value: '**Solutions:**\n• Check spelling and exact username\n• Try different games\n• User might not have played recently',
|
||||||
|
inline: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '⏰ Rate Limits',
|
||||||
|
value: '**What:** Commands have cooldowns to prevent spam\n' +
|
||||||
|
'**Wait times:** Search (3s), Admin (5s), Ping (1s)',
|
||||||
|
inline: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add back button
|
||||||
|
const backButton = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('help_back')
|
||||||
|
.setLabel('⬅️ Back to Main Help')
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
);
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [embed],
|
||||||
|
components: [backButton]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async handleHelpButton(interaction) {
|
async handleHelpButton(interaction) {
|
||||||
|
try {
|
||||||
const [action, category] = interaction.customId.split('_');
|
const [action, category] = interaction.customId.split('_');
|
||||||
|
|
||||||
if (category === 'back') {
|
if (category === 'back') {
|
||||||
const setupStatus = await this.checkServerSetupStatus(interaction.guildId);
|
const setupStatus = await this.checkServerSetupStatus(interaction.guildId);
|
||||||
await this.showMainHelp(interaction, setupStatus);
|
await this.showMainHelpButton(interaction, setupStatus);
|
||||||
} else {
|
} else {
|
||||||
const setupStatus = await this.checkServerSetupStatus(interaction.guildId);
|
const setupStatus = await this.checkServerSetupStatus(interaction.guildId);
|
||||||
await this.showCategoryHelp(interaction, category, setupStatus);
|
await this.showCategoryHelpButton(interaction, category, setupStatus);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error in handleHelpButton:', error);
|
||||||
|
await interaction.reply({
|
||||||
|
content: '❌ An error occurred while processing the help button.',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,46 +16,105 @@ async function generateInvite() {
|
|||||||
const invite = client.generateInvite({
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
88
src/scripts/syncGameChoices.js
Normal file
88
src/scripts/syncGameChoices.js
Normal 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;
|
||||||
134
src/scripts/testSupabaseConnection.js
Normal file
134
src/scripts/testSupabaseConnection.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const { createClient } = require('@supabase/supabase-js');
|
||||||
|
|
||||||
|
async function testSupabaseConnection() {
|
||||||
|
console.log('🔧 Testing Supabase Connection...\n');
|
||||||
|
|
||||||
|
// Check environment variables
|
||||||
|
console.log('📋 Environment Variables:');
|
||||||
|
console.log(`SUPABASE_URL: ${process.env.SUPABASE_URL ? '✅ Set' : '❌ Missing'}`);
|
||||||
|
console.log(`SUPABASE_KEY: ${process.env.SUPABASE_KEY ? '✅ Set' : '❌ Missing'}`);
|
||||||
|
|
||||||
|
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_KEY) {
|
||||||
|
console.log('\n❌ Missing required environment variables');
|
||||||
|
console.log('Make sure you have SUPABASE_URL and SUPABASE_KEY set in your .env file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nURL: ${process.env.SUPABASE_URL}`);
|
||||||
|
console.log(`Key: ${process.env.SUPABASE_KEY.substring(0, 20)}...\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialize Supabase client (same as in main bot)
|
||||||
|
const supabase = createClient(
|
||||||
|
process.env.SUPABASE_URL,
|
||||||
|
process.env.SUPABASE_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('📡 Testing basic connection...');
|
||||||
|
|
||||||
|
// Test 1: Check basic connection
|
||||||
|
const { data, error } = await supabase.from('games').select('count', { count: 'exact', head: true });
|
||||||
|
if (error) {
|
||||||
|
console.log('❌ Basic connection failed:', error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('✅ Basic connection successful');
|
||||||
|
|
||||||
|
// Test 2: Fetch all games
|
||||||
|
console.log('\n🎮 Testing games table...');
|
||||||
|
const { data: games, error: gamesError } = await supabase
|
||||||
|
.from('games')
|
||||||
|
.select('id, name, active')
|
||||||
|
.order('name');
|
||||||
|
|
||||||
|
if (gamesError) {
|
||||||
|
console.log('❌ Games query failed:', gamesError.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Found ${games.length} total games:`);
|
||||||
|
games.forEach(game => {
|
||||||
|
console.log(` ${game.active ? '✅' : '❌'} ${game.name} (ID: ${game.id})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: Fetch active games only
|
||||||
|
console.log('\n🔥 Testing active games filter...');
|
||||||
|
const { data: activeGames, error: activeError } = await supabase
|
||||||
|
.from('games')
|
||||||
|
.select('id, name')
|
||||||
|
.eq('active', true)
|
||||||
|
.order('name');
|
||||||
|
|
||||||
|
if (activeError) {
|
||||||
|
console.log('❌ Active games query failed:', activeError.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Found ${activeGames.length} active games:`);
|
||||||
|
activeGames.forEach(game => {
|
||||||
|
console.log(` 🎯 ${game.name} (ID: ${game.id})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: Test servers table
|
||||||
|
console.log('\n🏠 Testing servers table...');
|
||||||
|
const { data: servers, error: serversError } = await supabase
|
||||||
|
.from('servers')
|
||||||
|
.select('id, discord_server_id, server_name, active')
|
||||||
|
.limit(5);
|
||||||
|
|
||||||
|
if (serversError) {
|
||||||
|
console.log('❌ Servers query failed:', serversError.message);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Found ${servers.length} servers (showing first 5):`);
|
||||||
|
servers.forEach(server => {
|
||||||
|
console.log(` ${server.active ? '✅' : '❌'} ${server.server_name} (Discord ID: ${server.discord_server_id})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 5: Test active_subscriptions view
|
||||||
|
console.log('\n📋 Testing active_subscriptions view...');
|
||||||
|
const { data: subscriptions, error: subsError } = await supabase
|
||||||
|
.from('active_subscriptions')
|
||||||
|
.select('*')
|
||||||
|
.limit(5);
|
||||||
|
|
||||||
|
if (subsError) {
|
||||||
|
console.log('❌ Active subscriptions query failed:', subsError.message);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Found ${subscriptions.length} active subscriptions (showing first 5):`);
|
||||||
|
subscriptions.forEach(sub => {
|
||||||
|
console.log(` 🎮 ${sub.game_name} → Server ${sub.discord_server_id} → Channel ${sub.notification_channel_id}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 6: Test specific game lookup (simulate autocomplete)
|
||||||
|
console.log('\n🔍 Testing specific game lookup (autocomplete simulation)...');
|
||||||
|
const testGameName = 'VAIL'; // Try to find a common game
|
||||||
|
const { data: specificGame, error: specificError } = await supabase
|
||||||
|
.from('games')
|
||||||
|
.select('id, name')
|
||||||
|
.eq('name', testGameName)
|
||||||
|
.eq('active', true)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (specificError) {
|
||||||
|
console.log(`❌ Specific game lookup failed for "${testGameName}":`, specificError.message);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Found specific game: ${specificGame.name} (ID: ${specificGame.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🎉 All tests completed successfully!');
|
||||||
|
console.log('\n💡 If autocomplete still isn\'t working, check:');
|
||||||
|
console.log(' 1. Environment variables are set correctly in Coolify');
|
||||||
|
console.log(' 2. Bot has been restarted after fixing the code');
|
||||||
|
console.log(' 3. Discord slash commands have been re-deployed');
|
||||||
|
console.log(' 4. Try the /subscribe command to see if games appear there');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('❌ Unexpected error:', error.message);
|
||||||
|
console.log('Stack:', error.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testSupabaseConnection().catch(console.error);
|
||||||
@@ -23,6 +23,15 @@ class NotificationService {
|
|||||||
console.log(`Notification service listening on port ${port}`);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
90
src/tests/testTournaments.js
Normal file
90
src/tests/testTournaments.js
Normal 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 };
|
||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user