Skip to content

Latest commit

Β 

History

136 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

CTFBot 🚩

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.

License: MIT Node.js Version Discord.js

Features

  • πŸ“… 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 /rep command
  • πŸ“„ Document Anchoring β€” Permanently store documents on the blockchain
  • 🎯 Activity Points Economy β€” Admin-granted points, a leaderboard, and a merchandise store (AP or Rp)
  • ⏰ Automatic Reminders β€” Task deadline reminders sent to a dedicated channel

Commands

CTF Commands

  • /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 and solves from the configured platform
  • /setctfplatform: Select the CTF platform for this channel and set its API details
  • /pact: Manage pacts

Task Commands

  • /task add β€” Create a new task (title, description, assignee, deadline, optional timezone)
  • /task list [period] β€” View remaining tasks for this week/month/quarter/year
  • /task done <task_id | task> β€” Mark a task as completed (by UUID or fuzzy title search)
  • /task cancel <task_id | task> β€” Confirm and cancel a pending task (by UUID or fuzzy title search), removing its reminders

Task reports use Asia/Jakarta calendar boundaries. Large lists are split into multiple responses.

Completing/cancelling by title: Personnel no longer need to copy the UUID β€” pass the task title (or a distinctive part of it, even its description) as the task option and the bot fuzzy-searches all pending tasks. Because a fuzzy match can be wrong, every title-based completion/cancellation requires an ephemeral confirmation (Confirm / Deny buttons) before anything is recorded; nothing is written to the blockchain or the database until you confirm. If several tasks match closely, a picker shows one button per candidate β€” pick the one you meant. UUID-based completion and cancellation also ask for confirmation for consistency. Cancellation additionally removes the task's reminders.

Reputation Commands

  • /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 Commands

  • /document add β€” Anchor a document to the blockchain
  • /document get β€” Retrieve an anchored document

Activity Points Commands

  • /activitypoints [user] β€” View your (or another user's) activity points balance
  • /activityleaderboard [limit] β€” View the activity points leaderboard
  • /giveap <user|role> <points> β€” Grant activity points to a user or every member of a role (admin only)
  • /giveapbulk <csv> β€” Grant activity points to many users from a CSV with discord_id and points columns (admin only)
  • /store β€” Browse and buy the merchandise store with AP or Rp (no subcommand)
  • /store-confirm <purchase_id> β€” Confirm a pending Rp purchase once offline payment arrives (admin only)

Utility Commands

  • /ping β€” Check bot responsiveness
  • /help β€” List all available commands

CTF Platform Support

Every CTF channel is bound to exactly one platform. The binding is stored on the ctfs row so commands never have to guess:

Column Purpose
platform Adapter id, either ctfd or noctf. Defaults to ctfd for every pre-existing row.
api_base_url API origin to talk to. Needed because noCTF serves its API from a different origin than its web UI. Falls back to the CTF URL when unset.
platform_division_id Scoreboard division, required by noCTF and unused by CTFd.

The token is still stored in ctfs.api_token. CTFd sends it as Authorization: Token <api_token>, while noCTF sends it as Authorization: Bearer <token> and expects a copied session token. A noCTF session token lasts 7 days and has no refresh endpoint, so it has to be replaced periodically with /setctfplatform.

Choosing a platform

/setctfplatform platform:noctf api_base_url:https://api-k17ctf.secso.cc api_token:<token> division_id:2
/setctfplatform show:true

/setctfplatform needs the Manage Channels permission, and admins and governance users bypass that check like every other privileged command. The command probes the platform before it persists anything, and it reports whether the API answered, whether the CTF is currently active, and which division it used.

Behaviour differences

Concern CTFd noCTF
Challenge listing /api/v1/challenges /challenges, gated on the competition being active unless the token carries admin policies
Category category field Derived from tags.categories, taking the first comma separated entry, falling back to uncategorized
Solve attribution Per user via /api/v1/challenges/:id/solves Team scoped. /challenges/:id/solves returns a team id only, so the scoreboard endpoint is used to recover the user id
Scoreboard /api/v1/scoreboard /scoreboard/divisions/:id, which is why a division id is required
Bulk solve listing Not available /scoreboard/divisions/:id carries every solve in the division, not just the bot's teams

When noCTF reports a solve for a team, the sync attributes it to a registered member of that team. If no member is registered yet, the solve is parked under a per-platform synthetic user id (noctf:<id> or ctfd:<id>) and /registerctf claims it later. The prefix is per platform so a numeric id from one platform can never be claimed by a registration on the other.

That noCTF bulk listing is division-wide, and the token cannot narrow it: the platform serves the scoreboard and /users/query publicly and never filters by the caller's membership. Scope therefore comes from the registrations in the channel. A solve belonging to a registered Discord user is always recorded, and an unregistered solver is only parked when their team matches a team someone has already registered, so a sync never imports the rest of the division. One consequence is worth knowing: because scope is the registered teams, a solve is only parked once at least one teammate has run /registerctf. The first member of a team to register has nothing waiting to be claimed.

Adding another platform

  1. Add an entry to PLATFORMS in src/lib/constants/platforms.js. The id is persisted in ctfs.platform, so treat it as stable.
  2. Add a factory to FACTORIES in src/lib/platform/index.js that returns the normalized client interface documented at the top of that file.
  3. Add a prefix to PREFIX_BY_PLATFORM in src/lib/platform/syntheticUser.js.

Commands only ever call createPlatformClient, so they pick the new platform up without further changes.

Quick Start

Prerequisites

Installation

  1. Clone the repository

    git clone https://github.com/ucsits/ctfbot.git
    cd ctfbot
  2. Install dependencies

    pnpm install
  3. Configure environment variables

    cp .env.example .env

    Edit .env and fill in the required values:

    • DISCORD_TOKEN β€” Your Discord bot token
    • GUILD_ID β€” Your Discord server ID (for development)
    • CTF_CATEGORY_ID β€” Category ID where CTF channels will be created
    • CTFD_API_TOKEN β€” Your CTFd API token (optional)
    • LUCE_PORT β€” Luce blockchain RPC port (default: 5500)
  4. Run the bot

    Development mode (with auto-reload):

    pnpm dev

    Production mode:

    pnpm start

Setting Up Your Discord Server

  1. 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 .env as CTF_CATEGORY_ID
  2. 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

Architecture

src/
β”œβ”€β”€ commands/              # Slash command implementations
β”‚   β”œβ”€β”€ createctf.js       # Create CTF channels and events
β”‚   β”œβ”€β”€ registerctf.js     # User registration for CTFs
β”‚   β”œβ”€β”€ setctfplatform.js  # Pick the CTF platform for a channel
β”‚   β”œβ”€β”€ syncchallenges.js  # Sync challenges and solves from the platform
β”‚   β”œβ”€β”€ 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
β”‚   β”œβ”€β”€ platform/          # Platform registry and normalized client interface
β”‚   β”‚   β”œβ”€β”€ index.js       # createPlatformClient and platform lookup
β”‚   β”‚   β”œβ”€β”€ ctfd.adapter.js  # Wraps the existing CTFd client
β”‚   β”‚   └── syntheticUser.js  # Pending-solve user id prefixes per platform
β”‚   β”œβ”€β”€ noctf/
β”‚   β”‚   └── index.js       # noCTF client (separate API origin, Bearer auth)
β”‚   β”œβ”€β”€ 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

Database Schema

Core tables (existing):

  • ctfs β€” Stores CTF competition details
  • ctfs.platform, ctfs.api_base_url, ctfs.platform_division_id: Which platform a CTF channel syncs from, and how to reach it
  • ctf_registrations β€” Stores user registrations
  • ctf_challenges β€” Stores CTF challenges
  • ctf_challenge_solves β€” Tracks challenge solves
  • pacts β€” Pact agreements
  • admins β€” Admin user IDs

Blockchain-backed tables (new):

  • tasks β€” Tasks with reference to blockchain block height
  • task_reminders β€” Scheduled reminders for task deadlines
  • reputations β€” Reputation ledger (one entry per giver per day)
  • documents β€” Anchored documents with blockchain reference
  • activity_ledger β€” Append-only ledger of AP value movements (grants/spends)
  • activity_balances β€” Current AP balance per user
  • store_items β€” Merchandise catalog (Sticker, Keychain, Shirt, Jacket)
  • purchases β€” Purchase records (AP completed, Rp pendingβ†’confirmed)

Blockchain Block Data Schema

Every block stored on the Luce blockchain has a data field containing a JSON string. Below are the supported schemas with their type discriminator.

πŸ”Ή Task (type: "task")

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)

βœ… Task Completion (type: "task_done")

Created when a user runs /task done.

{
  "type": "task_done",
  "v": 1,
  "taskId": "a1b2c3d4-...",
  "title": "Implement login page",
  "assignedTo": "123456789012345678",
  "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
title string Task title, for recognizable audit events
assignedTo string Discord user ID of assignee

πŸ—‘οΈ Task Cancellation (type: "task_cancel")

Created when a user runs /task cancel confirm:True.

{
  "type": "task_cancel",
  "v": 1,
  "taskId": "a1b2c3d4-...",
  "title": "Implement login page",
  "assignedTo": "123456789012345678",
  "cancelledBy": "987654321098765432"
}
Field Type Description
type string Always "task_cancel"
v number Schema version (currently 1)
taskId string UUID of the cancelled task
title string Task title
assignedTo string Discord user ID of assignee
cancelledBy string Discord user ID of canceller

πŸ‘ Reputation (type: "rep")

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.

πŸ“„ Document (type: "document")

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")

🎯 Activity Points Grant (type: "ap_grant")

Created by /giveap (single user) and /giveapbulk / /giveap <role> (batch).

Single grant (v: 1):

{
  "type": "ap_grant",
  "v": 1,
  "grantedTo": "123456789012345678",
  "grantedBy": "987654321098765432",
  "amount": 50
}

Batch grant (v: 2) β€” one block for the whole batch, used by /giveapbulk and /giveap <role>:

{
  "type": "ap_grant",
  "v": 2,
  "grantedBy": "987654321098765432",
  "entries": [
    { "discord_id": "123456789012345678", "points": 50 },
    { "discord_id": "876543210987654321", "points": 50 }
  ],
  "count": 2,
  "total": 100
}
Field Type Description
type string Always "ap_grant"
v number 1 (single) or 2 (batch)
grantedTo string Discord user ID of recipient (v1 only)
grantedBy string Discord user ID of the granting admin
amount number Points granted (v1 only)
entries array {discord_id, points} list (v2 only)
count number Number of recipients (v2 only)
total number Total points granted (v2 only)

πŸ›’ Store Purchase (type: "ap_purchase")

Created when a user buys an item from the store. AP purchases complete immediately; Rp purchases start pending and are confirmed later.

{
  "type": "ap_purchase",
  "v": 1,
  "user": "123456789012345678",
  "itemName": "Shirt",
  "paymentMethod": "ap",
  "costAp": 200,
  "status": "completed"
}
Field Type Description
type string Always "ap_purchase"
v number Schema version (currently 1)
user string Discord user ID of buyer
itemName string Name of the purchased item
paymentMethod string "ap" or "rp"
costAp number AP cost (when paymentMethod is "ap")
costRp number Rp cost (when paymentMethod is "rp")
status string "completed" (AP) or "pending" (Rp)

βœ… Purchase Confirmation (type: "ap_confirm")

Created when an admin confirms a pending Rp purchase via /store-confirm.

{
  "type": "ap_confirm",
  "v": 1,
  "user": "123456789012345678",
  "confirmedBy": "987654321098765432",
  "itemName": "Shirt",
  "paymentMethod": "rp"
}
Field Type Description
type string Always "ap_confirm"
v number Schema version (currently 1)
user string Discord user ID of buyer
confirmedBy string Discord user ID of confirming admin
itemName string Name of the purchased item
paymentMethod string "rp" (or "ap" for completeness)

Schema Versioning

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.


Reminder System

A background service (src/services/reminder.js) polls every 30 seconds for unsent task reminders. Reminders are sent to 1524933314119467200 one hour before the deadline and at 09:00 Asia/Jakarta on the day before the deadline when those times are still in the future. Failed sends remain retryable.

Daily digests run after 04:00 Asia/Jakarta and weekly digests run Monday after 05:00 Asia/Jakarta; Monday sends avoid redundant notifications. Digest ranges and task reports use Asia/Jakarta calendar boundaries, and empty results are explicitly reported.

Flow:

  1. /task add β†’ creates blockchain block + DB row + reminder schedule
  2. Reminder service polls task_reminders table every 30s
  3. Due reminders are sent to channel 1524933314119467200
  4. Reminder is marked sent = 1 to prevent duplicates

Google Calendar Sync (Option A)

A background service (src/services/calendarSync.js) pushes bot tasks to a shared Google Calendar and reconciles bot-owned calendar events back into the task DB. This lets the team see task deadlines on their calendar and edit them there, with changes reflected back in Discord.

Push direction (bot β†’ Calendar):

  • /task add creates a new Calendar event (1 hour before the deadline).
  • /task done deletes the event from the calendar.
  • /task cancel patches the event status to cancelled.

Pull direction (Calendar β†’ bot):

  • Incremental sync using Google's syncToken to reduce API quota.
  • Only processes events with the private x-ctfbot-task extended property marker, so foreign events are never imported as tasks.
  • Detects: title changes, deadline changes, and event cancellation/deletion.
  • A reconciliation cycle runs every 60 seconds when enabled.

Setup

  1. Google Cloud Project β€” create one at https://console.cloud.google.com/, enable the Google Calendar API.

  2. OAuth 2.0 credentials β€” create an OAuth 2.0 Client ID (Desktop app type). Copy the client ID and client secret.

  3. Shared Calendar ID β€” create or choose a Google Calendar that the bot will own. Copy its ID (find it in Calendar settings β†’ Integrate calendar β†’ Calendar ID).

  4. Refresh token β€” visit the authorization URL below (generated by the bot or manually) with the calendar scope to obtain a one-time code, then exchange it for a refresh token:

    https://accounts.google.com/o/oauth2/v2/auth?client_id=YOUR_CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/calendar&access_type=offline&prompt=consent
    
  5. Configure .env:

    GOOGLE_CALENDAR_ENABLED=true
    GOOGLE_CLIENT_ID=your_client_id
    GOOGLE_CLIENT_SECRET=your_client_secret
    GOOGLE_REFRESH_TOKEN=your_refresh_token
    GOOGLE_CALENDAR_ID=your_calendar_id

Architecture

  • src/lib/google/oauth.js β€” OAuth 2.0 token refresh (fetch-based, no extra dependencies).
  • src/lib/google/calendar.js β€” Google Calendar API v3 REST wrappers and mapper functions (taskToCalendarEvent, calendarEventToTaskPatch).
  • src/services/calendarSync.js β€” poll loop (60s interval), push logic, incremental pull reconciliation.
  • migrations/019_add_calendar_sync.sql β€” adds calendar_event_id, calendar_synced_at, calendar_source columns to tasks, plus google_credentials and calendar_sync_state tables.

Development

Project Structure

The bot is built using the @sapphire/framework which provides:

  • Structured command handling
  • Event listener system
  • Plugin support
  • TypeScript-ready architecture

Running Tests

pnpm test

The suite is fully offline. The platform adapters are exercised against the real clients with only globalThis.fetch faked, so no credentials and no network are needed.

There is also an opt-in live probe that talks to a real noCTF deployment. It is read-only (it never registers, solves or writes) and skipped by default:

K17_LIVE=1 \
K17_API_TOKEN=<session token> \
K17_DIVISION_ID=2 \
K17_API_USERNAME=<username> \
pnpm test:live

K17_API_BASE_URL defaults to https://api-k17ctf.secso.cc. The token is optional: while the competition is active, anonymous reads are enough for every assertion. Set K17_API_TOKEN when the deployment gates the routes (before the start, or when hidden challenges matter), and K17_API_USERNAME to exercise the user lookup.

Code Style

This project uses tabs for indentation. Please ensure your editor is configured accordingly.

Logging

The bot includes a unified logging system built on top of the Sapphire framework's logger.

Log Levels

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=info

Where Logs Go

All 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.

Logger Architecture

The logger (src/lib/logger.js) works in two modes:

  1. Pre-init mode: Before the bot has logged into Discord, the logger falls back to raw console calls with ISO timestamps.
  2. Post-init mode: Once the SapphireClient is ready, the logger delegates to Sapphire's structured logger for consistent formatting across all commands and listeners.

Context Prefixes

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

Adding Logging to a New Module

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).

Contributing

Contributions are welcome! Please read our comprehensive guides:

Quick Start for Contributors

  1. Fork the repository
  2. Clone your fork: git clone https://github.com/YOUR_USERNAME/ctfbot.git
  3. Install dependencies: pnpm install
  4. Create a branch: git checkout -b feature/your-feature
  5. Make your changes
  6. Run linting: pnpm run lint:fix
  7. Commit: git commit -m "feat: add your feature"
  8. Push: git push origin feature/your-feature
  9. Open a Pull Request

See CONTRIBUTING.md for detailed guidelines.

Roadmap

  • 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

License

This project is licensed under the MIT License β€” see the LICENSE file for details.

Support

  • Issues: GitHub Issues
  • Discord: Join our server for support and discussions

Acknowledgments


Made with ❀️ by UCS ITS

About

UCS's specialized Discord bot for CTF

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages