UCS's specialized Discord bot for managing Capture The Flag (CTF) competitions, now with blockchain-backed task tracking, reputation, and document anchoring powered by Luce.
- 📅 Event Scheduling — Schedule CTF events with timezone support
- 🏗️ Channel Management — Automatically create dedicated CTF channels
- 👥 User Registration — Track team member participation
- 🗄️ Database Storage — Persistent storage of CTF data and registrations
- 🔗 CTFd Integration — Ready for CTFd platform integration (coming soon)
- ⛓️ Blockchain-Backed Tasks — Create and track tasks with immutable audit trail
- 👍 Reputation System — Give +1/-1 rep via reactions, replies, or
/repcommand - 📄 Document Anchoring — Permanently store documents on the blockchain
- ⏰ Automatic Reminders — Task deadline reminders sent to a dedicated channel
/schedule— Schedule custom events with timezone support/createctf— Create a CTF text channel and schedule its event/registerctf— Register your participation for the CTF in the current channel/addchalctf— Add challenges to a CTF/solvectf— Mark a challenge as solved/archivectf— Archive a completed CTF/chalpts— View challenge points/summarizectf— View CTF summary/syncchallenges— Sync challenges from CTFd/pact— Manage pacts
/task add— Create a new task (title, description, assignee, deadline)/task list [period]— View remaining tasks for this week/month/quarter/year/task done— Mark a task as completed
/rep [downvote]— Give +1 or -1 rep (must reply to someone's message; response is ephemeral)/repleaderboard [limit]— View the reputation leaderboard
You can also give rep by:
- Reacting with 👍 or 👎 to someone's message
- Replying to someone with
+1,-1, 👍, or 👎
/document add— Anchor a document to the blockchain/document get— Retrieve an anchored document
/ping— Check bot responsiveness/help— List all available commands
- Node.js v18.0.0 or higher
- pnpm package manager
- A Discord Bot Token (Create one here)
- Luce blockchain server running locally on port 5500
-
Clone the repository
git clone https://github.com/ucsits/ctfbot.git cd ctfbot -
Install dependencies
pnpm install
-
Configure environment variables
cp .env.example .env
Edit
.envand fill in the required values:DISCORD_TOKEN— Your Discord bot tokenGUILD_ID— Your Discord server ID (for development)CTF_CATEGORY_ID— Category ID where CTF channels will be createdCTFD_API_TOKEN— Your CTFd API token (optional)LUCE_PORT— Luce blockchain RPC port (default: 5500)
-
Run the bot
Development mode (with auto-reload):
pnpm dev
Production mode:
pnpm start
-
Create a CTF Category
- Right-click in your server's channel list
- Select "Create Category"
- Name it "CTF" or similar
- Right-click the category → Copy ID
- Paste the ID in
.envasCTF_CATEGORY_ID
-
Invite the Bot
- Go to Discord Developer Portal
- Select your application
- Go to OAuth2 → URL Generator
- Select scopes:
bot,applications.commands - Select permissions:
Manage Channels,Manage Events,Send Messages,Embed Links,Add Reactions,Read Message History - Copy and visit the generated URL
src/
├── commands/ # Slash command implementations
│ ├── createctf.js # Create CTF channels and events
│ ├── registerctf.js # User registration for CTFs
│ ├── schedule.js # Generic event scheduling
│ ├── task.js # Blockchain-backed task management
│ ├── rep.js # Reputation command (ephemeral)
│ ├── repleaderboard.js # Reputation leaderboard
│ ├── document.js # Document anchoring
│ ├── help.js # List all commands
│ └── ping.js # Health check
├── listeners/ # Event listeners
│ ├── ready.js # Bot ready event (starts reminder service)
│ ├── messageCreate.js # Detects reply-based +1/-1/👍/👎
│ ├── messageReactionAdd.js # Detects 👍/👎 reaction rep
│ └── applicationCommandRegistriesRegistered.js
├── services/
│ └── reminder.js # Background reminder poller (every 30s)
├── lib/
│ ├── luce/
│ │ └── index.js # Luce blockchain RPC client
│ ├── constants/
│ ├── embeds/
│ ├── errors/
│ ├── helpers/
│ ├── middleware/
│ ├── utils/
│ └── validators/
├── database/
│ ├── repositories/
│ │ ├── task.repository.js # Tasks + reminders queries
│ │ ├── reputation.repository.js # Reputation ledger queries
│ │ └── document.repository.js # Document queries
│ ├── connection.js
│ ├── index.js
│ └── migrations.js
├── migrations/ # SQL migration files
└── index.js # Main entry point
Core tables (existing):
ctfs— Stores CTF competition detailsctf_registrations— Stores user registrationsctf_challenges— Stores CTF challengesctf_challenge_solves— Tracks challenge solvespacts— Pact agreementsadmins— Admin user IDs
Blockchain-backed tables (new):
tasks— Tasks with reference to blockchain block heighttask_reminders— Scheduled reminders for task deadlinesreputations— Reputation ledger (one entry per giver per day)documents— Anchored documents with blockchain reference
Every block stored on the Luce blockchain has a data field containing a JSON
string. Below are the supported schemas with their type discriminator.
Created when a user runs /task add.
{
"type": "task",
"v": 1,
"taskId": "a1b2c3d4-...",
"title": "Implement login page",
"description": "Build the user authentication UI",
"assignedTo": "123456789012345678",
"createdBy": "987654321098765432",
"deadline": 1740000000
}| Field | Type | Description |
|---|---|---|
type |
string |
Always "task" |
v |
number |
Schema version (currently 1) |
taskId |
string |
UUID v4 |
title |
string |
Task title |
description |
string |
Task description (may be empty) |
assignedTo |
string |
Discord user ID of assignee |
createdBy |
string |
Discord user ID of creator |
deadline |
number |
Unix timestamp (seconds) |
Created when a user runs /task done.
{
"type": "task_done",
"v": 1,
"taskId": "a1b2c3d4-...",
"completedBy": "123456789012345678"
}| Field | Type | Description |
|---|---|---|
type |
string |
Always "task_done" |
v |
number |
Schema version (currently 1) |
taskId |
string |
UUID of the completed task |
completedBy |
string |
Discord user ID of completer |
Created via reaction (👍/👎), reply (+1/-1/👍/👎), or /rep command.
{
"type": "rep",
"v": 1,
"toUser": "123456789012345678",
"fromUser": "987654321098765432",
"amount": 1,
"reason": "reaction",
"date": "2026-07-10"
}| Field | Type | Description |
|---|---|---|
type |
string |
Always "rep" |
v |
number |
Schema version (currently 1) |
toUser |
string |
Discord user ID of recipient |
fromUser |
string |
Discord user ID of giver |
amount |
number |
1 (upvote) or -1 (downvote) |
reason |
string |
Trigger source: "reaction", "reply", or "" (slash command) |
date |
string |
UTC date in YYYY-MM-DD format (enforces daily limit) |
Daily limit: A user may only give rep once per UTC calendar day, regardless of target or sign. This is enforced at both the application and database level.
Created when a user runs /document add.
{
"type": "document",
"v": 1,
"docId": "a1b2c3d4-...",
"title": "Meeting Notes 2026-07-10",
"content": "Discussed sprint goals for Q3...",
"author": "123456789012345678",
"mimeType": "text/plain"
}| Field | Type | Description |
|---|---|---|
type |
string |
Always "document" |
v |
number |
Schema version (currently 1) |
docId |
string |
UUID v4 |
title |
string |
Document title |
content |
string |
Document content (plain text; base64 for binary in future versions) |
author |
string |
Discord user ID of author |
mimeType |
string |
MIME type (default: "text/plain") |
The v field allows future schema evolution without breaking existing blocks.
When introducing a breaking change, increment v and handle both versions in
your parser.
A background service (src/services/reminder.js) polls every 30 seconds for
unsent task reminders. When a reminder is due (set to 1 hour before the task
deadline), it sends a message to the channel ID 1524933314119467200.
Flow:
/task add→ creates blockchain block + DB row + reminder schedule- Reminder service polls
task_reminderstable every 30s - Due reminders are sent to channel
1524933314119467200 - Reminder is marked
sent = 1to prevent duplicates
The bot is built using the @sapphire/framework which provides:
- Structured command handling
- Event listener system
- Plugin support
- TypeScript-ready architecture
pnpm testThis project uses tabs for indentation. Please ensure your editor is configured accordingly.
The bot includes a unified logging system built on top of the Sapphire framework's logger.
Set the LOG_LEVEL environment variable to control verbosity:
| Level | Usage |
|---|---|
error |
Fatal errors and exceptions only |
warn |
Warnings + errors |
info |
Normal operational messages (default) |
debug |
Detailed debugging information |
trace |
Everything, including individual HTTP request logs |
LOG_LEVEL=infoAll logs are output to stdout (info/debug/trace) and stderr (error/warn). There is no file-based logging — use your process manager (e.g., systemd, Docker, PM2) to capture and rotate stdout/stderr.
The logger (src/lib/logger.js) works in two modes:
- Pre-init mode: Before the bot has logged into Discord, the logger
falls back to raw
consolecalls with ISO timestamps. - Post-init mode: Once the SapphireClient is ready, the logger delegates to Sapphire's structured logger for consistent formatting across all commands and listeners.
Child loggers with a context prefix are used throughout the codebase:
| Prefix | Module |
|---|---|
[ErrorHandler] |
src/lib/errorHandler.js |
[CTFd] |
src/lib/ctfd/index.js |
[DB] |
src/database/index.js |
[Migration] |
src/database/migrations.js |
[Luce] |
src/lib/luce/index.js |
const { logger } = require('./lib/logger');
const myLog = logger.child('MyModule');
myLog.info('Operation completed successfully');
myLog.error('Something went wrong', error);
myLog.debug('Detailed info: %s', detail);Logger methods follow the same signature as console.log:
logger.info(message, ...optionalArgs).
Contributions are welcome! Please read our comprehensive guides:
- 📖 Contributing Guide — Detailed contribution guidelines
- 🏗️ Project Structure — Understanding the codebase
- 🚀 Quick Reference — Fast reference for common tasks
- 🏛️ Architecture — System design and patterns
- 📊 Changelog — Version history
- Fork the repository
- Clone your fork:
git clone https://github.com/YOUR_USERNAME/ctfbot.git - Install dependencies:
pnpm install - Create a branch:
git checkout -b feature/your-feature - Make your changes
- Run linting:
pnpm run lint:fix - Commit:
git commit -m "feat: add your feature" - Push:
git push origin feature/your-feature - Open a Pull Request
See CONTRIBUTING.md for detailed guidelines.
- Blockchain-backed task management
- Reputation system (+1/-1 with daily limit)
- Document anchoring
- Additional rep triggers (message content, reactions on other platforms)
- Web dashboard for blockchain explorer
- Multi-chain support
This project is licensed under the MIT License — see the LICENSE file for details.
- Issues: GitHub Issues
- Discord: Join our server for support and discussions
- Built with discord.js
- Powered by @sapphire/framework
- Database: better-sqlite3
- Blockchain: Luce
Made with ❤️ by UCS ITS