@quadslab.io/discord-bot
v1.0.0
Published
A modular Discord bot framework with dynamic module loading and runtime migrations
Downloads
99
Readme
QuadsLabBot
A modular Discord bot framework built with TypeScript and Discord.js. Features a plugin architecture with dynamic module loading, runtime database migrations, AI integration, and an inter-module event bus. Build new features as self-contained modules without touching the core.
Features
- Drop-In Module System — Auto-discovered modules with their own commands, events, services, database migrations, settings, and leaderboards
- Slash Command Framework — Declarative command builder with permission guards, subcommands, autocomplete, and context menus
- Inter-Module Event Bus — Loosely coupled communication between modules (e.g., voice tracking emits session events that the points module listens to)
- Per-Module Database Migrations — Each module manages its own SQL migrations that run automatically on load
- Centralized Settings — Modules register configurable settings that server admins manage through
/settings - Leaderboard Registry — Any module can register leaderboard providers, all accessible through a unified
/leaderboardcommand - Cron Scheduler — Built-in scheduled task runner with per-module job registration
- AI Provider Abstraction — Pluggable AI backends (Claude, OpenAI) for modules that need AI features
- Docker Ready — Multi-stage Docker build with MySQL service, health checks, and compose orchestration
Quick Start
Prerequisites
- Node.js 18+
- MySQL 8.0+
- A Discord bot token (Discord Developer Portal)
Installation
# Clone the repository
git clone https://github.com/HardHeadHackerHead/discord-bot.git
cd QuadsLabBot
# Install dependencies
npm install
# Set up environment
cp .env.example .env
# Edit .env with your bot token and database credentials
# Generate Prisma client
npm run db:generate
# Push database schema
npm run db:push
# Start in development mode
npm run devDocker
# Start with Docker Compose (bot + MySQL)
docker compose up -d
# View logs
docker compose logs -f botConfiguration
Copy .env.example to .env and fill in the required values:
| Variable | Required | Description |
|---|---|---|
| BOT_TOKEN | Yes | Discord bot token |
| CLIENT_ID | Yes | Discord application client ID |
| DATABASE_URL | Yes | MySQL connection string (mysql://user:pass@host:3306/dbname) |
| DEV_GUILD_ID | No | Guild ID for instant slash command updates during development |
| NODE_ENV | No | development or production (default: development) |
| LOG_LEVEL | No | debug, info, warn, or error (default: debug) |
See .env.example for the full list of configuration options including website integration and ngrok tunnel settings.
Included Modules
These modules ship with the framework and can be enabled/disabled per server:
| Module | Description | Commands |
|---|---|---|
| admin | Module management, settings, bot stats | /modules, /reload, /settings, /lines, /clear |
| help | Lists all available commands | /commands |
| user-tracking | Tracks users joining/leaving guilds | /userinfo |
| message-tracking | Tracks message counts with spam cooldown | /messages |
| voice-tracking | Tracks voice channel time with session management | /voicetime |
| points | Currency system with balance and transaction history | /points |
| leaderboard | Unified leaderboard display for all module stats | /leaderboard |
| polls | Create and manage polls with voting | /poll |
| role-management | Self-assignable roles via dropdown menus | /roles |
| message-editor | Edit bot messages via emoji reactions | (reaction-based) |
How Modules Work Together
Modules communicate through events, not direct imports. This means any module can be removed without breaking others:
message-tracking ──emits──> "message-counted" ──listened by──> points
voice-tracking ──emits──> "session-ended" ──listened by──> points
points ──registers──> leaderboard provider ──displayed by──> leaderboardIf points isn't loaded, message-tracking and voice-tracking still work — they just track stats without awarding points.
Creating Modules
Module Structure
Every module lives in its own directory under src/modules/:
src/modules/my-module/
├── index.ts # Exports module INSTANCE (not class)
├── module.ts # Module class with metadata and lifecycle
├── LICENSE # Module-specific license (optional)
├── migrations/ # SQL migrations (auto-run on load)
│ └── 001_initial.sql
├── commands/ # Slash commands
│ └── my-command.ts
├── events/ # Discord event handlers
│ └── interactionCreate.ts
├── services/ # Business logic and database operations
│ └── MyService.ts
└── components/ # UI builders (embeds, buttons, modals)
└── MyPanel.tsMinimal Module Example
module.ts — Define metadata and lifecycle:
import { BaseModule, ModuleMetadata, ModuleContext } from '../../types/module.types.js';
import { command as pingCommand } from './commands/ping.js';
import { Logger } from '../../shared/utils/logger.js';
const logger = new Logger('Ping');
export class PingModule extends BaseModule {
readonly metadata: ModuleMetadata = {
id: 'ping',
name: 'Ping',
description: 'Simple ping/pong command',
version: '1.0.0',
author: 'Your Name',
isCore: false,
isPublic: true,
dependencies: [],
priority: 50,
};
// Set to null if no database tables needed
readonly migrationsPath = null;
constructor() {
super();
this.commands = [pingCommand];
}
async onLoad(context: ModuleContext): Promise<void> {
await super.onLoad(context);
logger.info('Ping module loaded');
}
}index.ts — Export an instance (not the class):
import { PingModule } from './module.js';
export default new PingModule();commands/ping.ts — Define a slash command:
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../../types/command.types.js';
export const command: SlashCommand = {
type: 'slash',
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Check bot latency') as SlashCommandBuilder,
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
const latency = Date.now() - interaction.createdTimestamp;
await interaction.reply(`Pong! ${latency}ms`);
},
};Drop this folder into src/modules/ping/, restart the bot, and the /ping command is live. No registration code, no config files — the framework discovers it automatically.
Full Development Guide
See docs/MODULE_DEVELOPMENT.md for the complete guide covering:
- Database migrations and the
DatabaseServiceAPI - Commands with subcommands, autocomplete, and context menus
- Event handlers and custom ID conventions
- Services with dependency injection pattern
- UI components (embeds, buttons, select menus, modals)
- Inter-module event bus (emitting and subscribing)
- Centralized settings system (per-guild configuration)
- Leaderboard registry (registering stat providers)
- Cron jobs (scheduled tasks)
- Panel state management for multi-step UI flows
Building Modules with Claude Code
This project is designed to work well with Claude Code. The module architecture follows strict, consistent patterns that Claude Code can replicate reliably. Here's how to use it effectively.
Creating a New Module
Give Claude Code context about what you want, and reference the existing patterns:
Read docs/MODULE_DEVELOPMENT.md and the points module in src/modules/points/
as a reference. Then create a new module called "warnings" that:
- Tracks user warnings with reasons
- /warn command (admin only) to warn a user
- /warnings command to view a user's warning history
- Stores warnings in a database table
- Emits a "warning-issued" event so other modules can reactClaude Code will follow the established patterns because:
- The module structure is enforced (every module has the same file layout)
- The types are well-documented (
ModuleMetadata,SlashCommand,BaseModule) - Real examples exist in the codebase to reference
Tips for Effective Prompts
Be specific about the module's behavior:
Create a "birthday" module that lets users set their birthday with /birthday set,
shows upcoming birthdays with /birthday list, and announces birthdays daily
using the cron system. Store dates in a birthday_dates table.Reference existing modules for complex patterns:
Look at how the points module subscribes to events from voice-tracking
and message-tracking. Create a "levels" module that listens to the same
events but calculates XP and levels instead of points. Register a
leaderboard showing level rankings.Ask for specific integrations:
Create a "welcome" module that sends a welcome embed when users join.
Register a setting for the welcome channel ID and welcome message text
using the centralized settings system so admins can configure it with /settings.What Claude Code Should Know
When creating modules, these are the key patterns:
| Pattern | How It Works |
|---|---|
| Service injection | Services are created in onLoad(), then passed to commands/events via setService() functions |
| Custom IDs | All interaction custom IDs use <module>:<action>:<data> format (e.g., polls:vote:abc123) |
| Table naming | Database tables are prefixed with the module ID (e.g., points_user_points) |
| Event naming | Module events use <module-id>:<event-name> format (e.g., voice-tracking:session-ended) |
| Exports | index.ts must export an instance: export default new MyModule() |
| Migrations | Numbered SQL files in migrations/ (e.g., 001_initial.sql) — run automatically |
| Cleanup | Always unsubscribe events, unregister leaderboards/settings/cron in onUnload() |
Example Prompts for Common Module Types
Tracking module (like message-tracking):
Create a "reaction-tracking" module that counts reactions users give and receive.
Track both given and received counts. Emit events when reactions are counted.
Register two leaderboards: "Most Reactions Given" and "Most Reactions Received".Admin module (like role-management):
Create an "auto-role" module with an admin command /autorole that configures
roles automatically assigned when users join. Support multiple roles per guild.
Use the settings system for an optional "delay" before role assignment.Game module (like polls):
Create a "trivia" module with /trivia that posts a question with 4 button
options. Use a panel component for the question embed and answer buttons.
Track correct answers per user and register a leaderboard.Integration module (with events):
Create a "milestones" module that listens to the points:awarded event.
When a user crosses a milestone threshold (100, 500, 1000, 5000 points),
send a congratulations embed to a configurable channel. Use the settings
system for the announcement channel.Project Structure
QuadsLabBot/
├── src/
│ ├── index.ts # Entry point
│ ├── bot.ts # Bot initialization and orchestration
│ ├── config/
│ │ └── environment.ts # Env validation with zod
│ ├── core/ # Core framework (don't modify for modules)
│ │ ├── ai/ # AI provider abstraction (Claude, OpenAI)
│ │ ├── client/ # Discord.js client wrapper
│ │ ├── commands/ # Command registration and routing
│ │ ├── cron/ # Scheduled task runner
│ │ ├── database/ # Prisma client + migration runner
│ │ ├── events/ # Event handler system
│ │ ├── leaderboards/ # Leaderboard registry
│ │ ├── modules/ # Module loader, registry, event bus
│ │ └── settings/ # Per-module settings system
│ ├── modules/ # Plugin modules (add yours here)
│ ├── shared/
│ │ └── utils/ # Logger, embed helpers, pagination, permissions
│ └── types/ # TypeScript types for modules, commands, events
├── prisma/ # Prisma schema and core migrations
├── docs/ # Documentation
├── Dockerfile # Multi-stage Docker build
└── docker-compose.yml # Docker Compose (bot + MySQL)Scripts
| Command | Description |
|---|---|
| npm run dev | Start in development mode with hot reload |
| npm run build | Compile TypeScript to JavaScript |
| npm start | Run the compiled bot |
| npm run db:generate | Generate Prisma client |
| npm run db:migrate | Run Prisma migrations |
| npm run db:push | Push schema changes to database |
| npm run db:studio | Open Prisma Studio (database GUI) |
| npm run lint | Run ESLint |
| npm run lint:fix | Run ESLint with auto-fix |
Contributing
See CONTRIBUTING.md for guidelines on how to contribute.
License
The core framework is licensed under the MIT License.
Individual modules in src/modules/ may have their own licenses. Check each module's LICENSE file for details. Modules without a license file fall under the project's MIT license.
