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
- Module Interface Contract
- Creating Your First Module
- Module Components
- Best Practices
- Example: Welcome Module
- Testing Your Module
- Publishing Your Module
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
- Discovery: The Module Loader scans
src/modules/for folders containing anindex.js - Initialization: Each module's
init()function receives core dependencies - Registration: Module registers its commands, buttons, modals, and events
- 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
- Start the bot:
npm run dev - In Discord, test your commands:
/your_command - Check console logs for errors
- 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.jsexports 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
- Study the
game-managementmodule as a reference - Plan your module's features and database schema
- Start with a simple command and expand
- Test thoroughly before deployment
- Share your module with the community!
Happy coding! 🚀