Skip to main content

Overview

RaidBot follows a modular architecture with clear separation of concerns. The codebase consists of 68 JavaScript files organized into logical directories.
wizbot/
├── bot.js                    # Main entry point
├── package.json              # Dependencies and scripts
├── config.example.json       # Configuration template
├── commands/                 # Slash command handlers (18 files)
├── raids/                    # Raid logic and reactions (2 files)
├── utils/                    # Utility functions (18 files)
├── db/                       # Database layer (6 files)
├── tests/                    # Test suites (15 files)
├── data/                     # Runtime data (gitignored)
│   └── wizbot.db            # SQLite database
└── *.js                      # Feature modules (10 files)

Root Directory

Core Files

bot.js
file
Main application entry point (413 lines)Responsibilities:
  • Initialize Discord client with required intents
  • Load state from database on startup
  • Register slash commands
  • Route interactions to handlers
  • Handle graceful shutdown
bot.js
const client = new Client({
    intents: [
        GatewayIntentBits.Guilds,
        GatewayIntentBits.GuildMessages,
        GatewayIntentBits.GuildMessageReactions,
        GatewayIntentBits.GuildMembers
    ],
    partials: [Partials.Message, Partials.Channel, Partials.Reaction]
});

client.once('clientReady', async () => {
    // Load all state
    loadRaidChannels();
    loadGuildSettings();
    
    // Initialize systems
    await registerCommands();
    await reinitializeRaids(client);
    startReminderScheduler();
});
state.js
file
State management layer (800+ lines)Hybrid state management with in-memory caches backed by SQLite:
  • activeRaids - Map of active raid data by message ID
  • raidChannels - Guild → channel ID mappings
  • guildSettings - Guild configuration cache
  • raidStats - User participation statistics
state.js
// In-memory caches for fast access
const activeRaids = new Map();
const raidChannels = new Map();
const guildSettings = new Map();

// Database-backed setters
function setActiveRaid(messageId, raidData) {
    activeRaids.set(messageId, raidData);
    // Immediately persist to database
    statements.insertRaid.run(serializeRaid(messageId, raidData));
}
reminderScheduler.js
file
Automated reminder and auto-close system (400+ lines)Runs every 60 seconds to:
  • Send creator reminders before raid starts
  • Send participant reminders to all signups
  • Auto-close full raids at configured threshold
  • Auto-close museum raids at start time
  • Check and spawn recurring raids
reminderScheduler.js
const CHECK_INTERVAL_MS = 60 * 1000;

async function runReminderCheck() {
    for (const [messageId, raidData] of activeRaids.entries()) {
        const secondsUntil = raidData.timestamp - now;
        
        if (secondsUntil <= settings.creatorReminderSeconds) {
            await sendCreatorReminder(client, raidData);
        }
        
        if (isRaidFull(raidData) && secondsUntil <= settings.autoCloseSeconds) {
            await autoCloseRaid(client, raidData);
        }
    }
}

Feature Modules

recurringManager.js
file
Schedule recurring raids with cron-like patterns (weekly, daily, custom intervals)
availabilityManager.js
file
User availability tracking with heatmap aggregation for 50+ users
pollManager.js
file
Time slot polling system with live vote tracking
templatesManager.js
file
Per-guild raid template customization (enable/disable, rename)
auditLog.js
file
Audit logging for admin actions and signup modifications
presence.js
file
Bot status updates with active raid count

Commands Directory

18 slash command handlers implementing Discord.js command builders.

Command Structure

commands/example.js
const { SlashCommandBuilder } = require('discord.js');

module.exports = {
    data: new SlashCommandBuilder()
        .setName('commandname')
        .setDescription('Description'),
    requiresManageGuild: true,  // Optional permission check
    async execute(interaction) {
        // Command logic
    }
};

Key Commands

Modal-based raid creation flow with natural language time parsing.Features:
  • Template selection for different raid types
  • Natural language time input (“tomorrow 7pm”, “next Friday 6:30pm”)
  • Optional duration and strategy fields
  • Automatic reaction setup based on template
commands/create.js
async execute(interaction) {
    const modal = new ModalBuilder()
        .setCustomId('raid:create')
        .setTitle('Create Raid')
        .addComponents(
            new ActionRowBuilder().addComponents(
                new TextInputBuilder()
                    .setCustomId('datetime')
                    .setLabel('Date and Time')
                    .setPlaceholder('tomorrow 7pm, next Friday 6:30pm')
            )
        );
    
    await interaction.showModal(modal);
}
Comprehensive raid management with action buttons.Actions:
  • Close signups (manual close with analytics)
  • Reopen signups (with confirmation)
  • Delete raid (with cascade cleanup)
  • Change time (reschedule with notifications)
  • Mark attendance (track no-shows)
commands/raid.js
const row = new ActionRowBuilder().addComponents(
    new ButtonBuilder()
        .setCustomId('raid:close')
        .setLabel('Close Signups')
        .setStyle(ButtonStyle.Danger),
    new ButtonBuilder()
        .setCustomId('raid:reopen')
        .setLabel('Reopen')
        .setStyle(ButtonStyle.Success)
);
Comprehensive statistics with multiple views.Views:
  • User stats (personal participation, role distribution)
  • Server stats (leaderboard, top roles)
  • Weekly/monthly trends
  • Inactive member detection
  • CSV export for external analysis
commands/stats.js
.addSubcommand(subcommand =>
    subcommand
        .setName('user')
        .setDescription('View user statistics')
        .addUserOption(option =>
            option.setName('user')
                .setDescription('User to view stats for')
        )
)
Manage recurring raid schedules with spawn time customization.Subcommands:
  • create - Set up recurring schedule
  • list - View active schedules
  • delete - Remove schedule
  • toggle - Enable/disable without deleting
  • trigger - Manually spawn next raid
commands/recurring.js
.addStringOption(option =>
    option.setName('interval')
        .setDescription('How often to create raids')
        .setRequired(true)
        .addChoices(
            { name: 'Daily', value: 'daily' },
            { name: 'Weekly', value: 'weekly' },
            { name: 'Custom', value: 'custom' }
        )
)

Full Command List

FileCommandLinesDescription
availability.js/availability900+User availability tracking and heatmaps
changelog.js/changelog100View release notes
create.js/create1,100+Interactive raid/museum creation
help.js/help500+Command documentation
leaderboard.js/leaderboard300+Server participation leaderboard
permissionspanel.js/permissions250+Role-based permission configuration
ping.js/ping50Bot health check
poll.js/poll400+Time slot polling
raid.js/raid1,500+Raid management panel
raidinfo.js/raidinfo400+List/view/export raids
raidsignup.js/raidsignup1,200+Admin signup modifications
recurring.js/recurring1,200+Recurring raid schedules
setchannel.js/setchannel400+Configure raid channels
settings.js/settings450+Reminder and auto-close settings
stats.js/stats600+Unified analytics
templates.js/templates750+Template customization
testalert.js/testalert50Test performance alerts
index.js
file
Command registry that exports all commands:
commands/index.js
module.exports = [
    require('./availability'),
    require('./create'),
    require('./raid'),
    // ... all commands
];
registerCommands.js
file
Registers slash commands with Discord API:
commands/registerCommands.js
async function registerCommands(clientId, token, commands) {
    const rest = new REST({ version: '10' }).setToken(token);
    const commandData = commands.map(cmd => cmd.data.toJSON());
    
    await rest.put(
        Routes.applicationCommands(clientId),
        { body: commandData }
    );
}

Raids Directory

Core raid logic and reaction handling.
reactionHandlers.js
file
Concurrency-safe reaction processing (600+ lines)Handles all reaction-based signups with mutex locks to prevent race conditions:
raids/reactionHandlers.js
const locks = new Map(); // messageId -> Mutex

async function handleReactionAdd(reaction, user) {
    // Acquire lock for this specific raid
    const lock = getLock(reaction.message.id);
    const release = await lock.acquire();
    
    try {
        // Rate limit check
        if (!reactionLimiter.isAllowed(user.id)) {
            await reaction.users.remove(user.id);
            return;
        }
        
        // Process signup atomically
        const raidData = activeRaids.get(reaction.message.id);
        if (raidData.closed) {
            await reaction.users.remove(user.id);
            return;
        }
        
        // Add to role or waitlist
        const roleIndex = raidData.signups.findIndex(
            r => r.emoji === reaction.emoji.name
        );
        // ... signup logic
    } finally {
        release();
    }
}
Features:
  • Per-raid mutex locks prevent concurrent modifications
  • Rate limiting (5 reactions per 10 seconds per user)
  • Automatic waitlist management with DM notifications
  • Support for raid, museum, key boss, and challenge mode signups
reinitialize.js
file
Raid state recovery on bot restart (1,100+ lines)Reinitializes all active raids from database:
raids/reinitialize.js
async function reinitializeRaids(client) {
    for (const [messageId, raidData] of activeRaids.entries()) {
        try {
            // Fetch raid message from Discord
            const channel = await client.channels.fetch(raidData.channelId);
            const message = await channel.messages.fetch(messageId);
            
            // Re-add reactions if missing
            for (const role of raidData.signups) {
                if (!message.reactions.cache.has(role.emoji)) {
                    await message.react(role.emoji);
                }
            }
            
            logger.info('Raid reinitialized', { raidId: raidData.raidId });
        } catch (error) {
            logger.warn('Failed to reinitialize raid', { messageId, error });
        }
    }
}

Utils Directory

18 utility modules providing shared functionality.

Core Utilities

Contextual logging with multiple severity levels.
utils/logger.js
const logger = new Logger({
    level: process.env.LOG_LEVEL || 'INFO',
    colorize: true,
    logToFile: process.env.LOG_TO_FILE === 'true'
});

logger.info('Raid created', {
    raidId: 'A1',
    guildId: '123',
    userId: '456'
});

// [2026-03-03T10:30:00.000Z] [INFO] [wizbot] Raid created
Features:
  • DEBUG, INFO, WARN, ERROR levels
  • Structured JSON output
  • File rotation at 5MB
  • Child loggers with context
Circuit breaker pattern for Discord API resilience.
utils/circuitBreaker.js
const discordApiBreaker = new CircuitBreaker({
    name: 'discord-api',
    failureThreshold: 5,      // Open after 5 failures
    successThreshold: 2,      // Close after 2 successes
    timeout: 60000            // Try half-open after 1 minute
});

await discordApiBreaker.execute(async () => {
    await channel.send('Message');
}, () => {
    // Fallback function
    logger.warn('Circuit open, skipping message');
});
States:
  • CLOSED - Normal operation
  • OPEN - Reject immediately
  • HALF_OPEN - Testing recovery
Sliding window rate limiting for reactions and commands.
utils/rateLimiter.js
const reactionLimiter = new RateLimiter({ 
    maxRequests: 5, 
    windowMs: 10000 
});

if (!reactionLimiter.isAllowed(user.id)) {
    return 'You\'re reacting too quickly!';
}
Embed generation and raid manipulation functions.
utils/raidHelpers.js
function updateRaidEmbed(message, raidData) {
    const embed = new EmbedBuilder()
        .setTitle(raidData.template.name)
        .setDescription(`**Time:** ${formatTimeLabel(raidData)}\n**Strategy:** ${raidData.strategy || 'None'}`);
    
    for (const role of raidData.signups) {
        const users = role.users.map(id => `<@${id}>`).join(', ');
        const waitlist = role.waitlist.length > 0 
            ? `\n*Waitlist:* ${role.waitlist.map(id => `<@${id}>`).join(', ')}`
            : '';
        embed.addFields({
            name: `${role.emoji} ${role.name} (${role.users.length}/${role.slots})`,
            value: users || 'No signups' + waitlist
        });
    }
    
    return message.edit({ embeds: [embed] });
}

Full Utility List

FilePurposeKey Functions
alerts.jsPerformance monitoringinitializeAlerts(), sendAlert()
analytics.jsStats aggregationcalculateTrends(), getInactiveMembers()
circuitBreaker.jsFault tolerancewithCircuitBreaker(), sendDMWithBreaker()
config.jsConfiguration loadingLoads config.json or environment variables
dmRetry.jsDM delivery retrysendDMWithRetry()
errorMessages.jsUser-friendly errorsformatError(), getErrorMessage()
idGenerator.jsRaid ID generationgenerateRaidId() (A1-Z9 format)
logger.jsStructured logginglogger.info(), logger.error()
metrics.jsPrometheus metricsincrementCounter(), recordHistogram()
raidFormatters.jsTime/link formattingformatTimeLabel(), buildMessageLink()
raidHelpers.jsRaid manipulationupdateRaidEmbed(), closeRaidSignup()
raidTypes.jsType checkingisTeamBased(), getTeamTypeLabel()
rateLimiter.jsRate limitingreactionLimiter.isAllowed()
timezoneHelper.jsTimezone conversionparseTimezone(), convertToUTC()
userLabels.jsUser display namesgetUserLabel(), getUserLabels()
validators.jsInput validationvalidateTimezone(), validateDays()
waitlistNotifications.jsWaitlist promotionprocessWaitlistOpenings()

Database Directory

SQLite database layer with migrations and utilities.
database.js
file
Database connection and schema management (174 lines)
db/database.js
const db = new Database(DB_PATH);

// Enable WAL mode for concurrent performance
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');

function initializeSchema() {
    const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
    db.exec(schema);
    runMigrations();
}

function prepare(sql) {
    return db.prepare(sql);
}

function transaction(fn) {
    return db.transaction(fn);
}
schema.sql
file
Database schema definition (200+ lines)Tables:
  • guilds - Guild configuration
  • raids - Active and historical raids
  • signups - Normalized signup data
  • user_stats - Global user statistics
  • guild_user_stats - Per-guild statistics
  • recurring_raids - Scheduled raids
  • availability - User availability windows
  • polls - Time slot polls
  • admin_roles - Permission configuration
schema.sql
CREATE TABLE IF NOT EXISTS raids (
  message_id TEXT PRIMARY KEY,
  raid_id TEXT,
  guild_id TEXT,
  type TEXT DEFAULT 'raid',
  timestamp INTEGER,
  creator_id TEXT,
  closed_at INTEGER,
  version INTEGER DEFAULT 1,
  FOREIGN KEY (guild_id) REFERENCES guilds(id)
);
migrate.js
file
JSON to SQLite migration script (500+ lines)Imports legacy JSON data files into SQLite database:
node db/migrate.js

Utility Scripts

FilePurpose
import-csv-stats.jsImport stats from CSV files
repair-stats.jsRecalculate stats from raid history
restore-stats.jsRestore stats from backup

Tests Directory

15 test files using Node.js built-in test runner.
tests/example.test.js
const test = require('node:test');
const assert = require('node:assert/strict');

test('description', () => {
    assert.equal(actual, expected);
});

Test Coverage

FileTestsCoverage Area
statePersistence.test.jsState managementactiveRaids persistence
waitlistNotifications.test.jsWaitlist logicPromotion and DM sending
reactionHandlers.test.jsReaction handlingSignup processing
rateLimiter.test.jsRate limitingSliding window algorithm
validators.test.jsInput validationTimezone, time, role validation
chrono.test.jsTime parsingNatural language dates
permissions.test.jsAccess controlRole-based permissions
poll.test.jsPolling systemVote recording
availability.test.jsAvailabilityTime window parsing
See the Testing Guide for how to run tests.

Data Directory

Runtime data storage (excluded from git).
data/
├── wizbot.db          # SQLite database (main)
├── wizbot.db-shm      # Shared memory (WAL mode)
├── wizbot.db-wal      # Write-ahead log (WAL mode)
Never commit the data/ directory to version control. It contains production data and is excluded in .gitignore.

Configuration

config.json
file
Bot configuration (not in repo, created from example):
config.json
{
  "clientId": "YOUR_CLIENT_ID",
  "token": "YOUR_BOT_TOKEN",
  "allowedGuildIds": ["GUILD_ID_1"],
  "ownerId": "YOUR_USER_ID"
}
package.json
file
Dependencies and scripts:
package.json
{
  "scripts": {
    "test": "node --test --test-force-exit"
  },
  "dependencies": {
    "discord.js": "^14.24.2",
    "better-sqlite3": "^12.5.0",
    "chrono-node": "^2.9.0",
    "async-mutex": "^0.5.0",
    "dotenv": "^17.2.3"
  }
}
1

Start with bot.js

Understand the initialization flow and event handling
2

Explore state.js

Learn how data is stored and retrieved
3

Read command files

See how user interactions are handled
4

Study raid logic

Understand reaction handling and concurrency control
5

Review utilities

Learn about reusable functions and patterns

Next Steps

Architecture

Understand the system design

Testing

Write and run tests

Contributing

Start contributing code

Build docs developers (and LLMs) love