cleanup and simplify
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
120
HOWITWORKS.md
Normal file
120
HOWITWORKS.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# How it works
|
||||
|
||||
Broccolini Bot is a single Node.js process. It does three things at once:
|
||||
|
||||
1. **Listens to Discord** — slash commands, button clicks, modals, ticket-channel messages.
|
||||
2. **Polls Gmail** — every N seconds, pulls unread `category:primary` mail and turns each thread into a Discord ticket channel.
|
||||
3. **Serves a couple of HTTP endpoints** — a public healthcheck and an internal config/control API.
|
||||
|
||||
State lives in MongoDB via Mongoose. There is no queue/worker tier and no public REST API.
|
||||
|
||||
---
|
||||
|
||||
## Startup
|
||||
|
||||
`broccolini-discord.js` is the entry point. The order matters:
|
||||
|
||||
1. **Module load** — env validation, Discord `Client` is constructed, `interactionCreate` and `messageCreate` listeners are registered, `client.login()` is called.
|
||||
2. The **public Express app** is defined at module scope. It has a 503 gate — any `/api/*` request before the bot is ready returns 503. (No `/api/*` routes are mounted in the MVP, so it's dormant.)
|
||||
3. The **internal Express app** binds at module load on `INTERNAL_API_PORT` (`0.0.0.0` inside the container, not host-published). Reachable only from peers on the `broccoli-net` Docker network. Auth header: `x-internal-secret`.
|
||||
4. **`client.once('ready')`** — once Discord finishes its handshake the bot connects MongoDB, starts the public HTTP listener, registers slash commands with Discord's REST API, then starts the Gmail poll plus optional `setInterval`s for auto-close, auto-unclaim, sweeping orphan ticket channels, and a 6-hour Tickets sweep.
|
||||
|
||||
Every `setInterval` in the `ready` block is wrapped through `trackInterval(...)` into a module-level `Set`. `handleShutdown` (SIGTERM/SIGINT) clears all of them, closes both HTTP servers, calls `client.destroy()`, then closes Mongo.
|
||||
|
||||
---
|
||||
|
||||
## Email → Discord (the main flow)
|
||||
|
||||
`gmail-poll.js`:
|
||||
|
||||
1. Lists unread messages in `category:primary`.
|
||||
2. For each thread, looks up an existing `Ticket` by `gmailThreadId`. If none, creates a Discord channel under `TICKET_CATEGORY_ID` (or an overflow category if the main is full at Discord's 50-channel limit) and inserts a `Ticket` document.
|
||||
3. Posts a welcome embed + action row (Close / Claim / Escalate) into the channel and pings `ROLE_ID_TO_PING`.
|
||||
4. On subsequent emails in the same thread, just appends the new message to the existing channel.
|
||||
|
||||
**Auth failure halts polling.** On `invalid_grant` / `unauthorized` / 401, `pollSuspended` flips to true, the poll interval is cleared, and the admin is DM'd once. The bot does not retry — fix the token and restart the container.
|
||||
|
||||
---
|
||||
|
||||
## Discord → Gmail (replies)
|
||||
|
||||
`handlers/messages.js` handles `messageCreate` in ticket channels:
|
||||
|
||||
- If the ticket's `gmailThreadId` starts with `discord-` or `discord-msg-`, it's a Discord-only ticket — skip Gmail entirely.
|
||||
- Otherwise, the staff message is forwarded to the customer via Gmail (threaded reply) using `services/gmail.js`. The staffer's per-user signature (`StaffSignature`) is appended.
|
||||
|
||||
Customer replies coming back via email are picked up by the next Gmail poll and appended to the existing ticket channel.
|
||||
|
||||
---
|
||||
|
||||
## Discord-only tickets
|
||||
|
||||
Two paths create them:
|
||||
|
||||
- **`/panel`** posts an "Open ticket" button. Clicking it opens a modal asking for email, game, and description. The bot creates a channel and a `Ticket` with a synthetic `gmailThreadId` like `discord-<channelId>`.
|
||||
- **Context menu "Create Ticket From Message"** does the same, prefilled from the source message (`gmailThreadId` like `discord-msg-<msgId>`).
|
||||
|
||||
Replies in these channels stay in Discord. No Gmail traffic.
|
||||
|
||||
---
|
||||
|
||||
## Channel rename / move queue
|
||||
|
||||
Discord rate-limits channel renames at **2 per 10 minutes per channel**. All channel ops route through `services/channelQueue.js`:
|
||||
|
||||
- `enqueueRename`, `enqueueSend`, `enqueueMove`, `enqueueDelete` — per-channel chained promises. Delete waits for both rename and send tails to drain.
|
||||
- Renames go through `utils/renamer.js`, which uses the `RENAMER_BOT` secondary token. On 401/403/429 from the secondary, the queue falls back to the primary bot's `channel.setName`.
|
||||
- Bypass sites (direct `channel.send` / `setName`) are tagged `// TODO(queue-migrate):` — grep to find them; they get migrated incrementally when touched.
|
||||
|
||||
Logging helpers in `services/debugLog.js` are **fire-and-forget** — `.catch(() => {})`, never `await`. They post to the configured log channels (system, automation, error, gmail, etc.).
|
||||
|
||||
---
|
||||
|
||||
## HTTP surfaces
|
||||
|
||||
**Public** (`app`, port `CONFIG.PORT`, default `5000`):
|
||||
- `GET /` → `Active` once ready, `Starting` before. Used by Docker `HEALTHCHECK`.
|
||||
- `/api/*` is gated behind `appReady` and currently unmounted.
|
||||
|
||||
**Internal** (`internalApp`, `INTERNAL_API_PORT`, `broccoli-net` only, header auth):
|
||||
- `GET /config`, `POST /config` — read/write a strict allowlist (`ALLOWED_CONFIG_KEYS` in `services/configSchema.js`). Unknown keys → 400.
|
||||
- `GET /discord/guild` — basic guild info for the settings UI.
|
||||
- `POST /restart`, `GET /restart/status` — exits the process so the container supervisor restarts it.
|
||||
- `POST /gmail/reload` — reloads the Gmail client after credential changes.
|
||||
|
||||
`.env` writes go through `services/configPersistence.js`, which stores values in **backtick** containers because dotenv v17 only decodes `\n`/`\r` inside double-quoted strings.
|
||||
|
||||
---
|
||||
|
||||
## Storage
|
||||
|
||||
MongoDB, one database, accessed via Mongoose. Models live in `models.js`:
|
||||
|
||||
| Collection | What it stores |
|
||||
|------------|----------------|
|
||||
| `Ticket` | One per email thread or Discord-only ticket. Tracks `gmailThreadId`, `discordThreadId`, status, claimer, priority, escalation tier, ticket tag, last activity, etc. |
|
||||
| `TicketCounter` | Per-sender local counter for ticket numbering. |
|
||||
| `Transcript` | Closed ticket → transcript message pointer. |
|
||||
| `Tag` | Saved-response templates (`/response`). |
|
||||
| `StaffSettings` | Per-user `notifyDm` preference. |
|
||||
| `StaffSignature` | Per-user email signature (valediction, display name, tagline). |
|
||||
|
||||
The `Ticket` schema indexes `{gmailThreadId}` (unique), `{status, lastActivity}`, `{senderEmail, status}`, `{discordThreadId}`.
|
||||
|
||||
---
|
||||
|
||||
## Background jobs (in `ready`)
|
||||
|
||||
| Job | Cadence | Toggle |
|
||||
|-----|---------|--------|
|
||||
| Gmail poll | `GMAIL_POLL_INTERVAL_SECONDS` (default 30s; runtime-tunable via `/gmailpoll`) | always |
|
||||
| `checkAutoClose` | configurable | `AUTO_CLOSE_ENABLED` |
|
||||
| `checkAutoUnclaim` | configurable | `AUTO_UNCLAIM_ENABLED` |
|
||||
| `reconcileDeletedTicketChannels` | hourly + on startup | always |
|
||||
| `services/tickets.startTicketsSweeps` | 6h, `.unref()`-ed | always |
|
||||
|
||||
---
|
||||
|
||||
## Settings UI (separate process)
|
||||
|
||||
`settings-site/` is its own small Express app with its own `package.json`, `Dockerfile`, and `CLAUDE.md`. It serves a password-protected dashboard that POSTs config changes to this bot's internal API using `INTERNAL_API_SECRET`. Any change to `ALLOWED_CONFIG_KEYS` here can break the UI there — keep them in sync.
|
||||
Reference in New Issue
Block a user