A Discord bot for running a Greek-language hackathon — team channels, registration tracking, and submission follow-ups.
Built with discord.py. User-facing strings (questions in team channels, error messages) are in Greek; this README is in English so others can adapt it for their own event.
!assign_channels(DAY 1) — reads the registration sheet, creates one private text channel per team, and grants view permission to each registered member.!export_status(DAY 1–3) — merges raw registration form responses with live Discord state and writes a CSV (in_server,discord_id,discord_handle,channels_joined,team_match). Mirror the CSV to a Google Sheet to feed!check_missing.!check_missing(DAY 2–3) — for each team channel, walks the team through a short interactive flow asking about absent members and whether to add anyone not yet in the channel. Replies containingναιadd the user; per-prompt timeouts (15 min nudge, 30 min abort).!ping_unsubmitted(SUBMISSION) — pings each registered team that hasn't yet appeared in the submissions sheet (SUBMIT_URL), reminding them to submit.!confirm_submitted(SUBMISSION) — confirms receipt in the channels of teams whose names are in the submissions sheet.!purge_bot(CLEANUP) — bulk-deletes the bot's own recent messages (current channel or all). Subject to Discord's 14-day bulk-delete window.!find_mystery(CLEANUP) — lists humans on the server who have no roles and aren't in the registration source. Useful for spotting lurkers.
All admin commands require administrator permission on the guild.
Two distinct data sources feed three different cogs — do not conflate them.
┌───────────────────────────────────┐
│ cogs/assignments.py │──▶ creates #<team-slug> channels
│ !assign_channels │
└───────────────────────────────────┘
▲
SHEET_REGISTRATION_URL ────────────────┤
(raw form responses) ▼
┌───────────────────────────────┐
│ cogs/export.py !export_status │──▶ registration_with_status.csv
└───────────────────────────────┘ │
│ (mirror to Google Sheet)
▼
SHEET_URL (merged)
│
▼
┌──────────────────────────────────┐
│ cogs/check_missing.py │
│ !check_missing │
└──────────────────────────────────┘
See CLAUDE.md for a deeper write-up of conventions (Greek column names with trailing spaces, channel slugging, the username-matching ladder, etc.).
python -m venv python_env
source python_env/bin/activate
pip install -r requirements.txt
cp .env.example .env
# fill in DISCORD_TOKEN, GUILD_ID, and SHEET_REGISTRATION_URLThen provide your registration data — either a Google Sheet (SHEET_REGISTRATION_URL, recommended) or a local xlsx (REGISTRATION_FILE, fallback). See samples/registration.sample.csv for the expected columns.
Run the bot:
python bot.pyIf you'd rather not host the registration data on Google Sheets:
- Leave
SHEET_REGISTRATION_URLempty in.env. - Set
REGISTRATION_FILEto the path of an.xlsxwhose columns matchsamples/registration.sample.csv— including the trailing spaces in Greek headers likeΌνομα Ομάδας.
All three consumers (!assign_channels, !export_status, !find_mystery) go through the same loader at utils/data_sources.py, so the same Excel file works for every command.
The bot needs the following gateway intents (enabled in the Discord developer portal and in bot.py, both already configured):
membersmessage_content
And these guild permissions on the team channels (or via a server role):
Manage Channels— to create per-team channelsManage Permissions— to grant view-access to specific membersRead Message History+Manage Messages— for!purge_botSend Messages— to post intros, questions, and confirmations
| Command | Purpose | Key flags |
|---|---|---|
!assign_channels |
Create team channels from the registration sheet and grant access | --dry-run |
!export_status |
Merge registration data with live Discord state | — |
!check_missing |
Walk each team through absent / outside-channel members | --dry-run |
!ping_unsubmitted |
Remind teams missing from the submissions sheet | --dry-run |
!confirm_submitted |
Confirm receipt in channels of teams that submitted | --dry-run |
!purge_bot |
Bulk-delete the bot's own recent messages | --all, --limit N, --dry-run |
!find_mystery |
List roleless humans not in the registration source | — |
| Variable | Required? | Default | Notes |
|---|---|---|---|
DISCORD_TOKEN |
yes | — | Bot token from the Discord developer portal |
GUILD_ID |
yes | — | Numeric ID of the target server |
SHEET_REGISTRATION_URL |
one of these two | — | Google Sheet with raw form responses |
REGISTRATION_FILE |
one of these two | Registration_2026.xlsx |
Local xlsx fallback when sheet URL is unset |
SHEET_URL |
for !check_missing |
— | Merged sheet (the output of !export_status mirrored to Google) |
SUBMIT_URL |
for !ping_unsubmitted / !confirm_submitted |
— | Google Sheet of submissions; must contain a Team: column |
PRIVATE_CHANNEL_CATEGORY |
no | — | Category for created channels; created if missing |
MERGED_OUTPUT_FILE |
no | registration_with_status.csv |
Output path for !export_status |
bot.py # entry point; loads all cogs
config.py # reads .env, exposes constants and Greek column names
cogs/
assignments.py # !assign_channels
check_missing.py # !check_missing
export.py # !export_status
mystery.py # !find_mystery
purge.py # !purge_bot
submissions.py # !ping_unsubmitted, !confirm_submitted
utils/
data_sources.py # registration loader (Google Sheet OR local xlsx)
discord_helpers.py # member index, channels map, channel creation
parsing.py # username parsing, slugify, normalize
sheets.py # Google Sheet URL → CSV export URL
samples/ # anonymised shape examples for the input files
CLAUDE.md # architectural notes / conventions
Apache 2.0 — see LICENSE.
