Files
broccolini-bot/README.md
2026-03-28 20:55:36 -05:00

464 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Broccolini Bot
A **Node.js** Discord bot that unifies **Gmail**, **Discord**, and **MongoDB** for support ticketing. Incoming emails become Discord ticket channels; staff messages in those channels are sent back to customers via Gmail. Discord-originated tickets (panels, context menu) live entirely in Discord. State is stored in MongoDB via Mongoose.
Built for game-server hosting support (Indifferent Broccoli), with game detection from email content, tiered escalation, optional staff “mirror” channels per claimer, saved responses, `/tag` categorization, and automation (auto-close, reminders, auto-unclaim).
**Jump to:** [Features](#features) · [Quick start](#quick-start) · [Configuration](#configuration) · [Staff categories](#staff-personal-categories--mirror-channels) · [Commands](#discord-commands) · [Project layout](#project-structure)
---
## Table of contents
- [Features](#features)
- [Architecture](#architecture)
- [Prerequisites](#prerequisites)
- [Quick start](#quick-start)
- [Installation](#installation)
- [Configuration](#configuration)
- [Staff personal categories & mirror channels](#staff-personal-categories--mirror-channels)
- [Running the bot](#running-the-bot-test-and-docker)
- [Discord commands](#discord-commands)
- [Ticket UI (buttons & modals)](#ticket-ui-buttons--modals)
- [Tag & response system](#tag--response-system)
- [Panel system](#panel-system)
- [Channel renames & moves (rate limits)](#channel-renames--moves-rate-limits)
- [Project structure](#project-structure)
- [Database collections](#database-collections)
- [HTTP: healthcheck & bOSScord API](#http-healthcheck--bosscord-api)
- [Gmail OAuth refresh token](#gmail-oauth-refresh-token)
- [Documentation in `docs/`](#documentation-in-docs)
- [Troubleshooting](#troubleshooting)
- [References](#references)
- [License](#license)
---
## Features
### Email → Discord
- Polls Gmail about every **30 seconds** for new mail.
- Creates a **Discord text channel** per email ticket (with overflow category support when a category is full).
- Detects **game** from subject/body using `GAME_LIST`.
- Posts welcome + action **buttons** (Close, Claim, Escalate / De-escalate where applicable).
### Discord → Gmail
- For **email-sourced** tickets, staff messages in the ticket channel are **forwarded** to the customer via Gmail (threaded).
- **Discord-only** tickets (`gmailThreadId` prefix `discord-` / `discord-msg-`) do not use Gmail for replies; conversation stays in Discord.
### Ticket management
- **Claim / Unclaim** via buttons (not slash commands); optional claim overwrite and auto-unclaim.
- **Priority** (`low` / `normal` / `medium` / `high`) with configurable emojis and `/priority`.
- **Escalation**: tier 2 and tier 3 categories (separate IDs for email vs Discord where configured); slash `/escalate` and in-channel buttons.
- **De-escalation** one step at a time (`/deescalate` or button).
- **Close** with confirmation; **force-close** for admins.
- **Transcripts** posted to a configured channel; closure email for email tickets.
- **Auto-close**, **inactivity reminders**, **auto-unclaim** (all optional via env).
### Staff personal categories (optional)
- Per-staff Discord **category map** so when someone **claims**, the main ticket channel can move into their category and a **mirror channel** can be created with a pinned embed linking back to the real ticket.
- When a **non-claimer** posts in the ticket channel, the mirror channel can be **pinged** with a quote and jump link; optional **DM** via `/notifydm`.
- Unclaim and close clean up mirror channels when configured.
See [Staff personal categories](#staff-personal-categories--mirror-channels).
### Extras
- **`/panel`**: “Open ticket” UI (modal collects email, game, description).
- **`/tag`**: ticket category dropdown; **`/response`**: saved templates with variable substitution.
- **`/setup`**: setup wizard for guild defaults.
- **`/accountinfo`**: website account lookup (email or Discord user).
- **`/stats`**, **`/search`**, **`/backup`**, **`/export`**, **`/email-routing`**.
- **Context menus**: create ticket from message; view user tickets.
- **Optional REST API** under `/api` for the bOSScord cockpit when `BOSSCORD_API_KEY` is set.
---
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ BROCCOLINI BOT │
├─────────────────────────────────────────────────────────────────┤
│ Gmail (inbox) ──► gmail-poll.js ──► Discord ticket channels │
│ │ ▲ │
│ ▼ │ │
│ services/gmail.js ◄── handlers/messages.js │
│ services/tickets.js handlers/buttons.js │
│ services/channelQueue.js handlers/commands.js│
│ services/staffChannel.js │
│ │ │
│ ▼ │
│ MongoDB (Mongoose) ◄── models.js │
│ │
│ Express: GET / → "Active" ; optional /api → routes/bosscord.js │
└─────────────────────────────────────────────────────────────────┘
```
**Typical email ticket lifecycle**
1. New unread mail → poll creates Discord channel + `Ticket` document.
2. Staff reply in channel → message handler sends Gmail reply (email tickets only).
3. Close confirmed → transcript, optional closure email, channel delete; DB marked closed.
---
## Prerequisites
| Requirement | Notes |
|-------------|--------|
| **Node.js** | **18+** recommended (Dockerfile uses 20). |
| **npm** | Install dependencies with `npm install`. |
| **MongoDB** | Atlas or self-hosted; connection string in `MONGODB_URI`. |
| **Discord application** | Bot token, application ID, privileged intents: **Message Content**, **Server Members**; also Guilds + Guild Messages. |
| **Google Cloud** | Gmail API enabled; OAuth2 client ID/secret + refresh token for the support mailbox. |
---
## Quick start
```bash
git clone <your-repo-url>
cd broccolini-bot
npm install
cp .env.example .env
```
1. Fill **Discord** (`DISCORD_TOKEN` or `DISCORD_BOT_TOKEN`, `DISCORD_APPLICATION_ID`, `DISCORD_GUILD_ID`, categories, `ROLE_ID_TO_PING`, transcript/log channels).
2. Fill **MongoDB** (`MONGODB_URI`).
3. Fill **Google** OAuth (`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `REFRESH_TOKEN`, `MY_EMAIL`) — use `node get-refresh-token.js` once if needed.
4. Run `npm start`.
5. In Discord, use **`/setup`** or verify categories and roles manually.
Restart after **any** `.env` change. After changing **slash command definitions**, restart so **`registerCommands()`** re-registers with Discord.
---
## Installation
Same as quick start. Optional:
- **Test env:** copy `.env.test.example``.env.test`, run `npm run start:test` (sets `ENV_FILE`).
- **1Password CLI:** `npm run start:1p` / `start:test:1p` inject secrets (see `docs/setup/1PASSWORD.md`).
**Do not commit** `.env` or `.env.test`. AI/agents should not edit production `.env` without explicit approval; see [ENV_AND_SECURITY.md](docs/setup/ENV_AND_SECURITY.md).
---
## Configuration
Configuration is **environment variables** only, loaded in [`config.js`](config.js) as `CONFIG`. Discord env names in tables below match `.env.example`.
### Discord (core)
| Variable | Required | Description |
|----------|----------|-------------|
| `DISCORD_TOKEN` | Yes* | Bot token (*or `DISCORD_BOT_TOKEN`, first non-empty wins after trim). |
| `DISCORD_APPLICATION_ID` | Yes | Used as `CLIENT_ID` for REST command registration. |
| `DISCORD_GUILD_ID` | Yes | Guild where slash commands are registered. |
| `TICKET_CATEGORY_ID` | Yes | Default category for **email** tickets (startup also validates this). |
| `DISCORD_TICKET_CATEGORY_ID` | No | Category for **Discord** panel/context tickets (falls back to `TICKET_CATEGORY_ID`). |
| `EMAIL_THREAD_CHANNEL_ID` / `DISCORD_THREAD_CHANNEL_ID` | No | Parent **text** channels for thread-style tickets, if you use threads. |
| `EMAIL_TICKET_OVERFLOW_CATEGORY_IDS` | No | Comma-separated extra categories when the main is full (50 channels). |
| `DISCORD_TICKET_OVERFLOW_CATEGORY_IDS` | No | Same for Discord ticket category. |
| `ROLE_ID_TO_PING` | Yes | Support role pinged on new tickets; aliases include `ROLE_TO_PING_ID` in code paths. |
| `ADDITIONAL_STAFF_ROLES` | No | Extra role IDs treated as staff for commands. |
| `BLACKLISTED_ROLES` | No | Roles blocked from opening tickets. |
| `TRANSCRIPT_CHANNEL_ID` | No | Where transcripts are posted on close. |
| `LOGGING_CHANNEL_ID` | No | Ticket lifecycle logs. |
| `DEBUGGING_CHANNEL_ID` | No | Optional error/debug forwarding. |
| `BACKUP_EXPORT_CHANNEL_ID` | No | Target for `/backup` and `/export`. |
| `ACCOUNT_INFO_CHANNEL_ID` | No | Account info flows. |
### Escalation categories
| Variable | Description |
|----------|-------------|
| `EMAIL_ESCALATED_CATEGORY_ID` | Legacy fallback; alias `ESCALATED_CATEGORY_ID`. |
| `DISCORD_ESCALATED_CATEGORY_ID` | Discord fallback tier-2style bucket. |
| `DISCORD_ESCALATED2_CHANNEL_ID` | Tier **2** placement for Discord tickets (or + fallback category). |
| `EMAIL_ESCALATED2_CHANNEL_ID` | Tier **2** for email tickets (env name says CHANNEL for legacy reasons). |
| `DISCORD_ESCALATED3_CHANNEL_ID` | Tier **3** Discord. |
| `EMAIL_ESCALATED3_CHANNEL_ID` | Tier **3** email. |
Slash `/escalate` and buttons require the appropriate tier IDs for **non-thread** channels (threads skip category moves).
### Staff personal categories & mirror channels
Optional organizational layer: **main ticket channel** stays where customers and staff talk; **mirror channel** is per-staffer category for notes + pings.
| Variable | Description |
|----------|-------------|
| `STAFF_CATEGORIES` | Map: `discordUserId:categoryId,discordUserId2:categoryId2`. If a claimer has **no** entry, **no** mirror channel is created (silent skip). |
| `STAFF_T1_CATEGORY` | Category for **unclaimed** “normal” tickets after unclaim (rename uses 🟢 prefix when rename limits allow). |
| `STAFF_T2_CATEGORY` | Pool category for **unclaimed tier-2** escalated tickets (used when moving after escalation / de-escalation flows). |
| `STAFF_T3_CATEGORY` | Pool category for **unclaimed tier-3** escalated tickets. |
| `UNCLAIMED_CATEGORY_ID` | Reserved in config for future/general fallback (currently not wired in code beyond `CONFIG`). |
If any of `STAFF_T1_CATEGORY` / `STAFF_T2_CATEGORY` / `STAFF_T3_CATEGORY` is unset, the corresponding **move is skipped** (no error).
### Google / Gmail
| Variable | Required | Description |
|----------|----------|-------------|
| `GOOGLE_CLIENT_ID` | Yes | OAuth2 client ID. |
| `GOOGLE_CLIENT_SECRET` | Yes | OAuth2 secret. |
| `REFRESH_TOKEN` | Yes | Long-lived refresh for the inbox account. |
| `MY_EMAIL` | Yes | Canonical support address (lowercased in config). |
### MongoDB
| Variable | Required |
|----------|----------|
| `MONGODB_URI` | Yes |
Test: `npm run test-mongodb` (or with `ENV_FILE=.env.test`).
### Server & optional API
| Variable | Default | Description |
|----------|---------|-------------|
| `DISCORD_ONLY_PORT` | `5000` | Express listen port (`CONFIG.PORT`). |
| `HEALTHCHECK_HOST` | *(all interfaces)* | e.g. `127.0.0.1` for local-only bind. |
| `BOSSCORD_API_KEY` | — | If set, mounts **`/api`** (bOSScord); use a strong random key. |
| `BOSSCORD_CORS_ORIGIN` | `*` | Optional CORS for the API. |
### Messaging & branding
See `.env.example` for defaults: `ESCALATION_MESSAGE` (`{support_name}`), `TICKET_WELCOME_MESSAGE`, `TICKET_CLAIMED_MESSAGE` / `TICKET_UNCLAIMED_MESSAGE` (`{staff_mention}`, `{staff_name}`), `DISCORD_CLOSE_MESSAGE`, `DISCORD_TRANSCRIPT_MESSAGE` (`{channel_name}`, `{email}`, `{date_opened}`, `{date_closed}`), `EMAIL_SIGNATURE` (`\n``<br>`), embed color hex vars, button labels/emojis, `SUPPORT_NAME`, `LOGO_URL`.
### Automation & limits
- **Auto-close:** `AUTO_CLOSE_ENABLED`, `AUTO_CLOSE_AFTER_HOURS`, `AUTO_CLOSE_MESSAGE`.
- **Reminders:** `REMINDER_ENABLED`, `REMINDER_AFTER_HOURS`, `REMINDER_MESSAGE` (`{ping}`, `{hours}`).
- **Limits:** `GLOBAL_TICKET_LIMIT`, `TICKET_LIMIT_PER_CATEGORY`, `RATE_LIMIT_TICKETS_PER_USER`, `RATE_LIMIT_WINDOW_MINUTES`.
- **Claim:** `ALLOW_CLAIM_OVERWRITE`, `AUTO_UNCLAIM_*`, `CLAIM_TIMEOUT_*`.
- **Priority:** `PRIORITY_ENABLED`, `DEFAULT_PRIORITY`, `PRIORITY_*_EMOJI`.
### Game list
`GAME_LIST=comma,separated,names` — used for detection/normalization in email handling.
---
## Staff personal categories & mirror channels
When configured:
1. **Claim:** DB stores `claimerId` (Discord user id) and optional `staffChannelId`. Main channel may **move** to `STAFF_CATEGORIES.get(claimerId)`. A **mirror** text channel may be created under that staffers category, with a **pinned embed** (ticket number, customer, game, subject, link to original channel).
2. **Customer / other user messages** in the real ticket channel: mirror channel gets a **ping**, quote (truncated), and **jump link**; if the claimer enabled **`/notifydm`**, they also get a **DM** (`StaffSettings` in MongoDB).
3. **Unclaim:** mirror channel is **deleted**; `claimerId` / `staffChannelId` cleared; channel may rename with 🟢 and move to `STAFF_T1_CATEGORY` if set.
4. **Escalation / de-escalation:** slash and button flows may apply **priority emojis** to names and move channels to `STAFF_T2_CATEGORY` / `STAFF_T3_CATEGORY` / `STAFF_T1_CATEGORY` when configured; mirror channels can move with the tier category.
5. **Close:** mirror channel is **deleted** when the ticket closes.
---
## Running the bot (test and Docker)
```bash
npm start
# or
node broccolini-discord.js
```
**Test:**
```bash
npm run start:test
npm run test-mongodb:test
```
**Docker** (see [`Dockerfile`](Dockerfile)):
```bash
docker build -t broccolini-bot .
docker run --env-file .env -p 5000:5000 broccolini-bot
```
Ensure `MONGODB_URI` and Discord token are available inside the container.
---
## Discord commands
Most commands require **staff** (`ROLE_ID_TO_PING` or `ADDITIONAL_STAFF_ROLES`). **`/help`** is available more broadly per registration.
| Command | Description |
|---------|-------------|
| **`/setup`** | Guild setup wizard (panel, role, category, transcript channel, etc.). |
| **`/panel`** | Post a ticket **Open** button in a channel (optional `type`: thread / category / both; custom title/description). |
| **`/email-routing`** | Choose whether **new email** tickets create **threads** vs **category channels** (`GuildSettings` in DB). |
| **`/escalate`** | **Required:** `level` (Tier 2 or Tier 3), `action` (`unclaim` clears `claimedBy` + `claimerId` after escalation, `keep` preserves claim). |
| **`/deescalate`** | Step down one tier (tier 3 → 2 → normal). |
| **`/notifydm`** | `on` / `off` — DM when a **non-claimer** replies in a ticket you claimed (mirror ping still applies). |
| **`/add`**, **`/remove`** | Add/remove user overwrites on the current ticket channel. |
| **`/transfer`** | Set `claimedBy` to another staff member (must have staff role). |
| **`/move`** | Move channel to another **category** (direct `setParent`). |
| **`/force-close`** | Close without button confirmation (still archives transcript best-effort). |
| **`/topic`** | Set Discord channel topic. |
| **`/priority`** | `low` / `normal` / `medium` / `high`. |
| **`/tag`** | Set ticket tag category from dropdown. |
| **`/response`** | Subcommands: `send`, `create`, `edit`, `delete`, `list` (saved responses). |
| **`/accountinfo`** | Subcommands: `email`, `discord`. |
| **`/search`** | Search tickets by email, subject, or number. |
| **`/stats`** | Bot analytics snapshot. |
| **`/backup`**, **`/export`** | Post TSV exports to `BACKUP_EXPORT_CHANNEL_ID`. |
| **`/help`** | In-bot command summary embed. |
**Context menus**
- **Create Ticket From Message** — opens a ticket prefilled from a message.
- **View User Tickets** — lists recent tickets for a user (by sender tag match).
---
## Ticket UI (buttons & modals)
- **Open ticket** (panel): modal fields are **account email**, **game**, **description** (not “priority” in the modal).
- In ticket channels: **Close**, **Claim/Unclaim**, **Escalate** (tier choice), **De-escalate** as built in [`utils/ticketComponents.js`](utils/ticketComponents.js) / [`handlers/buttons.js`](handlers/buttons.js).
- **Email routing** and **tag delete** confirmations use additional button custom IDs.
---
## Tag & response system
### `/tag`
Sets `ticketTag` from a fixed list (Server Down, Billing, etc.). Channel naming may incorporate tag/priority emojis via ticket naming logic.
### `/response`
Templates support variables such as `{ticket.user}`, `{ticket.email}`, `{ticket.number}`, `{ticket.subject}`, `{staff.name}`, `{staff.mention}`, `{server.name}`, `{date}`, `{time}` (see [`utils.js`](utils.js) / handler docs).
---
## Panel system
1. Run **`/panel`** targeting a channel (and optional style: thread-only, category-only, or both buttons).
2. User clicks **Open ticket** → modal → bot creates thread or channel per configuration.
3. Welcome embeds + action row are posted; `Ticket` stores `discordThreadId`, `ticketNumber`, etc.
---
## Channel renames & moves (rate limits)
Discord allows **two renames per 10 minutes** per channel. The bot serializes renames/moves through [`services/channelQueue.js`](services/channelQueue.js) (`p-queue`). If rename is blocked, staff see a message with a **relative time** to retry.
---
## Project structure
```
broccolini-bot/
├── broccolini-discord.js # Entry: Discord client, Express, Gmail poll interval, jobs
├── config.js # Env → CONFIG (+ STAFF_CATEGORIES map, game lists)
├── db-connection.js # Mongo connect + require models
├── models.js # Mongoose schemas (Ticket, Tag, StaffSettings, …)
├── utils.js # Email/game helpers, template variables
├── utils/ticketComponents.js # Action row builders
├── gmail-poll.js # Ingest Gmail → Discord ticket creation
├── get-refresh-token.js # One-shot OAuth refresh token helper
├── commands/register.js # Slash + context menu registration (discord.js v14)
├── handlers/
│ ├── buttons.js # Claim/close/modals/escalate buttons, ticket create modal
│ ├── commands.js # Slash handlers, runEscalation/runDeescalation
│ ├── messages.js # Staff ↔ Gmail relay; mirror pings + notifydm
│ ├── accountinfo.js
│ ├── analytics.js
│ └── setup.js
├── services/
│ ├── gmail.js
│ ├── tickets.js # Auto-close, reminders, auto-unclaim, naming helpers
│ ├── channelQueue.js # enqueueRename / enqueueMove
│ ├── staffChannel.js # Mirror create/ping/move/delete
│ ├── staffSettings.js # notifydm prefs
│ ├── guildSettings.js
│ └── debugLog.js
├── routes/bosscord.js # Optional /api routes
├── api/bosscordClient.js
├── scripts/ # Maintenance / one-off utilities
├── docs/ # Deeper guides (setup, security, MongoDB, API notes)
├── Dockerfile
├── package.json
└── .env.example / .env.test.example
```
---
## Database collections
| Model / collection | Role |
|--------------------|------|
| **Ticket Gmail thread id, Discord channel/thread id, status, priority, claim (`claimedBy` display name), `claimerId`, `staffChannelId`, escalation tier, `welcomeMessageId`, `ticketTag`, etc.** |
| **TicketCounter** | Per-sender local counters (legacy paths). |
| **Transcript** | Links closed tickets to transcript message IDs. |
| **Tag** | Saved response name + content. |
| **GuildSettings** | e.g. `emailRouting`: `thread` \| `category`. |
| **StaffSettings** | Per-user `notifyDm` (+ `guildId`, `updatedAt`). |
| **CloseRequest** | Pending close workflow if used. |
| **User**, **Host**, **DashboardMetrics**, **ErrorLog** | Shared / website-era schemas in the same `models.js` file. |
---
## HTTP: healthcheck & bOSScord API
- **`GET /`** → plain text **`Active`** (intended for load balancers / Docker `HEALTHCHECK`).
- **`/api/*`** is registered **only after** `ready` when `BOSSCORD_API_KEY` is set. JSON body parsing enabled. See [`routes/bosscord.js`](routes/bosscord.js) for routes.
---
## Gmail OAuth refresh token
```bash
node get-refresh-token.js
```
Requires `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in `.env`, and redirect URI **`http://localhost:3000/oauth2callback`** registered on the Google OAuth client. Paste the printed refresh token into `.env` as `REFRESH_TOKEN`.
---
## Documentation in `docs/`
Index: **[docs/README.md](docs/README.md)**. Highlights:
| Doc | Topic |
|-----|--------|
| [ENV_AND_SECURITY.md](docs/setup/ENV_AND_SECURITY.md) | Secrets, test env, agent rules |
| [MONGODB_SETUP.md](docs/setup/MONGODB_SETUP.md) | Database |
| [QUICKSTART.md](docs/setup/QUICKSTART.md) | First-time orientation |
| [PROJECT_STRUCTURE.md](docs/setup/PROJECT_STRUCTURE.md) | Layout (may overlap this README) |
---
## Troubleshooting
| Symptom | Checks |
|---------|--------|
| **Commands missing** | Correct `DISCORD_APPLICATION_ID` + `DISCORD_GUILD_ID`; **restart** bot; Discord can take time to sync. |
| **Gmail not ingesting** | `REFRESH_TOKEN`, API enablement, inbox auth; regenerate token if revoked. |
| **MongoDB errors** | `MONGODB_URI`, Atlas IP allowlist, `npm run test-mongodb`. |
| **Channels not creating** | Bot **Manage Channels** in ticket categories; category not full (50) unless overflow set. |
| **Modal / button no response** | Intents + permissions; bot online; check `DEBUGGING_CHANNEL_ID` / console. |
| **Renames “too quickly”** | Discord rename cooldown; wait for channel queue / timestamp in bot message. |
---
## References
| Technology | Link |
|------------|------|
| discord.js v14 | [discord.js guide](https://discordjs.guide/) |
| Google APIs (Gmail) | [googleapis Node](https://github.com/googleapis/google-api-nodejs-client) |
| Mongoose | [mongoosejs.com](https://mongoosejs.com/) |
| Express | [expressjs.com](https://expressjs.com/) |
---
## License
ISC