v1.2.11: Add tournaments command with filtering and improve error handling
Features: - Add /tournaments command with status and game filters - Add tournament API integration to PlayerService - Add beautiful tournament embed builders with prize pools - Protect sync-games from overwriting manual customizations - Improve error handling with proper MessageFlags Changes: - Add getTournamentsData() and getTournamentData() to PlayerService - Add tournament command handler with pagination support - Add tournament embed builders with rich formatting - Update deployment docs for v1.2.10 features - Add issue tracker documentation - Add tournament testing script - Fix ephemeral flags throughout CommandHandler - Improve permission checks with PermissionFlagsBits Version: 1.2.11
This commit is contained in:
@@ -1,8 +1,30 @@
|
|||||||
# 🚀 VRBattles Discord Bot - Deployment Guide
|
# 🚀 VRBattles Discord Bot - Deployment Guide
|
||||||
|
|
||||||
## 📦 **Latest Version: v1.2.7**
|
## 📦 **Latest Version: v1.2.10**
|
||||||
|
|
||||||
### **What's New in v1.2.7:**
|
### **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)
|
- ✅ Fixed Docker multi-platform build (x86_64 + ARM64)
|
||||||
- ✅ Resolved "exec format error" on Coolify deployment
|
- ✅ Resolved "exec format error" on Coolify deployment
|
||||||
- ✅ Enhanced deployment script with buildx support
|
- ✅ Enhanced deployment script with buildx support
|
||||||
@@ -18,7 +40,7 @@
|
|||||||
### **Docker Images Available:**
|
### **Docker Images Available:**
|
||||||
```
|
```
|
||||||
far54/vrbattles-discord-bot:latest (always current)
|
far54/vrbattles-discord-bot:latest (always current)
|
||||||
far54/vrbattles-discord-bot:v1.2.7 (specific version)
|
far54/vrbattles-discord-bot:v1.2.10 (specific version)
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Multi-Platform Support:**
|
### **Multi-Platform Support:**
|
||||||
@@ -63,6 +85,7 @@ NODE_ENV=production
|
|||||||
- `/finduser` - Search players (9 game dropdown)
|
- `/finduser` - Search players (9 game dropdown)
|
||||||
- `/findteam` - Search teams (9 game dropdown)
|
- `/findteam` - Search teams (9 game dropdown)
|
||||||
- `/matchhistory` - View match history (optional game filter)
|
- `/matchhistory` - View match history (optional game filter)
|
||||||
|
- `/tournaments` - **NEW!** View all tournaments with filters
|
||||||
- `/subscribe` - Admin: Subscribe to game notifications
|
- `/subscribe` - Admin: Subscribe to game notifications
|
||||||
- `/unsubscribe` - Admin: Remove game subscriptions
|
- `/unsubscribe` - Admin: Remove game subscriptions
|
||||||
- `/list_subscriptions` - Admin: View active subscriptions
|
- `/list_subscriptions` - Admin: View active subscriptions
|
||||||
|
|||||||
@@ -159,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.
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vrbattles-discord-bot",
|
"name": "vrbattles-discord-bot",
|
||||||
"version": "1.2.7",
|
"version": "1.2.11",
|
||||||
"description": "VRBattles Discord Bot - Player search, team lookup, match notifications, and more for VR gaming communities",
|
"description": "VRBattles Discord Bot - Player search, team lookup, match notifications, and more for VR gaming communities",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -84,10 +84,11 @@ echo -e " 2. Update your application image to: ${FULL_IMAGE_NAME}:${VERSION}"
|
|||||||
echo -e " 3. Restart the application"
|
echo -e " 3. Restart the application"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}🎯 Features in this version:${NC}"
|
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 " ✅ All 9 games in dropdowns"
|
||||||
echo -e " ✅ Working help button navigation"
|
echo -e " ✅ Working help button navigation"
|
||||||
echo -e " ✅ No debug message spam"
|
|
||||||
echo -e " ✅ Automatic Supabase game sync"
|
echo -e " ✅ Automatic Supabase game sync"
|
||||||
echo -e " ✅ Optimized Docker image"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}🎉 Deployment complete!${NC}"
|
echo -e "${GREEN}🎉 Deployment complete!${NC}"
|
||||||
26
src/Bot.js
26
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');
|
||||||
@@ -79,7 +79,7 @@ class Bot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleInteraction(interaction) {
|
async handleInteraction(interaction) {
|
||||||
if (!interaction.isCommand() && !interaction.isButton()) {
|
if (!interaction.isChatInputCommand() && !interaction.isButton()) {
|
||||||
this.logger.debug('Ignoring non-command/button interaction');
|
this.logger.debug('Ignoring non-command/button interaction');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -93,31 +93,19 @@ class Bot {
|
|||||||
this.processingLock.set(lockKey, true);
|
this.processingLock.set(lockKey, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (interaction.isCommand()) {
|
if (interaction.isChatInputCommand()) {
|
||||||
// Quick defer with timeout handling
|
// Defer once to acknowledge within the 3s window
|
||||||
const isPublicCommand = ['finduser', 'matchhistory', 'findteam'].includes(interaction.commandName);
|
const isPublicCommand = ['finduser', 'matchhistory', 'findteam'].includes(interaction.commandName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await interaction.deferReply(!isPublicCommand ? { flags: MessageFlags.Ephemeral } : {});
|
||||||
interaction.deferReply({ ephemeral: !isPublicCommand }),
|
|
||||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Defer timeout')), 2500))
|
|
||||||
]);
|
|
||||||
} catch (deferError) {
|
} catch (deferError) {
|
||||||
this.logger.warn('Failed to defer interaction:', {
|
this.logger.warn('Failed to defer interaction:', {
|
||||||
error: deferError.message,
|
error: deferError.message,
|
||||||
command: interaction.commandName,
|
command: interaction.commandName,
|
||||||
guild: interaction.guild?.name
|
guild: interaction.guild?.name
|
||||||
});
|
});
|
||||||
// Try to reply immediately if defer failed
|
// If we failed to acknowledge, do not attempt another response
|
||||||
try {
|
return;
|
||||||
await interaction.reply({
|
|
||||||
content: '⏳ Processing your request...',
|
|
||||||
ephemeral: !isPublicCommand
|
|
||||||
});
|
|
||||||
} catch (replyError) {
|
|
||||||
this.logger.error('Failed to reply after defer timeout:', replyError);
|
|
||||||
return; // Give up on this interaction
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('Processing command', {
|
this.logger.debug('Processing command', {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
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');
|
||||||
@@ -22,9 +22,8 @@ class CommandHandler {
|
|||||||
// Check cooldown
|
// Check cooldown
|
||||||
const cooldownTime = this.cooldownManager.checkCooldown(interaction);
|
const cooldownTime = this.cooldownManager.checkCooldown(interaction);
|
||||||
if (cooldownTime > 0) {
|
if (cooldownTime > 0) {
|
||||||
await interaction.reply({
|
await interaction.editReply({
|
||||||
content: `⏰ Please wait ${cooldownTime.toFixed(1)} seconds before using this command again.`,
|
content: `⏰ Please wait ${cooldownTime.toFixed(1)} seconds before using this command again.`
|
||||||
flags: ['Ephemeral']
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -60,10 +59,12 @@ class CommandHandler {
|
|||||||
case 'version':
|
case 'version':
|
||||||
await this.handleVersion(interaction);
|
await this.handleVersion(interaction);
|
||||||
break;
|
break;
|
||||||
|
case 'tournaments':
|
||||||
|
await this.handleTournaments(interaction);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: '❌ Unknown command.',
|
content: '❌ Unknown command.'
|
||||||
flags: ['Ephemeral']
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -83,7 +84,7 @@ class CommandHandler {
|
|||||||
} else {
|
} else {
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: '❌ An error occurred while processing your command.',
|
content: '❌ An error occurred while processing your command.',
|
||||||
flags: ['Ephemeral']
|
flags: MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (replyError) {
|
} catch (replyError) {
|
||||||
@@ -164,10 +165,9 @@ class CommandHandler {
|
|||||||
async handleRegisterServer(interaction) {
|
async handleRegisterServer(interaction) {
|
||||||
try {
|
try {
|
||||||
// Check for admin permissions
|
// Check for admin permissions
|
||||||
if (!interaction.member.permissions.has('ADMINISTRATOR')) {
|
if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: '❌ You need administrator permissions to register this server.',
|
content: '❌ You need administrator permissions to register this server.'
|
||||||
ephemeral: true
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -197,8 +197,7 @@ class CommandHandler {
|
|||||||
if (!result || typeof result !== 'object') {
|
if (!result || typeof result !== 'object') {
|
||||||
this.logger.error('Invalid result from registerServer:', { result, guildId, serverName });
|
this.logger.error('Invalid result from registerServer:', { result, guildId, serverName });
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: '❌ Server registration failed due to invalid response. Please try again or contact support.',
|
content: '❌ Server registration failed due to invalid response. Please try again or contact support.'
|
||||||
ephemeral: true
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -211,8 +210,7 @@ class CommandHandler {
|
|||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: '❌ Server registration failed - no server data returned. This might be a database permissions issue.',
|
content: '❌ Server registration failed - no server data returned. This might be a database permissions issue.'
|
||||||
ephemeral: true
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -234,8 +232,7 @@ class CommandHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: message,
|
content: message
|
||||||
ephemeral: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log the successful operation
|
// Log the successful operation
|
||||||
@@ -260,8 +257,7 @@ class CommandHandler {
|
|||||||
: '❌ An error occurred while registering the server. Please try again later.';
|
: '❌ An error occurred while registering the server. Please try again later.';
|
||||||
|
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: errorMessage,
|
content: errorMessage
|
||||||
ephemeral: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
@@ -446,8 +442,7 @@ class CommandHandler {
|
|||||||
// 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.',
|
content: '❌ Invalid team name provided.'
|
||||||
ephemeral: false
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -460,8 +455,7 @@ class CommandHandler {
|
|||||||
|
|
||||||
if (!sanitizedTeamName) {
|
if (!sanitizedTeamName) {
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: '❌ Team name must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).',
|
content: '❌ Team name must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).'
|
||||||
ephemeral: false
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -479,8 +473,7 @@ class CommandHandler {
|
|||||||
|
|
||||||
if (!teamData || !teamData.success || !teamData.teams || teamData.teams.length === 0) {
|
if (!teamData || !teamData.success || !teamData.teams || teamData.teams.length === 0) {
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: '❌ No teams found matching your search criteria.',
|
content: '❌ No teams found matching your search criteria.'
|
||||||
ephemeral: false
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -489,8 +482,7 @@ class CommandHandler {
|
|||||||
|
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: embeds,
|
embeds: embeds,
|
||||||
components: [],
|
components: []
|
||||||
ephemeral: false
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error in handleFindTeam:', {
|
this.logger.error('Error in handleFindTeam:', {
|
||||||
@@ -564,7 +556,7 @@ class CommandHandler {
|
|||||||
|
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: 'Select menu interactions are not yet implemented.',
|
content: 'Select menu interactions are not yet implemented.',
|
||||||
flags: ['Ephemeral']
|
flags: MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error in handleSelectMenu:', {
|
this.logger.error('Error in handleSelectMenu:', {
|
||||||
@@ -640,8 +632,7 @@ class CommandHandler {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Error in handleVersion:', error);
|
this.logger.error('Error in handleVersion:', error);
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: '❌ An error occurred while fetching version information.',
|
content: '❌ An error occurred while fetching version information.'
|
||||||
flags: ['Ephemeral']
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -654,6 +645,97 @@ class CommandHandler {
|
|||||||
.setURL(`https://www.vrbattles.gg/profile/${username}`)
|
.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;
|
||||||
@@ -44,15 +44,23 @@ async function syncGameChoices() {
|
|||||||
const deployPath = path.join(__dirname, '../../deploy-commands.js');
|
const deployPath = path.join(__dirname, '../../deploy-commands.js');
|
||||||
let deployContent = fs.readFileSync(deployPath, 'utf8');
|
let deployContent = fs.readFileSync(deployPath, 'utf8');
|
||||||
|
|
||||||
// Pattern to match the game choices in addChoices()
|
// Pattern to match ONLY game choice blocks (avoid tournament status choices)
|
||||||
const choicesPattern = /\.addChoices\(\s*([^)]+)\s*\)/g;
|
// 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'
|
||||||
|
);
|
||||||
|
|
||||||
// Replace all instances with updated choices
|
|
||||||
const newChoicesBlock = `.addChoices(
|
const newChoicesBlock = `.addChoices(
|
||||||
${choicesCode}
|
${choicesCode}
|
||||||
)`;
|
)`;
|
||||||
|
|
||||||
deployContent = deployContent.replace(choicesPattern, newChoicesBlock);
|
deployContent = deployContent.replace(commandPattern, `$1${newChoicesBlock}`);
|
||||||
|
});
|
||||||
|
|
||||||
// Write updated file
|
// Write updated file
|
||||||
fs.writeFileSync(deployPath, deployContent);
|
fs.writeFileSync(deployPath, deployContent);
|
||||||
|
|||||||
@@ -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