Skip to content

🤖 Discord Bot

The LIPAIX Discord bot provides real-time communication and management tools for players and event coordinators. This guide covers everything you need to know about the bot's architecture, commands, and integration with the backend.

🌟 Bot Overview

What is the Discord Bot?

The LIPAIX Discord Bot is a custom Discord application that helps manage player communications, event coordination, and team organization. It integrates directly with the PayloadCMS backend to provide real-time information and automated workflows.

Key Features

  1. 📅 Event Management - Check upcoming events and player availability
  2. 👥 Player Coordination - Manage team selections and player status
  3. 🔔 Automated Notifications - Remind players about upcoming shows
  4. 📊 Availability Tracking - Real-time availability status updates
  5. 🎭 Team Building - Organize players into teams for shows

Why Discord for LIPAIX?

  • Real-time Communication - Instant updates and notifications
  • Familiar Interface - Most players already use Discord
  • Rich Interactions - Slash commands, embeds, and reactions
  • Integration - Easy to connect with external APIs
  • Mobile Support - Access from anywhere, anytime

🏗️ Bot Architecture

High-Level Architecture

Discord Server → Discord Bot → LIPAIX Backend → PostgreSQL Database
      ↓              ↓              ↓              ↓
User Commands  Command Handler  API Requests   Data Storage
Notifications  Event Handler    Business Logic  Player Data
Team Updates   Response Formatter Response Data  Event Info

Technical Stack

  • Discord.js - Official Discord API library for Node.js
  • TypeScript - Type safety and better developer experience
  • Express - HTTP server for health checks and webhooks
  • tsup - Fast TypeScript bundler
  • dotenv - Environment variable management

Project Structure

apps/discord-bot/
├── src/
│   ├── actions/              # Bot action handlers
│   │   ├── handleInteractions.ts
│   │   ├── listCommands.ts
│   │   └── registerCommands.ts
│   ├── commands/             # Slash command definitions
│   │   ├── commands.ts
│   │   ├── disposCommand.ts
│   │   └── selecsCommand.ts
│   ├── core/                 # Core bot logic
│   │   └── injection.ts
│   ├── index.ts              # Bot entry point
│   └── types.ts              # Bot-specific types
├── package.json              # Dependencies and scripts
├── tsconfig.json             # TypeScript configuration
└── README.md                 # Bot documentation

🚀 Getting Started

Prerequisites

  1. Discord Application - Create a Discord application in the Developer Portal
  2. Bot Token - Generate a bot token for authentication
  3. Server Access - Bot must be invited to your Discord server
  4. Environment Variables - Configure bot settings

Environment Setup

bash
# .env.local
DISCORD_TOKEN=your-bot-token-here
DISCORD_CLIENT_ID=your-application-id
DISCORD_GUILD_ID=your-server-id

# Backend integration
PAYLOAD_URL=http://localhost:3000
PAYLOAD_SECRET=your-payload-secret

Discord Application Setup

  1. Create Application

  2. Create Bot User

    • Go to "Bot" section
    • Click "Add Bot"
    • Copy the Bot Token
    • Enable required permissions
  3. Configure OAuth2

    • Go to "OAuth2" → "URL Generator"
    • Select scopes: bot, applications.commands
    • Select permissions: Send Messages, Use Slash Commands
    • Use generated URL to invite bot to server
  4. Set Bot Permissions

    • Send Messages
    • Use Slash Commands
    • Embed Links
    • Read Message History
    • Add Reactions

⚙️ Bot Configuration

Main Configuration

typescript
import { Client, GatewayIntentBits, Collection } from 'discord.js'
import { config } from 'dotenv'
import express from 'express'
import { handleInteractions } from './actions/handleInteractions'
import { registerCommands } from './actions/registerCommands'

// Load environment variables
config()

// Create Discord client
const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
})

// Command collection
client.commands = new Collection()

// Bot ready event
client.once('ready', async () => {
  console.log(`🤖 Bot is ready! Logged in as ${client.user?.tag}`)
  
  try {
    // Register slash commands
    await registerCommands(client)
    console.log('✅ Slash commands registered successfully')
  } catch (error) {
    console.error('❌ Failed to register slash commands:', error)
  }
})

// Handle interactions (slash commands)
client.on('interactionCreate', async (interaction) => {
  try {
    await handleInteractions(interaction)
  } catch (error) {
    console.error('❌ Error handling interaction:', error)
    
    if (interaction.isRepliable()) {
      await interaction.reply({
        content: '❌ An error occurred while processing your command.',
        ephemeral: true,
      })
    }
  }
})

// Error handling
client.on('error', (error) => {
  console.error('❌ Discord client error:', error)
})

process.on('unhandledRejection', (error) => {
  console.error('❌ Unhandled promise rejection:', error)
})

// Start Express server for health checks
const app = express()
const PORT = process.env.PORT || 3002

app.get('/health', (req, res) => {
  res.json({ status: 'ok', bot: 'online' })
})

app.listen(PORT, () => {
  console.log(`🌐 Health check server running on port ${PORT}`)
})

// Login to Discord
client.login(process.env.DISCORD_TOKEN)

Command Registration

typescript
import { Client, REST, Routes, SlashCommandBuilder } from 'discord.js'
import { disposCommand } from '../commands/disposCommand'
import { selecsCommand } from '../commands/selecsCommand'

export async function registerCommands(client: Client) {
  const commands = [
    disposCommand.data,
    selecsCommand.data,
  ]

  const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN!)

  try {
    console.log('🔄 Registering slash commands...')

    if (process.env.DISCORD_GUILD_ID) {
      // Register commands for specific guild (faster for development)
      await rest.put(
        Routes.applicationGuildCommands(
          process.env.DISCORD_CLIENT_ID!,
          process.env.DISCORD_GUILD_ID
        ),
        { body: commands }
      )
      console.log('✅ Guild commands registered successfully')
    } else {
      // Register commands globally (slower, but works everywhere)
      await rest.put(
        Routes.applicationCommands(process.env.DISCORD_CLIENT_ID!),
        { body: commands }
      )
      console.log('✅ Global commands registered successfully')
    }
  } catch (error) {
    console.error('❌ Failed to register commands:', error)
    throw error
  }
}

🎯 Slash Commands

Command Structure

Each slash command follows a consistent structure:

typescript
import { SlashCommandBuilder, SlashCommandSubcommandsOnlyBuilder } from 'discord.js'

export interface Command {
  data: SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder
  execute: (interaction: any) => Promise<void>
}

Dispos Command (Player Availability)

The /dispos command allows players to check and manage their availability for upcoming events.

typescript
import { SlashCommandBuilder, CommandInteraction, EmbedBuilder } from 'discord.js'
import { Command } from './commands'

export const disposCommand: Command = {
  data: new SlashCommandBuilder()
    .setName('dispos')
    .setDescription('Gérer vos disponibilités pour les événements')
    .addSubcommand(subcommand =>
      subcommand
        .setName('check')
        .setDescription('Vérifier vos disponibilités')
        .addStringOption(option =>
          option
            .setName('event')
            .setDescription('ID de l\'événement (optionnel)')
            .setRequired(false)
        )
    )
    .addSubcommand(subcommand =>
      subcommand
        .setName('update')
        .setDescription('Mettre à jour votre disponibilité')
        .addStringOption(option =>
          option
            .setName('event')
            .setDescription('ID de l\'événement')
            .setRequired(true)
        )
        .addStringOption(option =>
          option
            .setName('status')
            .setDescription('Votre disponibilité')
            .setRequired(true)
            .addChoices(
              { name: '✅ Disponible', value: 'available' },
              { name: '❌ Indisponible', value: 'unavailable' },
              { name: '🤔 Peut-être', value: 'maybe' }
            )
        )
        .addStringOption(option =>
          option
            .setName('notes')
            .setDescription('Notes additionnelles (optionnel)')
            .setRequired(false)
        )
    ),

  async execute(interaction: CommandInteraction) {
    const subcommand = interaction.options.getSubcommand()
    
    try {
      switch (subcommand) {
        case 'check':
          await this.handleCheck(interaction)
          break
        case 'update':
          await this.handleUpdate(interaction)
          break
        default:
          await interaction.reply({
            content: '❌ Commande non reconnue',
            ephemeral: true,
          })
      }
    } catch (error) {
      console.error('Error in dispos command:', error)
      await interaction.reply({
        content: '❌ Une erreur est survenue lors du traitement de votre commande',
        ephemeral: true,
      })
    }
  },

  async handleCheck(interaction: CommandInteraction) {
    const eventId = interaction.options.getString('event')
    
    try {
      // Fetch availability data from backend
      const response = await fetch(
        `${process.env.PAYLOAD_URL}/api/availabilities?where[player][equals]=${interaction.user.id}${eventId ? `&where[event][equals]=${eventId}` : ''}`
      )
      
      if (!response.ok) {
        throw new Error('Failed to fetch availability data')
      }
      
      const data = await response.json()
      
      if (data.docs.length === 0) {
        await interaction.reply({
          content: '📋 Aucune disponibilité trouvée pour vos événements',
          ephemeral: true,
        })
        return
      }
      
      // Create availability embed
      const embed = new EmbedBuilder()
        .setTitle('📋 Vos Disponibilités')
        .setColor(0x00ff00)
        .setTimestamp()
      
      for (const availability of data.docs) {
        const statusEmoji = {
          'available': '',
          'unavailable': '',
          'maybe': '🤔',
          'unanswered': ''
        }[availability.status]
        
        embed.addFields({
          name: `${statusEmoji} ${availability.event.title}`,
          value: `**Date:** ${new Date(availability.event.date).toLocaleDateString('fr-FR')}\n**Statut:** ${availability.status}\n${availability.notes ? `**Notes:** ${availability.notes}` : ''}`,
          inline: false,
        })
      }
      
      await interaction.reply({ embeds: [embed], ephemeral: true })
      
    } catch (error) {
      console.error('Error checking availability:', error)
      await interaction.reply({
        content: '❌ Erreur lors de la récupération de vos disponibilités',
        ephemeral: true,
      })
    }
  },

  async handleUpdate(interaction: CommandInteraction) {
    const eventId = interaction.options.getString('event')!
    const status = interaction.options.getString('status')!
    const notes = interaction.options.getString('notes')
    
    try {
      // Check if availability already exists
      const existingResponse = await fetch(
        `${process.env.PAYLOAD_URL}/api/availabilities?where[player][equals]=${interaction.user.id}&where[event][equals]=${eventId}`
      )
      
      if (!existingResponse.ok) {
        throw new Error('Failed to check existing availability')
      }
      
      const existingData = await existingResponse.json()
      let response
      
      if (existingData.docs.length > 0) {
        // Update existing availability
        const availabilityId = existingData.docs[0].id
        response = await fetch(
          `${process.env.PAYLOAD_URL}/api/availabilities/${availabilityId}`,
          {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ status, notes }),
          }
        )
      } else {
        // Create new availability
        response = await fetch(
          `${process.env.PAYLOAD_URL}/api/availabilities`,
          {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              player: interaction.user.id,
              event: eventId,
              status,
              notes,
            }),
          }
        )
      }
      
      if (!response.ok) {
        throw new Error('Failed to update availability')
      }
      
      const statusEmoji = {
        'available': '',
        'unavailable': '',
        'maybe': '🤔'
      }[status]
      
      await interaction.reply({
        content: `${statusEmoji} Votre disponibilité a été mise à jour avec succès!`,
        ephemeral: true,
      })
      
    } catch (error) {
      console.error('Error updating availability:', error)
      await interaction.reply({
        content: '❌ Erreur lors de la mise à jour de votre disponibilité',
        ephemeral: true,
      })
    }
  },
}

Selecs Command (Team Selections)

The /selecs command allows event coordinators to view and manage team selections for upcoming shows.

typescript
import { SlashCommandBuilder, CommandInteraction, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'
import { Command } from './commands'

export const selecsCommand: Command = {
  data: new SlashCommandBuilder()
    .setName('selecs')
    .setDescription('Gérer les sélections d\'équipe pour les événements')
    .addSubcommand(subcommand =>
      subcommand
        .setName('list')
        .setDescription('Lister les sélections d\'équipe')
        .addStringOption(option =>
          option
            .setName('event')
            .setDescription('ID de l\'événement (optionnel)')
            .setRequired(false)
        )
    )
    .addSubcommand(subcommand =>
      subcommand
        .setName('create')
        .setDescription('Créer une nouvelle sélection d\'équipe')
        .addStringOption(option =>
          option
            .setName('event')
            .setDescription('ID de l\'événement')
            .setRequired(true)
        )
        .addStringOption(option =>
          option
            .setName('name')
            .setDescription('Nom de la sélection')
            .setRequired(true)
        )
    ),

  async execute(interaction: CommandInteraction) {
    const subcommand = interaction.options.getSubcommand()
    
    try {
      switch (subcommand) {
        case 'list':
          await this.handleList(interaction)
          break
        case 'create':
          await this.handleCreate(interaction)
          break
        default:
          await interaction.reply({
            content: '❌ Commande non reconnue',
            ephemeral: true,
          })
      }
    } catch (error) {
      console.error('Error in selecs command:', error)
      await interaction.reply({
        content: '❌ Une erreur est survenue lors du traitement de votre commande',
        ephemeral: true,
      })
    }
  },

  async handleList(interaction: CommandInteraction) {
    const eventId = interaction.options.getString('event')
    
    try {
      // Fetch team selections from backend
      const response = await fetch(
        `${process.env.PAYLOAD_URL}/api/selections${eventId ? `?where[event][equals]=${eventId}` : ''}`
      )
      
      if (!response.ok) {
        throw new Error('Failed to fetch team selections')
      }
      
      const data = await response.json()
      
      if (data.docs.length === 0) {
        await interaction.reply({
          content: '👥 Aucune sélection d\'équipe trouvée',
          ephemeral: true,
        })
        return
      }
      
      // Create selections embed
      const embed = new EmbedBuilder()
        .setTitle('👥 Sélections d\'Équipe')
        .setColor(0x0099ff)
        .setTimestamp()
      
      for (const selection of data.docs) {
        const playerCount = selection.players?.length || 0
        const maxPlayers = selection.maxPlayers || 'Non défini'
        
        embed.addFields({
          name: `🏆 ${selection.name}`,
          value: `**Événement:** ${selection.event.title}\n**Joueurs:** ${playerCount}/${maxPlayers}\n**Statut:** ${selection.status}`,
          inline: false,
        })
      }
      
      await interaction.reply({ embeds: [embed], ephemeral: true })
      
    } catch (error) {
      console.error('Error listing selections:', error)
      await interaction.reply({
        content: '❌ Erreur lors de la récupération des sélections',
        ephemeral: true,
      })
    }
  },

  async handleCreate(interaction: CommandInteraction) {
    const eventId = interaction.options.getString('event')!
    const name = interaction.options.getString('name')!
    
    try {
      // Create new team selection
      const response = await fetch(
        `${process.env.PAYLOAD_URL}/api/selections`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            name,
            event: eventId,
            status: 'draft',
            players: [],
            maxPlayers: 8, // Default max players
          }),
        }
      )
      
      if (!response.ok) {
        throw new Error('Failed to create team selection')
      }
      
      const selection = await response.json()
      
      await interaction.reply({
        content: `✅ Sélection d'équipe "${name}" créée avec succès!`,
        ephemeral: true,
      })
      
    } catch (error) {
      console.error('Error creating selection:', error)
      await interaction.reply({
        content: '❌ Erreur lors de la création de la sélection',
        ephemeral: true,
      })
    }
  },
}

🔄 Interaction Handling

Command Router

The interaction handler routes slash commands to their appropriate handlers:

typescript
import { Interaction, ChatInputCommandInteraction } from 'discord.js'
import { disposCommand } from '../commands/disposCommand'
import { selecsCommand } from '../commands/selecsCommand'

const commands = new Map([
  ['dispos', disposCommand],
  ['selecs', selecsCommand],
])

export async function handleInteractions(interaction: Interaction) {
  if (!interaction.isChatInputCommand()) {
    return
  }

  const command = commands.get(interaction.commandName)
  
  if (!command) {
    console.warn(`⚠️ Unknown command: ${interaction.commandName}`)
    return
  }

  try {
    await command.execute(interaction)
  } catch (error) {
    console.error(`❌ Error executing command ${interaction.commandName}:`, error)
    
    if (interaction.replied || interaction.deferred) {
      await interaction.followUp({
        content: '❌ Une erreur est survenue lors de l\'exécution de la commande',
        ephemeral: true,
      })
    } else {
      await interaction.reply({
        content: '❌ Une erreur est survenue lors de l\'exécution de la commande',
        ephemeral: true,
      })
    }
  }
}

🔌 Backend Integration

API Integration

The bot integrates with the PayloadCMS backend through REST APIs:

typescript
export class BackendService {
  private baseUrl: string
  private apiKey: string

  constructor() {
    this.baseUrl = process.env.PAYLOAD_URL!
    this.apiKey = process.env.PAYLOAD_SECRET!
  }

  async fetchEvents(filters?: any) {
    const queryParams = new URLSearchParams()
    
    if (filters) {
      Object.entries(filters).forEach(([key, value]) => {
        queryParams.append(`where[${key}][equals]`, value as string)
      })
    }
    
    const response = await fetch(
      `${this.baseUrl}/api/shows?${queryParams.toString()}`,
      {
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
      }
    )
    
    if (!response.ok) {
      throw new Error(`Backend API error: ${response.status}`)
    }
    
    return response.json()
  }

  async updateAvailability(availabilityId: string, data: any) {
    const response = await fetch(
      `${this.baseUrl}/api/availabilities/${availabilityId}`,
      {
        method: 'PATCH',
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      }
    )
    
    if (!response.ok) {
      throw new Error(`Backend API error: ${response.status}`)
    }
    
    return response.json()
  }

  async createTeamSelection(data: any) {
    const response = await fetch(
      `${this.baseUrl}/api/selections`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      }
    )
    
    if (!response.ok) {
      throw new Error(`Backend API error: ${response.status}`)
    }
    
    return response.json()
  }
}

Data Synchronization

The bot keeps data synchronized with the backend:

typescript
export class SyncService {
  private backendService: BackendService
  private syncInterval: NodeJS.Timeout | null = null

  constructor() {
    this.backendService = new BackendService()
  }

  startSync(intervalMs: number = 5 * 60 * 1000) { // 5 minutes
    this.syncInterval = setInterval(async () => {
      try {
        await this.syncData()
      } catch (error) {
        console.error('❌ Sync error:', error)
      }
    }, intervalMs)
    
    console.log('🔄 Data sync started')
  }

  stopSync() {
    if (this.syncInterval) {
      clearInterval(this.syncInterval)
      this.syncInterval = null
      console.log('⏹️ Data sync stopped')
    }
  }

  private async syncData() {
    // Sync events
    const events = await this.backendService.fetchEvents({
      status: 'published',
      date: { greater_than: new Date().toISOString() }
    })
    
    // Sync availabilities
    const availabilities = await this.backendService.fetchAvailabilities()
    
    // Update bot's internal cache
    this.updateCache(events, availabilities)
    
    console.log('✅ Data sync completed')
  }

  private updateCache(events: any, availabilities: any) {
    // Update bot's internal data structures
    // This could include updating command options, cached responses, etc.
  }
}

🚀 Deployment

Railway Deployment

The Discord bot is deployed on Railway alongside the web application:

typescript
[build]
builder = "nixpacks"

[deploy]
startCommand = "pnpm start"
healthcheckPath = "/health"
restartPolicyType = "on_failure"

Environment Variables

bash
# Railway environment variables
DISCORD_TOKEN=your-production-bot-token
DISCORD_CLIENT_ID=your-production-client-id
DISCORD_GUILD_ID=your-production-server-id
PAYLOAD_URL=https://your-domain.com
PAYLOAD_SECRET=your-production-secret
PORT=3002

Health Checks

The bot includes health check endpoints for monitoring:

typescript
// Health check endpoints
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    bot: client.user ? 'online' : 'offline',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
  })
})

app.get('/health/detailed', (req, res) => {
  res.json({
    status: 'ok',
    bot: {
      user: client.user?.tag,
      id: client.user?.id,
      status: client.user ? 'online' : 'offline',
    },
    guilds: client.guilds.cache.size,
    commands: client.commands.size,
    system: {
      uptime: process.uptime(),
      memory: process.memoryUsage(),
      version: process.version,
    },
  })
})

🧪 Testing

Unit Testing

typescript
import { disposCommand } from '../../src/commands/disposCommand'
import { CommandInteraction } from 'discord.js'

// Mock Discord.js
jest.mock('discord.js', () => ({
  SlashCommandBuilder: jest.fn().mockImplementation(() => ({
    setName: jest.fn().mockReturnThis(),
    setDescription: jest.fn().mockReturnThis(),
    addSubcommand: jest.fn().mockReturnThis(),
  })),
}))

// Mock fetch
global.fetch = jest.fn()

describe('DisposCommand', () => {
  let mockInteraction: Partial<CommandInteraction>

  beforeEach(() => {
    mockInteraction = {
      options: {
        getSubcommand: jest.fn(),
        getString: jest.fn(),
      },
      reply: jest.fn(),
      followUp: jest.fn(),
    }
  })

  afterEach(() => {
    jest.clearAllMocks()
  })

  it('should handle check subcommand', async () => {
    mockInteraction.options!.getSubcommand = jest.fn().mockReturnValue('check')
    mockInteraction.options!.getString = jest.fn().mockReturnValue(null)
    
    // Mock successful API response
    (global.fetch as jest.Mock).mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        docs: [
          {
            id: '1',
            status: 'available',
            event: { title: 'Test Event', date: '2024-01-01' },
            notes: 'Test notes',
          },
        ],
      }),
    })

    await disposCommand.execute(mockInteraction as CommandInteraction)

    expect(mockInteraction.reply).toHaveBeenCalledWith(
      expect.objectContaining({
        embeds: expect.arrayContaining([
          expect.objectContaining({
            title: '📋 Vos Disponibilités',
          }),
        ]),
        ephemeral: true,
      })
    )
  })

  it('should handle update subcommand', async () => {
    mockInteraction.options!.getSubcommand = jest.fn().mockReturnValue('update')
    mockInteraction.options!.getString = jest.fn()
      .mockReturnValueOnce('event-1') // event
      .mockReturnValueOnce('available') // status
      .mockReturnValueOnce('Test notes') // notes
    
    // Mock successful API response
    (global.fetch as jest.Mock)
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ docs: [] }), // No existing availability
      })
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ id: 'new-availability' }),
      })

    await disposCommand.execute(mockInteraction as CommandInteraction)

    expect(mockInteraction.reply).toHaveBeenCalledWith({
      content: '✅ Votre disponibilité a été mise à jour avec succès!',
      ephemeral: true,
    })
  })
})

Integration Testing

typescript
import { Client } from 'discord.js'
import { handleInteractions } from '../../src/actions/handleInteractions'

describe('Bot Integration', () => {
  let mockClient: Partial<Client>

  beforeEach(() => {
    mockClient = {
      commands: new Map(),
    }
  })

  it('should handle valid slash commands', async () => {
    // Test command routing and execution
  })

  it('should handle invalid commands gracefully', async () => {
    // Test error handling for unknown commands
  })

  it('should maintain command state', async () => {
    // Test command state management
  })
})

🔒 Security

Authentication & Authorization

typescript
export function requireRole(requiredRole: string) {
  return (interaction: CommandInteraction) => {
    // Check if user has required role in Discord server
    const member = interaction.member as any
    const hasRole = member.roles.cache.some((role: any) => 
      role.name === requiredRole || role.id === requiredRole
    )
    
    if (!hasRole) {
      throw new Error(`Access denied. Required role: ${requiredRole}`)
    }
    
    return true
  }
}

export function requirePermission(permission: string) {
  return (interaction: CommandInteraction) => {
    // Check if user has required Discord permission
    const member = interaction.member as any
    const hasPermission = member.permissions.has(permission)
    
    if (!hasPermission) {
      throw new Error(`Access denied. Required permission: ${permission}`)
    }
    
    return true
  }
}

Input Validation

typescript
export function validateEventId(eventId: string): boolean {
  // Validate event ID format
  return /^[a-zA-Z0-9-_]+$/.test(eventId)
}

export function validateStatus(status: string): boolean {
  // Validate availability status
  const validStatuses = ['available', 'unavailable', 'maybe']
  return validStatuses.includes(status)
}

export function sanitizeInput(input: string): string {
  // Remove potentially dangerous characters
  return input.replace(/[<>\"'&]/g, '')
}

📊 Monitoring & Logging

Logging Strategy

typescript
export class Logger {
  static info(message: string, context?: any) {
    console.log(`[INFO] ${new Date().toISOString()}: ${message}`, context || '')
  }

  static warn(message: string, context?: any) {
    console.warn(`[WARN] ${new Date().toISOString()}: ${message}`, context || '')
  }

  static error(message: string, context?: any) {
    console.error(`[ERROR] ${new Date().toISOString()}: ${message}`, context || '')
  }

  static debug(message: string, context?: any) {
    if (process.env.NODE_ENV === 'development') {
      console.log(`[DEBUG] ${new Date().toISOString()}: ${message}`, context || '')
    }
  }
}

Performance Monitoring

typescript
export class Metrics {
  private static commandExecutions = new Map<string, number>()
  private static commandErrors = new Map<string, number>()
  private static responseTimes = new Map<string, number[]>()

  static recordCommandExecution(commandName: string) {
    const current = this.commandExecutions.get(commandName) || 0
    this.commandExecutions.set(commandName, current + 1)
  }

  static recordCommandError(commandName: string) {
    const current = this.commandErrors.get(commandName) || 0
    this.commandErrors.set(commandName, current + 1)
  }

  static recordResponseTime(commandName: string, timeMs: number) {
    const times = this.responseTimes.get(commandName) || []
    times.push(timeMs)
    this.responseTimes.set(commandName, times)
  }

  static getMetrics() {
    return {
      commandExecutions: Object.fromEntries(this.commandExecutions),
      commandErrors: Object.fromEntries(this.commandErrors),
      averageResponseTimes: Object.fromEntries(
        Array.from(this.responseTimes.entries()).map(([cmd, times]) => [
          cmd,
          times.reduce((a, b) => a + b, 0) / times.length
        ])
      ),
    }
  }
}

🤖 Discord Bot Summary

The LIPAIX Discord Bot provides real-time communication and management tools for players and event coordinators, integrating seamlessly with the PayloadCMS backend.

Key features include:

  • Slash Commands - Easy-to-use bot interactions
  • Real-time Updates - Instant availability and team updates
  • Backend Integration - Seamless connection with PayloadCMS
  • Role-based Access - Secure command execution
  • Health Monitoring - Built-in monitoring and logging

Released under the MIT License.