# Architecture This page explains how the bot is put together, so you know where to look and where to make a change. ## The big picture ``` Discord gateway (websocket) | discordgo session | +--------------------------+---------------------------+ | | | interactions gateway events background loops (slash commands, (messages, joins, bans, (Twitch, reminders, buttons, modals) voice, audit log, ...) raid watch, snapshots) | | | internal/commands internal/events internal/events, security | | | +----------- services (rules) <------ repos (SQL) -----+ | PostgreSQL <----> velox-dashboard-api <----> dashboard ``` The bot is one Go process. It keeps one websocket connection to Discord and one connection pool to PostgreSQL. It has no HTTP server of its own. ## Startup `main.go` wires everything in this order: 1. **Config**: `config.LoadConfig()` reads the environment (and `.env`). 2. **Database**: `db.NewDB()` opens a `database/sql` pool using the pgx driver. 3. **Repos**: one repo per area (`levelrepo`, `settingsrepo`, `ticketrepo`, ...). A repo is the only place that writes SQL. 4. **Services**: small structs holding the rules for one area (`level`, `meeting`, `welcome`, ...), built on top of repos, collected in `services.Services`. 5. **Singletons for the bigger modules**: tickets and security get their repo with `cmdtickets.Init(...)` and `security.Configure(...)`, and are reached later through `tickets.Default()` and `security.Default()`. 6. **Bot**: `bot.NewBot()` creates the Discord session and chooses the gateway intents. 7. **`bot.Start()`** opens the connection and then: - starts the music player (if Lavalink is configured) and its command listener, - starts the ticket and security engines and their command listeners (each reconnects every 10 seconds if its database connection drops), - registers every slash command in `GUILD_ID` (or globally), - registers the gateway event handlers, - starts the Twitch and schedule-reminder loops. 8. **`bot.WatchBeta()`** reads the beta settings from the database and keeps them fresh every 30 seconds. See [Beta features](beta-features.md). 9. The process waits for `SIGINT` or `SIGTERM`. ## Layers The code follows one direction: commands and events call services, services call repos, repos run SQL. | Layer | Where | Responsibility | | --- | --- | --- | | Commands | `internal/commands/` | Describe the slash command, check who is allowed, read the options, answer the user. No SQL. | | Events | `internal/events` | React to gateway events and run background loops. | | Engines | `internal/security`, `internal/tickets`, `internal/music` | The bigger modules, with their own state and flows. | | Services | `internal/db/services/` | Rules and orchestration (for example the XP rules in `level`). | | Repos | `internal/db/repos/repo` | SQL, and nothing else. | Commands reach services through the package-level `services.Global`, set by `services.NewServices()`. A shared helper in each command package (for example `RequireMeetingService`) answers "service not available" if it is nil. Event handlers receive the services as an argument. ## How a slash command runs 1. Discord sends an interaction. discordgo calls `commands.HandleInteraction` (`internal/commands/commands.go`). 2. For a slash command, the dispatcher looks the command's name up in the `handlers` map and calls the handler. 3. The handler checks the context (server only? permissions?), finds the subcommand in `i.ApplicationCommandData().Options`, reads the options, calls a service, and responds with `s.InteractionRespond`. Buttons, select menus and modals do not have a command name. They carry a **custom ID**, and the dispatcher sends them to a feature by its **prefix**: `music:`, `schedule:`, `projects:`, `tickets:` or `security:`. A feature owns everything after its prefix. Some commands have a response that takes longer than three seconds (anything that calls Discord several times). Those answer with a deferred response first, then edit it when done. Search the code for `InteractionResponseDeferredChannelMessageWithSource` for examples. ## How a gateway event runs `bot.Start()` registers one handler per event with `Session.AddHandler`. Most of them hand the event to a function in `internal/events`, together with the services. A few go to the security engine first, because it must decide before anything else happens: - **Member joins**: the security engine runs first. If it removes the member (join lock, Join Gate, raid mode) there is no welcome message or default role. Otherwise the welcome, default role and join log run. - **Messages**: the security engine's heat check runs, then the leveling handler. - **Audit log entries**: only the security engine (anti-nuke, unauthorised bots). - **Voice state changes**: meeting rooms and the music player. ## Background loops | Loop | Where | Interval | What it does | | --- | --- | --- | --- | | Twitch live check | `events/twitch_live.go` | 30 s | Checks configured streamers and posts when one goes live | | Schedule reminders | `events/schedule_reminders.go` | 1 min | DMs participants 10 minutes before a session | | Raid watch | `security` | 30 s | Ends raid mode after a quiet period | | Snapshots | `security` | 6 h | Saves roles and channels while anti-nuke is on (keeps three) | | Beta settings | `bot/bot.go` (`WatchBeta`) | 30 s | Re-reads who the beta testers are and which servers opted in, and adds or removes beta commands to match | | Command listeners | music, tickets, security | on `NOTIFY` | Carry out dashboard requests | ## The link to the dashboard The bot and the dashboard API do not talk to each other directly. They meet in PostgreSQL, in two ways: 1. **Settings tables.** The dashboard writes settings (log channel, security thresholds, ticket categories, beta role). The bot reads them when it needs them. Hot paths cache them for a short time, so a raid of thousands of joins does not become thousands of queries. 2. **Command tables with `NOTIFY`.** When the dashboard needs the bot to *do* something (skip a song, post the ticket panel, release a quarantined member), it inserts a row in `music_commands`, `ticket_commands` or `security_commands` and issues `NOTIFY` on that channel. The bot has a long-lived connection that runs `LISTEN`, picks up the row, validates it again and carries it out. Never trust a row just because it is in the table: the listeners re-check permissions themselves. Both directions are described table by table in [Database](database.md). ## Gateway intents Set in `internal/bot/bot.go`: | Intent | Why | | --- | --- | | Guilds | slash commands, channel and role events | | Guild Moderation (bans, audit log) | ban logs, anti-nuke | | Guild Members (privileged) | joins, leaves, member updates | | Guild Messages and Message Content (privileged) | leveling, spam heat, message logs | | Guild Voice States | meeting rooms, music | | Direct Messages | direct messages sent to the bot | If you add a feature that needs another event, add its intent here too, and enable it in the developer portal if it is privileged. ## Concurrency and safety notes - discordgo calls event handlers on goroutines, so handlers must not share unsynchronised state. The engines use mutexes, and per-server memory is bounded. - Security-sensitive actions do not rely on Discord's command permissions, because server admins can change those. The handlers check the owner and the trusted list themselves (see the [Security](features.md#security) section). - User-controlled text (usernames, reasons) is escaped or kept out of mentions. Messages only ping roles that are configured for it. - Settings that decide who has access (the beta role) fail closed: if they cannot be read, nobody gets access. ## Where things live | I want to find... | Look in | | --- | --- | | The definition and handler of `/foo` | `internal/commands/foo/public/` (the `registry.go` next to it re-exports it) | | The list of all commands and the dispatcher | `internal/commands/commands.go` | | What happens on a gateway event | `internal/bot/bot.go`, then `internal/events/` | | The SQL behind a feature | `internal/db/repos/repo/repo.go` | | The rules behind a feature | `internal/db/services//` | | The table definitions | `schema.sql` and [Database](database.md) | | Environment variables | `internal/config/config.go` |