🤖 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
- 📅 Event Management - Check upcoming events and player availability
- 👥 Player Coordination - Manage team selections and player status
- 🔔 Automated Notifications - Remind players about upcoming shows
- 📊 Availability Tracking - Real-time availability status updates
- 🎭 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 InfoTechnical 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
- Discord Application - Create a Discord application in the Developer Portal
- Bot Token - Generate a bot token for authentication
- Server Access - Bot must be invited to your Discord server
- Environment Variables - Configure bot settings
Environment Setup
# .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-secretDiscord Application Setup
Create Application
- Go to Discord Developer Portal
- Click "New Application"
- Name it "LIPAIX Bot"
- Copy the Application ID
Create Bot User
- Go to "Bot" section
- Click "Add Bot"
- Copy the Bot Token
- Enable required permissions
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
Set Bot Permissions
- Send Messages
- Use Slash Commands
- Embed Links
- Read Message History
- Add Reactions
⚙️ Bot Configuration
Main Configuration
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
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:
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.
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.
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:
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:
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:
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:
[build]
builder = "nixpacks"
[deploy]
startCommand = "pnpm start"
healthcheckPath = "/health"
restartPolicyType = "on_failure"Environment Variables
# 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=3002Health Checks
The bot includes health check endpoints for monitoring:
// 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
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
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
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
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
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
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
