Team Search Improvements: Added required game selection before team name input Added VR Battles image as the main embed image Removed buttons for cleaner display Added sorting by game mode (Squads > Duo > Solo) Made team display more compact with icons and shortened stats Added SQL injection protection and input sanitization User Search Improvements: Made game selection required before username input Added VR Battles image as the main embed image Added better user status messages: played Security Enhancements: Added input validation and sanitization at multiple levels Limited input lengths to prevent buffer overflow Added proper error handling and logging Implemented safe API calls with timeouts and validation Added protection against SQL injection Code Organization: Improved error messages for better user feedback Added comprehensive logging for monitoring Made responses visible to everyone in the channel Cleaned up code structure and removed redundant parts Development Environment: Set up proper development configuration Added environment variable management Improved command deployment process
356 lines
14 KiB
JavaScript
356 lines
14 KiB
JavaScript
const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
|
const EmbedBuilders = require('../utils/embedBuilders');
|
|
|
|
class CommandHandler {
|
|
constructor(playerService, supabase, logger, serverRegistrationService, subscriptionCommands) {
|
|
this.playerService = playerService;
|
|
this.supabase = supabase;
|
|
this.logger = logger;
|
|
this.serverRegistrationService = serverRegistrationService;
|
|
this.subscriptionCommands = subscriptionCommands;
|
|
}
|
|
|
|
async handleCommand(interaction) {
|
|
try {
|
|
switch (interaction.commandName) {
|
|
case 'register_server':
|
|
await this.handleRegisterServer(interaction);
|
|
break;
|
|
case 'ping':
|
|
await interaction.editReply({ content: 'Pong!', ephemeral: true });
|
|
break;
|
|
case 'finduser':
|
|
await this.handleFindUser(interaction);
|
|
break;
|
|
case 'matchhistory':
|
|
await this.handleMatchHistory(interaction);
|
|
break;
|
|
case 'subscribe':
|
|
await this.subscriptionCommands.handleSubscribe(interaction);
|
|
break;
|
|
case 'unsubscribe':
|
|
await this.subscriptionCommands.handleUnsubscribe(interaction);
|
|
break;
|
|
case 'list_subscriptions':
|
|
await this.subscriptionCommands.handleListSubscriptions(interaction);
|
|
break;
|
|
case 'findteam':
|
|
await this.handleFindTeam(interaction);
|
|
break;
|
|
default:
|
|
await interaction.editReply({
|
|
content: '❌ Unknown command',
|
|
ephemeral: true
|
|
});
|
|
}
|
|
} catch (error) {
|
|
this.logger.error('Command handling error:', {
|
|
command: interaction.commandName,
|
|
error: error.message,
|
|
stack: error.stack
|
|
});
|
|
|
|
try {
|
|
await interaction.editReply({
|
|
content: '❌ An error occurred while processing your command.',
|
|
ephemeral: true
|
|
});
|
|
} catch (followUpError) {
|
|
this.logger.error('Failed to send error response:', {
|
|
error: followUpError.message,
|
|
originalError: error.message
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
async handleRegisterServer(interaction) {
|
|
try {
|
|
// Check for admin permissions
|
|
if (!interaction.member.permissions.has('ADMINISTRATOR')) {
|
|
await interaction.editReply({
|
|
content: '❌ You need administrator permissions to register this server.',
|
|
ephemeral: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
const guildId = interaction.guildId;
|
|
const serverName = interaction.guild.name;
|
|
|
|
this.logger.debug('Starting server registration', {
|
|
guildId,
|
|
serverName,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Attempt the database operation
|
|
const result = await this.serverRegistrationService.registerServer(guildId, serverName);
|
|
|
|
// Log the result
|
|
this.logger.debug('Server registration result received', {
|
|
status: result.status,
|
|
serverId: result.server?.id,
|
|
guildId,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Prepare response message based on status
|
|
let message;
|
|
switch (result.status) {
|
|
case 'created':
|
|
message = '✅ Server successfully registered! You can now use subscription commands.';
|
|
break;
|
|
case 'updated':
|
|
message = '✅ Server information has been updated!';
|
|
break;
|
|
case 'exists':
|
|
message = '✅ This server is already registered and ready to use subscription commands!';
|
|
break;
|
|
default:
|
|
message = '❌ An unexpected error occurred during registration.';
|
|
}
|
|
|
|
await interaction.editReply({
|
|
content: message,
|
|
ephemeral: true
|
|
});
|
|
|
|
// Log the successful operation
|
|
this.logger.info('Server registration completed', {
|
|
status: result.status,
|
|
guildId,
|
|
serverId: result.server.id,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
this.logger.error('Error in handleRegisterServer:', {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
guildId: interaction.guildId,
|
|
serverName: interaction.guild?.name
|
|
});
|
|
|
|
// Send error message to user
|
|
const errorMessage = error.message === 'Invalid guildId provided' || error.message === 'Invalid serverName provided'
|
|
? '❌ Invalid server information provided.'
|
|
: '❌ An error occurred while registering the server. Please try again later.';
|
|
|
|
await interaction.editReply({
|
|
content: errorMessage,
|
|
ephemeral: true
|
|
});
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async handleFindUser(interaction) {
|
|
try {
|
|
const username = interaction.options.getString('username');
|
|
const gameFilter = interaction.options.getString('game');
|
|
|
|
// Input validation
|
|
if (!username || typeof username !== 'string') {
|
|
await interaction.editReply({
|
|
content: '❌ Invalid username provided.'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Enhanced sanitization
|
|
const sanitizedUsername = username
|
|
.replace(/[^a-zA-Z0-9\s\-_.]/g, '')
|
|
.trim()
|
|
.slice(0, 100);
|
|
|
|
if (!sanitizedUsername) {
|
|
await interaction.editReply({
|
|
content: '❌ Username must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Log sanitized input for monitoring
|
|
this.logger.debug('User search input:', {
|
|
original: username,
|
|
sanitized: sanitizedUsername,
|
|
game: gameFilter,
|
|
userId: interaction.user.id,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
const userData = await this.playerService.findUserByUsername(sanitizedUsername);
|
|
if (!userData || !userData.success) {
|
|
await interaction.editReply({
|
|
content: '❌ User not found or an error occurred while fetching data.'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const playerData = typeof userData.player_data === 'string'
|
|
? JSON.parse(userData.player_data)
|
|
: userData.player_data;
|
|
|
|
// Check if the user is on a team for the specified game
|
|
const isOnTeam = playerData.teams && Object.values(playerData.teams)
|
|
.some(team => team.game_name === gameFilter);
|
|
|
|
// Check if the user has played the specified game
|
|
const hasPlayedGame = playerData.stats?.games &&
|
|
Object.keys(playerData.stats.games)
|
|
.some(game => game.toLowerCase() === gameFilter.toLowerCase());
|
|
|
|
if (!hasPlayedGame) {
|
|
const teamMessage = isOnTeam ?
|
|
`\nThey are on a team for ${gameFilter} but haven't played any matches yet.` :
|
|
'';
|
|
await interaction.editReply({
|
|
content: `✅ User found, but they haven't played ${gameFilter} yet.${teamMessage}`
|
|
});
|
|
return;
|
|
}
|
|
|
|
const embed = EmbedBuilders.createUserEmbed(playerData, gameFilter, this.playerService);
|
|
const row = this.createActionRow(playerData.username);
|
|
|
|
await interaction.editReply({
|
|
embeds: [embed],
|
|
components: [row]
|
|
});
|
|
} catch (error) {
|
|
this.logger.error('Error in handleFindUser:', {
|
|
error: error.message,
|
|
username: interaction.options.getString('username'),
|
|
game: interaction.options.getString('game'),
|
|
userId: interaction.user.id,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
await interaction.editReply({
|
|
content: '❌ An error occurred while searching for the user.'
|
|
});
|
|
}
|
|
}
|
|
|
|
async handleMatchHistory(interaction) {
|
|
try {
|
|
const username = interaction.options.getString('username');
|
|
const gameFilter = interaction.options.getString('game');
|
|
|
|
const userData = await this.playerService.findUserByUsername(username);
|
|
if (!userData || !userData.success) {
|
|
await interaction.editReply({
|
|
content: '❌ User not found or an error occurred while fetching data.',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const playerData = typeof userData.player_data === 'string'
|
|
? JSON.parse(userData.player_data)
|
|
: userData.player_data;
|
|
|
|
// Extract matches from player data
|
|
const matches = Object.values(playerData.matches || {})
|
|
.filter(match => !gameFilter || match.game_name.toLowerCase() === gameFilter.toLowerCase())
|
|
.sort((a, b) => new Date(b.start_time) - new Date(a.start_time))
|
|
.slice(0, 10);
|
|
|
|
const embed = EmbedBuilders.createMatchHistoryEmbed(playerData, matches);
|
|
const row = EmbedBuilders.createActionRow(playerData.username, this.playerService);
|
|
|
|
await interaction.editReply({
|
|
embeds: [embed],
|
|
components: [row],
|
|
});
|
|
} catch (error) {
|
|
this.logger.error('Error in handleMatchHistory:', {
|
|
error: error.message,
|
|
username: interaction.options.getString('username'),
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async handleFindTeam(interaction) {
|
|
try {
|
|
const teamName = interaction.options.getString('teamname');
|
|
const gameFilter = interaction.options.getString('game');
|
|
|
|
// Input validation
|
|
if (!teamName || typeof teamName !== 'string') {
|
|
await interaction.editReply({
|
|
content: '❌ Invalid team name provided.'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Enhanced sanitization - only allow alphanumeric characters, spaces, and specific symbols
|
|
// Limit the length to prevent buffer overflow attacks
|
|
const sanitizedTeamName = teamName
|
|
.replace(/[^a-zA-Z0-9\s\-_.]/g, '') // Remove any characters that aren't alphanumeric, space, hyphen, underscore, or period
|
|
.trim()
|
|
.slice(0, 100); // Limit length to 100 characters
|
|
|
|
if (!sanitizedTeamName) {
|
|
await interaction.editReply({
|
|
content: '❌ Team name must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Log sanitized input for monitoring
|
|
this.logger.debug('Team search input:', {
|
|
original: teamName,
|
|
sanitized: sanitizedTeamName,
|
|
game: gameFilter,
|
|
userId: interaction.user.id,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Game filter is pre-validated by Discord's slash command choices
|
|
const teamData = await this.playerService.findTeamByName(sanitizedTeamName, gameFilter);
|
|
|
|
if (!teamData || !teamData.success || !teamData.teams || teamData.teams.length === 0) {
|
|
await interaction.editReply({
|
|
content: '❌ No teams found matching your search criteria.'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const embeds = EmbedBuilders.createTeamEmbed(teamData.teams);
|
|
|
|
await interaction.editReply({
|
|
embeds: embeds,
|
|
components: []
|
|
});
|
|
} catch (error) {
|
|
this.logger.error('Error in handleFindTeam:', {
|
|
error: error.message,
|
|
teamName: interaction.options.getString('teamname'),
|
|
game: interaction.options.getString('game'),
|
|
userId: interaction.user.id,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
await interaction.editReply({
|
|
content: '❌ An error occurred while searching for teams.'
|
|
});
|
|
}
|
|
}
|
|
|
|
createActionRow(username) {
|
|
return new ActionRowBuilder().addComponents(
|
|
new ButtonBuilder()
|
|
.setLabel('🔵 View Profile')
|
|
.setStyle(ButtonStyle.Link)
|
|
.setURL(`https://www.vrbattles.gg/profile/${username}`),
|
|
new ButtonBuilder()
|
|
.setLabel('🟡 Join Main Discord')
|
|
.setStyle(ButtonStyle.Link)
|
|
.setURL('https://discord.gg/j3DKVATPGQ')
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = CommandHandler; |