Module Guide

Discord bot modules and commands

Module Development Guide

Learn how to create new modules and extend the Discord Game Management Bot platform.

Table of Contents


Module Architecture

The bot uses a plugin-based architecture where each feature is a self-contained module. This design provides:

  • Isolation: Modules don't directly depend on each other
  • Scalability: Add features without modifying core code
  • Maintainability: Clear separation of concerns
  • Reusability: Modules can be shared across projects

How Modules Work

  1. Discovery: The Module Loader scans src/modules/ for folders containing an index.js
  2. Initialization: Each module's init() function receives core dependencies
  3. Registration: Module registers its commands, buttons, modals, and events
  4. Routing: Core system routes Discord interactions to the correct module

Module Interface Contract

Every module must export a default object with this structure:

export default {
  // Module metadata
  name: 'module-name',           // Unique identifier
  version: '1.0.0',              // Semantic version
  
  // Lifecycle method
  async init(core) {
    // Called once when bot starts
    // Receive core dependencies (client, db, logger, etc.)
  },
  
  // Optional: Register slash commands
  registerCommands() {
    // Return array of SlashCommandBuilder objects
    return [];
  },
  
  // Optional: Register button interactions
  registerButtons() {
    // Return Map of customId -> handler function
    return new Map();
  },
  
  // Optional: Register modal interactions
  registerModals() {
    // Return Map of customId -> handler function
    return new Map();
  },
  
  // Optional: Register select menu interactions
  registerSelectMenus() {
    // Return Map of customId -> handler function
    return new Map();
  },
  
  // Optional: Register Discord event listeners
  registerEvents() {
    // Return Map of eventName -> handler function
    return new Map();
  }
};

Creating Your First Module

Step 1: Create Module Directory

cd src/modules
mkdir my-feature
cd my-feature

Step 2: Create Module Entry Point

Create src/modules/my-feature/index.js:

export default {
  name: 'my-feature',
  version: '1.0.0',
  
  async init(core) {
    this.core = core;
    core.logger.info('My Feature module initialized');
  },

  registerCommands() {
    return [];
  },

  registerButtons() {
    return new Map();
  },

  registerModals() {
    return new Map();
  },

  registerEvents() {
    return new Map();
  }
};

Step 3: Start the Bot

The Module Loader will automatically discover and load your module:

npm run dev

You should see:

[INFO] Loaded module: my-feature
[INFO] My Feature module initialized

Module Components

Commands

Create slash commands in src/modules/my-feature/commands/:

Example: commands/hello.js

import { SlashCommandBuilder } from 'discord.js';

export default {
  data: new SlashCommandBuilder()
    .setName('hello')
    .setDescription('Say hello')
    .addStringOption(option =>
      option
        .setName('name')
        .setDescription('Your name')
        .setRequired(false)
    ),

  async execute(interaction, core) {
    const name = interaction.options.getString('name') || 'World';
    await interaction.reply(`Hello, ${name}!`);
    
    // Access core dependencies
    core.logger.info(`${interaction.user.tag} used /hello`);
  }
};

Register the command:

// In index.js
import helloCommand from './commands/hello.js';

export default {
  // ...
  registerCommands() {
    return [helloCommand];
  }
};

Buttons

Create button handlers in src/modules/my-feature/buttons/:

Example: buttons/confirm.js

import { ButtonBuilder, ButtonStyle, ActionRowBuilder } from 'discord.js';

export function createButton() {
  return new ButtonBuilder()
    .setCustomId('my_feature_confirm')
    .setLabel('Confirm')
    .setStyle(ButtonStyle.Success)
    .setEmoji('✅');
}

export async function handleConfirmButton(interaction, core) {
  await interaction.reply({
    content: 'Confirmed!',
    flags: MessageFlags.Ephemeral
  });
  
  core.logger.info(`${interaction.user.tag} clicked confirm`);
}

Register the button:

// In index.js
import { handleConfirmButton } from './buttons/confirm.js';

export default {
  // ...
  registerButtons() {
    return new Map([
      ['my_feature_confirm', handleConfirmButton]
    ]);
  }
};

Modals

Create modal handlers in src/modules/my-feature/modals/:

Example: modals/feedback.js

import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from 'discord.js';

export function createFeedbackModal() {
  return new ModalBuilder()
    .setCustomId('my_feature_feedback')
    .setTitle('Feedback Form')
    .addComponents(
      new ActionRowBuilder().addComponents(
        new TextInputBuilder()
          .setCustomId('feedback_text')
          .setLabel('Your Feedback')
          .setStyle(TextInputStyle.Paragraph)
          .setRequired(true)
      )
    );
}

export async function handleFeedbackModal(interaction, core) {
  const feedback = interaction.fields.getTextInputValue('feedback_text');
  
  // Save to database
  await core.db.feedback.create({
    data: {
      userId: interaction.user.id,
      guildId: interaction.guild.id,
      content: feedback
    }
  });
  
  await interaction.reply({
    content: 'Thank you for your feedback!',
    flags: MessageFlags.Ephemeral
  });
}

Register the modal:

// In index.js
import { handleFeedbackModal } from './modals/feedback.js';

export default {
  // ...
  registerModals() {
    return new Map([
      ['my_feature_feedback', handleFeedbackModal]
    ]);
  }
};

Events

Listen to Discord events:

Example: Event handler

// In index.js
export default {
  // ...
  registerEvents() {
    return new Map([
      ['guildMemberAdd', async (member, core) => {
        core.logger.info(`New member joined: ${member.user.tag}`);
        
        // Send welcome message
        const channel = member.guild.systemChannel;
        if (channel) {
          await channel.send(`Welcome ${member}!`);
        }
      }],
      
      ['messageCreate', async (message, core) => {
        if (message.author.bot) return;
        
        // Custom message handling
        core.logger.debug(`Message from ${message.author.tag}: ${message.content}`);
      }]
    ]);
  }
};

Services

Organize business logic in src/modules/my-feature/services/:

Example: services/dataService.js

export class DataService {
  constructor(core) {
    this.db = core.db;
    this.logger = core.logger;
  }
  
  async saveData(guildId, data) {
    try {
      await this.db.myData.create({
        data: {
          guildId,
          content: JSON.stringify(data),
          createdAt: new Date()
        }
      });
      
      this.logger.info(`Data saved for guild ${guildId}`);
      return { success: true };
      
    } catch (error) {
      this.logger.error('Failed to save data:', error);
      throw error;
    }
  }
  
  async getData(guildId) {
    return await this.db.myData.findMany({
      where: { guildId }
    });
  }
}

Use in module:

import { DataService } from './services/dataService.js';

export default {
  async init(core) {
    this.core = core;
    this.dataService = new DataService(core);
  }
};

Best Practices

1. Module Independence

Don't:

// Don't import from other modules
import { gameService } from '../game-management/services/gameService.js';

Do:

// Communicate through database or events
await core.db.game.findMany({ ... });

2. Error Handling

Don't:

async execute(interaction) {
  const data = await riskyOperation();  // Could throw
  await interaction.reply(data);
}

Do:

async execute(interaction, core) {
  try {
    const data = await riskyOperation();
    await interaction.reply(data);
  } catch (error) {
    core.logger.error('Operation failed:', error);
    
    await interaction.reply({
      content: '❌ An error occurred. Please try again.',
      flags: MessageFlags.Ephemeral
    });
  }
}

3. Permission Checks

Always validate permissions:

import { PermissionFlagsBits } from 'discord.js';

async execute(interaction, core) {
  // Check user permissions
  if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
    return interaction.reply({
      content: '❌ You need Administrator permission to use this command.',
      flags: MessageFlags.Ephemeral
    });
  }
  
  // Check bot permissions
  const botPermissions = interaction.guild.members.me.permissions;
  if (!botPermissions.has(PermissionFlagsBits.ManageRoles)) {
    return interaction.reply({
      content: '❌ I need Manage Roles permission to do this.',
      flags: MessageFlags.Ephemeral
    });
  }
  
  // Proceed with command
}

4. Database Schema

Define your module's database tables in prisma/schema.prisma:

model MyModuleData {
  id        String   @id @default(cuid())
  guildId   String
  userId    String?
  data      String   // JSON data
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  
  @@index([guildId])
}

Run migrations:

npm run db:migrate

5. Logging

Use appropriate log levels:

core.logger.debug('Detailed debug information');
core.logger.info('General information');
core.logger.warn('Warning - something might be wrong');
core.logger.error('Error occurred', error);

Example: Welcome Module

A complete example module that welcomes new members:

Directory structure:

src/modules/welcome/
├── index.js
├── commands/
│   └── setwelcome.js
└── services/
    └── welcomeService.js

index.js:

import setwelcomeCommand from './commands/setwelcome.js';
import { WelcomeService } from './services/welcomeService.js';

export default {
  name: 'welcome',
  version: '1.0.0',
  
  async init(core) {
    this.core = core;
    this.welcomeService = new WelcomeService(core);
    core.logger.info('Welcome module initialized');
  },

  registerCommands() {
    return [setwelcomeCommand];
  },

  registerEvents() {
    return new Map([
      ['guildMemberAdd', (member, core) => {
        this.welcomeService.sendWelcome(member);
      }]
    ]);
  }
};

commands/setwelcome.js:

import { SlashCommandBuilder, ChannelType, PermissionFlagsBits } from 'discord.js';

export default {
  data: new SlashCommandBuilder()
    .setName('setwelcome')
    .setDescription('Set the welcome channel')
    .addChannelOption(option =>
      option
        .setName('channel')
        .setDescription('Channel for welcome messages')
        .setRequired(true)
        .addChannelTypes(ChannelType.GuildText)
    )
    .setDefaultMemberPermissions(PermissionFlagsBits.Administrator),

  async execute(interaction, core) {
    const channel = interaction.options.getChannel('channel');
    
    await core.db.guild.upsert({
      where: { id: interaction.guild.id },
      update: { welcomeChannel: channel.id },
      create: { id: interaction.guild.id, welcomeChannel: channel.id }
    });
    
    await interaction.reply({
      content: `✅ Welcome channel set to ${channel}`,
      flags: MessageFlags.Ephemeral
    });
  }
};

services/welcomeService.js:

export class WelcomeService {
  constructor(core) {
    this.core = core;
  }
  
  async sendWelcome(member) {
    try {
      const guild = await this.core.db.guild.findUnique({
        where: { id: member.guild.id }
      });
      
      if (!guild?.welcomeChannel) return;
      
      const channel = await member.guild.channels.fetch(guild.welcomeChannel);
      if (!channel) return;
      
      await channel.send({
        content: `Welcome ${member} to ${member.guild.name}! 🎉`,
        embeds: [{
          color: 0x00FF00,
          description: `We're glad to have you here!`,
          timestamp: new Date()
        }]
      });
      
      this.core.logger.info(`Sent welcome message for ${member.user.tag}`);
      
    } catch (error) {
      this.core.logger.error('Failed to send welcome message:', error);
    }
  }
}

Testing Your Module

Manual Testing

  1. Start the bot: npm run dev
  2. In Discord, test your commands: /your_command
  3. Check console logs for errors
  4. Verify database changes with: npm run db:studio

Unit Tests

Create tests in tests/modules/your-module/:

import { describe, it, before } from 'node:test';
import assert from 'node:assert';
import { WelcomeService } from '../../../src/modules/welcome/services/welcomeService.js';

describe('WelcomeService', () => {
  let service;
  let mockCore;
  
  before(() => {
    mockCore = {
      db: { /* mock database */ },
      logger: {
        info: () => {},
        error: () => {}
      }
    };
    
    service = new WelcomeService(mockCore);
  });
  
  it('should send welcome message', async () => {
    const mockMember = {
      guild: { id: '123', name: 'Test Server' },
      user: { tag: 'TestUser#1234' }
    };
    
    // Test your service method
    await service.sendWelcome(mockMember);
    
    assert.ok(true, 'Welcome message sent');
  });
});

Run tests:

npm test

Publishing Your Module

1. Documentation

Create README.md in your module folder:

# Welcome Module

Sends welcome messages to new members.

## Features
- Configurable welcome channel
- Custom welcome messages
- Event-driven architecture

## Commands
- `/setwelcome` - Set welcome channel (Admin only)

## Setup
1. Run `/setwelcome #channel`
2. New members will receive welcome messages

2. Version Your Module

Update version in index.js:

export default {
  name: 'welcome',
  version: '1.1.0',  // Semantic versioning
  // ...
};

3. Share Your Module

  • Push to GitHub as a separate repository
  • Include installation instructions
  • Document dependencies
  • Provide example configurations

Core API Reference

The core object passed to your module contains:

{
  client: Discord.Client,        // Discord.js client
  db: PrismaClient,              // Database client
  config: ConfigManager,         // Configuration
  logger: Logger,                // Logger instance
  permissions: PermissionsUtil,  // Permission utilities
  errors: ErrorClasses           // Custom error classes
}

Available Methods

Logger:

core.logger.debug(message, ...args)
core.logger.info(message, ...args)
core.logger.warn(message, ...args)
core.logger.error(message, error, ...args)

Config:

core.config.get(key)           // Get config value
core.config.set(key, value)    // Set config value

Database:

core.db.modelName.create({ data })
core.db.modelName.findUnique({ where })
core.db.modelName.findMany({ where })
core.db.modelName.update({ where, data })
core.db.modelName.delete({ where })

Advanced Patterns

Scheduled Tasks

Use node-cron for scheduled tasks:

import cron from 'node-cron';

export default {
  async init(core) {
    // Run every day at midnight
    cron.schedule('0 0 * * *', async () => {
      core.logger.info('Running daily cleanup...');
      await this.cleanup(core);
    });
  },
  
  async cleanup(core) {
    // Your cleanup logic
  }
};

Module Configuration

Store module-specific config:

export default {
  async init(core) {
    this.config = {
      enabled: true,
      maxItems: 10,
      timeout: 5000
    };
  }
};

Inter-Module Communication

Use events or shared database tables:

// Module A emits event
core.client.emit('customEvent', { data: 'value' });

// Module B listens
registerEvents() {
  return new Map([
    ['customEvent', async (data, core) => {
      core.logger.info('Received event:', data);
    }]
  ]);
}

Troubleshooting

Module not loading:

  • Check module folder is in src/modules/
  • Verify index.js exports default object
  • Check console for error messages

Commands not appearing:

  • Ensure registerCommands() returns array
  • Verify SlashCommandBuilder syntax
  • Wait up to 1 hour for global commands

Buttons not working:

  • Check customId matches registration
  • Verify button handler is async function
  • Ensure Map is returned from registerButtons()

Next Steps

  1. Study the game-management module as a reference
  2. Plan your module's features and database schema
  3. Start with a simple command and expand
  4. Test thoroughly before deployment
  5. Share your module with the community!

Happy coding! 🚀