Architecture¶
High-level shape of the system.
Components¶
┌────────────────────┐
│ Slash API │
│ (api.slash.com) │
└──────────┬─────────┘
│ events
▼ (POST webhook)
┌────────────────────────────────────────┐
│ GCP e2-micro VM │
│ │
│ Caddy :443 ── reverse_proxy ──┐ │
│ (auto-TLS via Let's Encrypt) │ │
│ ▼ │
│ Bot process (systemd-managed) │
│ │
│ ┌──────────────────┐ ┌────────────┐ │
│ │ aiohttp server │ │ discord.py │ │
│ │ :8080 │ │ Bot client │ │
│ │ (webhook recv) │ │ (slash cmds│ │
│ └────────┬─────────┘ │ + tasks) │ │
│ │ └────────────┘ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ webhook.handlers.dispatch │ │
│ │ (signature verify, dedupe, │ │
│ │ route to ingest + poster) │ │
│ └────────┬─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ core/* services + repos │ │
│ │ (members, payments, dues, │ │
│ │ rooms, transactions, ...) │ │
│ └────┬───────────────────┬─────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌────────┐ │
│ │ Mongo │ │ Slash │ │
│ │ (motor) │ │ HTTP │ │
│ └────┬────┘ │ client │ │
│ │ └───┬────┘ │
│ │ via TLS │ │
└────────┼───────────────────┼────────────┘
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ MongoDB Atlas │ │ Slash API │
│ (cluster0) │ │ (api calls) │
└────────────────┘ └────────────────┘
Discord gateway (WebSocket, outbound from VM)
▲
│
┌──────────┴──────────┐
│ Discord │
│ (slash commands, │
│ message posts) │
└─────────────────────┘
Code layout¶
.
├── main.py Entrypoint — wires bot + webhook server + tasks
├── config/
│ ├── settings.py pydantic-settings model from .env
│ └── db.py Mongo client + collection registry + index ensures
├── slash/ Slash HTTP API client
│ ├── client.py aiohttp wrapper, headers, retries
│ ├── exceptions.py typed errors (Auth, NotFound, Conflict, ...)
│ ├── webhook_verify.py RSA-SHA256 signature check (pinned public key)
│ ├── payloads/ Request body builders
│ ├── parsers/ Response parsers
│ └── requests/ Endpoint orchestrators (call payload + parser)
├── webhook/ aiohttp server + event routing
│ ├── server.py /health and /webhook/slash routes
│ ├── handlers.py dispatch + dedup + route
│ ├── txn_poster.py post/edit live txn cards
│ └── card_poster.py post card lifecycle events
├── ui/ Discord-side UI
│ ├── txn_card.py Embed builder for live txn cards
│ ├── txn_view.py View with [Add Details] / [Attach Receipt] buttons
│ ├── txn_modal.py Add Details modal (memo + purchased-by)
│ └── card_event.py Embed builder for card lifecycle events
├── core/ Pure business logic, no Discord imports
│ ├── members/ onboarding, deactivation
│ ├── semesters/ calendar (Sep 1–Dec 31, Jan 1–May 31) + repo
│ ├── rooms/ rooms + per-semester assignments + share math
│ ├── payments/ payment + deduction CRUD
│ ├── donations/ donation CRUD
│ ├── promises/ IOU CRUD + reminder helpers
│ ├── payment_plans/ plans + installment generation
│ ├── expenses/ recurring obligations
│ ├── dues/ derive owed/paid from payments + room + semester
│ ├── transactions/ Slash txn mirror in Mongo
│ ├── cards/ Slash card ↔ Discord user mapping
│ ├── receipts/ local file save + per-kind dispatch
│ ├── reminders/ personal todo list
│ ├── ideas/ feature backlog
│ ├── audit/ officer action log
│ ├── exports/ XLSX builders (dues + bank)
│ └── mcc/ MCC code → human category mapping
├── commands/ discord.py cogs (slash commands)
│ ├── _base/ shared decorators, embed builders, formatters
│ ├── admin/ /onboard, /list_members, /sync, ...
│ ├── semesters/ /set_semester, /current_semester, /end_semester
│ ├── rooms/ /set_room, /assign_room, ...
│ ├── dues/ /log_payment, /undo_payment, /deduct_dues, ...
│ ├── dues_view/ /my_dues, /dues_status, /dues_leaderboard
│ ├── promises/ /promise, /promises, /cancel_promise, ...
│ ├── payment_plans/ /payment_plan, /show_plan, ...
│ ├── donations/ /log_donation, /donations
│ ├── expenses/ /log_expense, /pay_expense, /expense_summary
│ ├── receipts/ /attach_receipt
│ ├── slash_view/ /balance, /spending, /charges, /txn, ...
│ ├── cards/ /link_card, /list_cards
│ ├── reminders/ /reminder, /reminders, autocomplete
│ ├── ideas/ /idea, /ideas
│ ├── audit/ /audit
│ ├── exports/ /export_dues, /export_bank
│ └── help.py /help — categorized listing
├── tasks/ discord.ext.tasks loops
│ ├── promise_reminders.py daily 13:00 UTC — promise DMs
│ ├── plan_reminders.py daily 13:00 UTC — plan installment DMs
│ ├── reminder_digest.py daily 13:00 UTC — personal reminder digest
│ └── txn_sync.py hourly Slash → Mongo catch-up
└── docs/ this site (mkdocs-material)
Layer separation¶
core/*modules never import Discord or Slash HTTP. They speak in terms of business entities (members, payments, dues state) and talk to Mongo via the repository pattern.commands/*cogs are thin wrappers overcore/*services. They handle Discord input parsing, error message formatting, and embed building. No business logic.webhook/*is the inbound side — receives external POSTs, dispatches to core services + the txn poster.ui/*is for Discord-side rendering (embeds, views, modals) shared between webhook posts and slash commands.
State¶
- MongoDB Atlas — the canonical store for everything bot-side: members, payments, dues, transactions mirror, audit, etc.
- Slash API — canonical for actual money state (balances, transactions). The bot mirrors but never mutates without an explicit user action.
- Local filesystem — receipt files under
/home/cryso/bot/receipts/<year>/<kind>/. Permanent until manually deleted. Back this up if you care.
Process model¶
One Linux process. discord.py (gateway) and aiohttp (webhook server) share the same asyncio event loop. Mongo (motor) and Slash HTTP calls (aiohttp client) are async too — never blocks the loop.
Restart impact:
- Slash command response: would 503 mid-restart, very brief.
- Webhook delivery: Slash retries automatically. May be a 1–2s delay during boot.
- Discord gateway reconnect: discord.py auto-resumes within a few seconds.
systemd's Restart=on-failure brings it back automatically if it crashes.