Dynamic overflow categories

This commit is contained in:
indifferentketchup
2026-03-28 20:55:36 -05:00
parent 6b4fd65d4b
commit 1496a96274
10 changed files with 679 additions and 584 deletions

779
README.md
View File

@@ -1,39 +1,33 @@
# Broccolini Bot
A Node.js support-ticket bot that connects **Gmail**, **Discord**, and **MongoDB** into a unified ticketing system. Incoming support emails become Discord ticket channels; staff replies in Discord are sent back to the sender via Gmail. All ticket state is persisted in MongoDB.
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, configurable automation (auto-close, reminders, auto-unclaim), and a full set of Discord slash commands, buttons, modals, and context menus.
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).
**Quick links:** [Installation](#installation) · [Configuration](#configuration) · [Discord Commands](#discord-commands) · [Documentation](#documentation)
**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
## Table of contents
- [Features](#features)
- [Architecture](#architecture)
- [Prerequisites](#prerequisites)
- [Quick start](#quick-start)
- [Installation](#installation)
- [Configuration](#configuration)
- [Discord](#discord)
- [Google OAuth2 / Gmail](#google-oauth2--gmail)
- [MongoDB](#mongodb)
- [Branding & Messages](#branding--messages)
- [Automation](#automation)
- [Ticket Limits & Permissions](#ticket-limits--permissions)
- [Priority Levels](#priority-levels)
- [Claiming Options](#claiming-options)
- [Button & Embed Customization](#button--embed-customization)
- [Running the Bot](#running-the-bot)
- [Test Environment](#test-environment)
- [Discord Commands](#discord-commands)
- [Tag System](#tag-system)
- [Panel System](#panel-system)
- [Project Structure](#project-structure)
- [Database Schema](#database-schema)
- [API Integrations](#api-integrations)
- [Healthcheck](#healthcheck)
- [Documentation](#documentation)
- [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)
@@ -42,532 +36,425 @@ Built for game-server hosting support (Indifferent Broccoli), with game detectio
## Features
### Email-to-Discord Ticketing
- Polls Gmail every 30 seconds for unread emails in the primary inbox
- Creates a dedicated Discord channel per ticket (`ticket-{sender}-{number}`)
- Detects the game from the email subject/body and tags the ticket accordingly
- Sends a rich embed with ticket metadata and action buttons (Claim, Close)
### EmailDiscord
### Discord-to-Email Replies
- Staff messages in a ticket channel are forwarded to the original sender via Gmail
- Replies are threaded in Gmail so the sender sees a continuous conversation
- 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).
### Ticket Management
- **Claim / Unclaim** -- Staff can claim tickets; optional auto-unclaim after inactivity
- **Priority Levels** -- Low, Normal, High with color-coded embeds
- **Escalation** -- Move urgent tickets to a dedicated escalation category
- **Transfer / Move** -- Reassign tickets between staff or categories
- **Close Confirmation** -- Prevents accidental closes with a confirmation prompt
- **Transcripts** -- Full conversation transcripts posted to a dedicated channel on close
- **Auto-Close** -- Automatically close tickets after configurable hours of inactivity
- **Inactivity Reminders** -- Notify the channel when a ticket goes stale
### Discord → Gmail
### Panel System
- Deploy a "Open Ticket" button panel to any channel with `/panel`
- Users click the button, fill out a modal form, and a ticket is created
- 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.
### Tag System (Saved Responses)
- Set ticket category with `/tag` (dropdown); create reusable response templates with `/response create`
- Dynamic template variables: `{ticket.user}`, `{staff.name}`, `{server.name}`, `{date}`, etc.
- Autocomplete-enabled `/tag` command for instant use
### Ticket management
### Account Info Lookup
- `/accountinfo` searches website users by email or Discord ID
- Results show linked servers, game details, and user metadata
- **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).
### Analytics & Logging
- In-memory tracking of command usage, button clicks, and errors
- `/stats` shows uptime, interaction counts, and error rate
- Configurable logging channel for ticket lifecycle events
### 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 │─────>│ gmail-poll.js │─────>│ Discord
(inbox) (every 30s) │ │ (ticket channel)│
└───────────┘ └───────┬────────┘ └───────▲──────────┘
│ │
v
┌────────────────┐ ┌──────────────────┐
│ services/ handlers/
│ gmail.js │<────>│ messages.js
│ tickets.js buttons.js
│ guildSettings │ │ commands.js │
│ └───────┬────────┘ └──────────────────┘
│ │ │
│ v │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ MongoDB │ │ Express │ │
│ │ (Mongoose) │ │ (healthcheck) │ │
│ └────────────────┘ └──────────────────┘ │
│ │
│ Events: │
│ ready → Connect DB, register commands, start jobs │
│ interactionCreate → Buttons, slash commands, modals, menus │
│ messageCreate → Discord replies → Gmail │
└────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 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
└─────────────────────────────────────────────────────────────────┘
```
**Ticket lifecycle:**
**Typical email ticket lifecycle**
1. **Inbound email** -- Gmail poll detects a new unread message, creates a Discord channel and a MongoDB record.
2. **Staff reply** -- A message in the Discord ticket channel is forwarded to the sender via Gmail.
3. **Close** -- A transcript is generated, a closure email is sent, and the Discord channel is deleted.
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 | Version |
|-------------|---------|
| Node.js | ≥ 18.x |
| npm | ≥ 9.x |
| MongoDB | ≥ 5.x (Atlas or self-hosted) |
| 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. |
You will also need:
- A **Discord bot** with the following intents enabled: Guilds, Guild Messages, Message Content, Guild Members
- A **Google Cloud project** with the Gmail API enabled and OAuth2 credentials (Client ID, Client Secret, Refresh Token)
---
## Installation
Single-level repo: all commands run from the repo root. Create `.env` in the repo root (copy from `.env.example`).
## Quick start
```bash
git clone <your-repo-url>
cd broccolini-bot
npm install
cp .env.example .env
# Edit .env with your Discord, Gmail, and MongoDB credentials (see Configuration).
```
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
Create a `.env` file in the repo root (same directory as this README). All configuration is loaded via environment variables.
Configuration is **environment variables** only, loaded in [`config.js`](config.js) as `CONFIG`. Discord env names in tables below match `.env.example`.
> **Important:** After changing `.env`, you must **restart the process** (`npm start` / `node broccolini-discord.js`) for new values to take effect. If you add or change **slash commands** (e.g. `/escalate`, `/email-routing`, `/panel` options), restart the bot so it can **re-register** commands with Discord; otherwise new or updated commands may not appear.
> **Agent rule:** Changes to `.env` by an AI/agent must **require explicit user confirmation**. Prefer proposing changes to `.env.test` first and migrating to `.env` only after the user approves. See [ENV_AND_SECURITY.md](docs/setup/ENV_AND_SECURITY.md).
### Discord
### Discord (core)
| Variable | Required | Description |
|---|---|---|
| `DISCORD_TOKEN` | Yes | Bot token from the Discord Developer Portal |
| `DISCORD_GUILD_ID` | Yes | Server (guild) ID where the bot operates |
| `DISCORD_APPLICATION_ID` | Yes | Application ID for registering slash commands |
| `TICKET_CATEGORY_ID` | Yes | Channel category ID where email ticket channels are created |
| `EMAIL_TICKET_OVERFLOW_CATEGORY_IDS` | No | Comma-separated category IDs; used when main email category has 50 channels |
| `DISCORD_TICKET_CATEGORY_ID` | No | Category for Discord panel tickets (defaults to `TICKET_CATEGORY_ID`) |
| `DISCORD_TICKET_OVERFLOW_CATEGORY_IDS` | No | Comma-separated category IDs; used when main Discord ticket category has 50 channels |
| `ROLE_ID_TO_PING` | Yes | Role ID to ping when a new ticket arrives |
| `TRANSCRIPT_CHANNEL_ID` | No | Channel ID for posting ticket transcripts |
| `LOGGING_CHANNEL_ID` | No | Channel ID for lifecycle log messages |
| `DEBUGGING_CHANNEL_ID` | No | Channel ID for error logs (escalate, deescalate, email-routing, Gmail poll, etc.) |
| `BACKUP_EXPORT_CHANNEL_ID` | No | Channel ID where `/backup` and `/export` post ticket dump files |
| `ACCOUNT_INFO_CHANNEL_ID` | No | Channel ID for account info lookups (and `/accountinfo` visibility) |
| `EMAIL_ESCALATED_CATEGORY_ID` | No | Category ID for escalated email tickets (tier 2+) |
| `DISCORD_ESCALATED_CATEGORY_ID` | No | Category ID for escalated Discord-origin tickets |
| `ESCALATION_MESSAGE` | No | Message sent when a ticket is escalated (supports `{support_name}`) |
|----------|----------|-------------|
| `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. |
### Google OAuth2 / Gmail
### 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 from Google Cloud Console |
| `GOOGLE_CLIENT_SECRET` | Yes | OAuth2 Client Secret |
| `REFRESH_TOKEN` | Yes | OAuth2 Refresh Token for the support inbox |
| `MY_EMAIL` | Yes | The support email address (e.g. `support@example.com`) |
|----------|----------|-------------|
| `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 | Description |
|---|---|---|
| `MONGODB_URI` | Yes | MongoDB connection string (e.g. `mongodb+srv://user:pass@cluster/dbname`) |
| Variable | Required |
|----------|----------|
| `MONGODB_URI` | Yes |
### Branding & Messages
Test: `npm run test-mongodb` (or with `ENV_FILE=.env.test`).
### Server & optional API
| Variable | Default | Description |
|---|---|---|
| `SUPPORT_NAME` | -- | Display name for the support system |
| `LOGO_URL` | -- | URL to the logo shown in embeds |
| `EMAIL_SIGNATURE` | -- | HTML signature appended to outgoing emails (use `\n` for line breaks) |
| `TICKET_CLOSE_SUBJECT_PREFIX` | `[Resolved]` | Prefix added to the subject of closure emails |
| `TICKET_CLOSE_MESSAGE` | *(see config.js)* | Body of the ticket closure email |
| `TICKET_CLOSE_SIGNATURE` | *(see config.js)* | Signature on the closure email |
| `TICKET_WELCOME_MESSAGE` | *(see config.js)* | Message posted when a ticket channel is created |
| `TICKET_CLAIMED_MESSAGE` | *(see config.js)* | Message posted when a ticket is claimed (supports `{staff_name}`) |
| `TICKET_UNCLAIMED_MESSAGE` | *(see config.js)* | Message posted when a ticket is unclaimed |
|----------|---------|-------------|
| `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. |
### Automation
### Messaging & branding
| Variable | Default | Description |
|---|---|---|
| `AUTO_CLOSE_ENABLED` | `false` | Enable automatic ticket closure after inactivity |
| `AUTO_CLOSE_AFTER_HOURS` | `72` | Hours of inactivity before auto-close triggers |
| `AUTO_CLOSE_MESSAGE` | *(see config.js)* | Message sent when a ticket is auto-closed |
| `REMINDER_ENABLED` | `false` | Enable inactivity reminder messages |
| `REMINDER_AFTER_HOURS` | `24` | Hours of inactivity before a reminder is sent |
| `REMINDER_MESSAGE` | *(see config.js)* | Reminder message (supports `{hours}` variable) |
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`.
### Ticket Limits & Permissions
### Automation & limits
| Variable | Default | Description |
|---|---|---|
| `GLOBAL_TICKET_LIMIT` | `5` | Maximum concurrent open tickets globally |
| `TICKET_LIMIT_PER_CATEGORY` | `3` | Maximum tickets per category |
| `RATE_LIMIT_TICKETS_PER_USER` | `0` | Max tickets a user can create per window (0 = disabled) |
| `RATE_LIMIT_WINDOW_MINUTES` | `60` | Window in minutes for per-user ticket creation limit |
| `BLACKLISTED_ROLES` | -- | Comma-separated role IDs that cannot open tickets |
| `ADDITIONAL_STAFF_ROLES` | -- | Comma-separated role IDs with staff-level permissions |
- **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`.
### Priority Levels
### Game list
| Variable | Default | Description |
|---|---|---|
| `PRIORITY_ENABLED` | `false` | Enable the priority system |
| `DEFAULT_PRIORITY` | `normal` | Default priority for new tickets |
| `PRIORITY_HIGH_EMOJI` | `🔴` | Emoji for high-priority tickets |
| `PRIORITY_MEDIUM_EMOJI` | `🟡` | Emoji for normal/medium-priority tickets (default level is normal) |
| `PRIORITY_LOW_EMOJI` | `🟢` | Emoji for low-priority tickets |
### Claiming Options
| Variable | Default | Description |
|---|---|---|
| `AUTO_UNCLAIM_ENABLED` | `false` | Automatically unclaim tickets after inactivity |
| `AUTO_UNCLAIM_AFTER_HOURS` | `24` | Hours before auto-unclaim triggers |
| `ALLOW_CLAIM_OVERWRITE` | `false` | Allow claiming an already-claimed ticket |
| `CLAIM_TIMEOUT_ENABLED` | `false` | Enable claim timeout |
| `CLAIM_TIMEOUT_HOURS` | `48` | Hours before a claim times out |
### Channel rename rate limit
Ticket channels are renamed automatically when you **claim**, **unclaim**, **escalate**, or **deescalate**. [Discords API](https://discord.com/developers/docs/topics/rate-limits) allows **2 channel renames per 10 minutes** per channel. The bot enforces this: if the limit is reached, the rename is skipped and the channel gets:
**Channel renamed too quickly. Try again \<t:*unlock_timestamp*:R\>.**
The timestamp is a Discord relative-time marker (e.g. “in 8 minutes”). After the window resets, the next claim/unclaim/escalate/deescalate can rename again.
### Button & Embed Customization
| Variable | Default | Description |
|---|---|---|
| `BUTTON_LABEL_CLOSE` | `Close Ticket` | Label for the close button |
| `BUTTON_LABEL_CLAIM` | `Claim` | Label for the claim button |
| `BUTTON_LABEL_UNCLAIM` | `Unclaim` | Label for the unclaim button |
| `BUTTON_EMOJI_CLOSE` | `🔒` | Emoji on the close button |
| `BUTTON_EMOJI_CLAIM` | `📌` | Emoji on the claim button |
| `BUTTON_EMOJI_UNCLAIM` | `🔓` | Emoji on the unclaim button |
| `EMBED_COLOR_OPEN` | `0x00FF00` | Embed color for open tickets |
| `EMBED_COLOR_CLOSED` | `0xFF0000` | Embed color for closed tickets |
| `EMBED_COLOR_CLAIMED` | `0xFFFF00` | Embed color for claimed tickets |
| `EMBED_COLOR_ESCALATED` | `0xFF6600` | Embed color for escalated tickets |
| `EMBED_COLOR_INFO` | `0x1e2124` | Embed color for info messages (and embeds next to ticket buttons) |
### Game List
Set `GAME_LIST` to a comma-separated list of game names. The bot uses this list for auto-detection from email subjects/bodies:
```env
GAME_LIST=Project Zomboid, Satisfactory, Palworld, Minecraft, Valheim, ...
```
`GAME_LIST=comma,separated,names` — used for detection/normalization in email handling.
---
## Running the Bot
## 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
# Start the bot
npm start
# Or directly
# or
node broccolini-discord.js
```
On startup the bot will:
1. Validate required environment variables
2. Connect to MongoDB (with automatic reconnection)
3. Register all slash commands to the configured guild
4. Begin polling Gmail every 30 seconds
5. Start background jobs (auto-close, reminders, auto-unclaim)
6. Launch an Express healthcheck server
**Note:** Changing `.env` requires restarting the bot. Slash commands are registered on startup; if commands dont update, restart the bot to re-register.
### Test Environment
To try config changes without affecting production, use a **test env**. Copy `.env.test.example` to `.env.test`, fill it with test-only values (e.g. test guild, test MongoDB database), and run:
**Test:**
```bash
npm run start:test
npm run test-mongodb:test
```
Other test scripts: `npm run test-mongodb:test`. After confirming behavior in test, migrate only the desired variables to `.env`. See **[ENV_AND_SECURITY.md](docs/setup/ENV_AND_SECURITY.md)** for the full workflow, security checklist, and agent rules.
**Docker** (see [`Dockerfile`](Dockerfile)):
To test the MongoDB connection from the repo root: `npm run test-mongodb`.
---
## Discord Commands
### Ticket Management
| Command | Description |
|---|---|
| `/claim` | Claim the current ticket |
| `/unclaim` | Release your claim on the current ticket |
| `/close` | Close the current ticket (with confirmation) |
| `/force-close` | Close the current ticket without confirmation |
| `/priority <level>` | Set ticket priority (`low`, `normal`, `medium`, `high`). Posts: *upgraded to [Emoji][Level][Emoji]*, *downgraded to...*, or *returned to Normal*. Email sent when set to **high**. |
| `/topic <text>` | Set the ticket channel topic |
| `/escalate [reason] [tier]` | Escalate the ticket to tier 2 or 3 (optional tier; buttons also available) |
| `/deescalate` | De-escalate the ticket one step |
### User & Channel Management
| Command | Description |
|---|---|
| `/add <user>` | Add a user to the current ticket channel |
| `/remove <user>` | Remove a user from the current ticket channel |
| `/transfer <staff>` | Transfer the ticket to another staff member |
| `/move <category>` | Move the ticket to a different category |
### Tags & Saved Responses
| Command | Description |
|---|---|
| `/tag` | Set ticket category (dropdown: ⬇️ Server Down, ⏳ Stuck Restarting, 📵 Can't Connect, 🐌 Server Lag, 💳 Billing, 💸 Refund Request, 🔧 Mod Help, 💾 Backup Restore, 🌍 World / Save, ⚙️ Server Config). Posts: *Your ticket has been categorized as [Emoji][Tag][Emoji].* |
| `/response send <name>` | Send a saved response (autocomplete-enabled) |
| `/response create <name> <content>` | Create a new saved response |
| `/response edit <name> <content>` | Edit an existing saved response |
| `/response delete <name>` | Delete a saved response |
| `/response list` | List all saved responses |
### Utilities
| Command | Description |
|---|---|
| `/panel [channel] [type] [title] [description]` | Deploy a ticket-creation panel (type: thread, category, or both) |
| `/email-routing` | Switch where new email tickets are created (threads or category channels) |
| `/accountinfo <email or discord>` | Look up a user's account information |
| `/search <query>` | Search tickets |
| `/stats` | Show bot statistics and analytics |
| `/backup` | Export full ticket list to a .txt file in the backup/export channel |
| `/export [status] [limit]` | Export tickets (optional filter and limit) to a .txt file in the backup/export channel |
| `/help` | Display the command reference |
### Context Menus
| Menu | Description |
|---|---|
| **Create Ticket From Message** | Right-click a message to create a ticket from it |
---
## Tag & Response System
### Ticket category (`/tag`)
Use `/tag` in a ticket channel and pick a category from the dropdown (e.g. ⬇️ Server Down, 💳 Billing, 🔧 Mod Help). The bot posts: *Your ticket has been categorized as [Emoji][Tag][Emoji].* Channel name is not changed.
### Saved response tags (`/response`)
Saved responses are reusable templates stored in MongoDB. Use `/response send`, `/response create`, etc. They support dynamic variables that are replaced at send time:
| Variable | Resolves To |
|---|---|
| `{ticket.user}` | Ticket sender's name |
| `{ticket.email}` | Ticket sender's email |
| `{ticket.number}` | Ticket number |
| `{ticket.subject}` | Ticket subject line |
| `{staff.name}` | Current staff member's display name |
| `{staff.mention}` | Current staff member's mention |
| `{server.name}` | Discord server name |
| `{date}` | Current date |
| `{time}` | Current time |
**Example:**
```
/response create name:greeting content:Hi {ticket.user}! Thanks for reaching out about "{ticket.subject}". I'm {staff.name} and I'll be helping you today.
```bash
docker build -t broccolini-bot .
docker run --env-file .env -p 5000:5000 broccolini-bot
```
---
## Panel System
The panel system allows users to create tickets directly from Discord without sending an email.
1. Deploy a panel: `/panel #support title:Need Help? description:Click below to open a ticket!`
2. Users click the **Open Ticket** button
3. A modal form appears asking for subject, description, and priority
4. On submission, a ticket channel is created with all the same features as email tickets
Ensure `MONGODB_URI` and Discord token are available inside the container.
---
## Project Structure
## 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 point - initializes bot, events, and jobs
├── config.js # Environment variable loading and CONFIG export
├── db-connection.js # MongoDB connection with reconnect logic
├── models.js # Mongoose schemas (Ticket, User, Tag, etc.)
├── utils.js # Text processing, game detection, template vars
├── gmail-poll.js # Gmail polling loop and ticket creation
├── game-options.json # Game configuration data
├── commands/
│ └── register.js # Slash command and context menu registration
├── 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/
│ ├── accountinfo.js # /accountinfo command and button handler
│ ├── analytics.js # In-memory analytics and error tracking
│ ├── buttons.js # Button interactions (claim, close, priority, etc.)
│ ├── commands.js # All slash command handlers
│ ├── messages.js # Discord → Gmail reply forwarding
│ └── setup.js # Guild setup / configuration flow
│ ├── 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/
│ ├── debugLog.js # Structured debug logging
│ ├── gmail.js # Gmail OAuth2, send replies, closure emails
│ ├── guildSettings.js # Guild-specific settings (DB + cache)
── tickets.js # Ticket CRUD, auto-close, reminders, auto-unclaim
├── scripts/
── backup-env.js # Copy .env to .env.backup
│ └── test-mongodb.js # MongoDB connection test
├── docs/ # Additional documentation (QUICKSTART, MONGODB_SETUP, ENV_AND_SECURITY, etc.)
├── .env # Environment variables (not committed)
│ ├── 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
└── package-lock.json
└── .env.example / .env.test.example
```
---
## Database Schema
## Database collections
The bot uses MongoDB via Mongoose. Key collections:
| Collection | Purpose |
|---|---|
| `Ticket` | Core ticket data: Gmail thread ID, Discord channel ID, sender info, status, priority, claimed-by, timestamps |
| `TicketCounter` | Auto-incrementing ticket numbers per sender |
| `Transcript` | Transcript message references for closed tickets |
| `Tag` | Saved response templates (name, content, creator) |
| `CloseRequest` | Tracks pending close confirmations |
| `User` | Website user accounts (email, Discord ID, linked servers) |
| `Host` | Game server/host metadata and metrics |
| `DashboardMetrics` | Aggregated dashboard statistics |
| `ErrorLog` | Persisted error records |
| 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. |
---
## API Integrations
## HTTP: healthcheck & bOSScord API
### Gmail API
- **Authentication:** OAuth2 with Client ID, Client Secret, and Refresh Token
- **Polling:** `users.messages.list` for unread messages in the primary inbox
- **Reading:** `users.messages.get` to fetch full message content
- **Sending:** `users.messages.send` for threaded replies and closure emails
### Discord API (discord.js v14)
- **Intents:** Guilds, GuildMessages, MessageContent, GuildMembers
- **Interactions:** Slash commands, buttons, modals, context menus, autocomplete
- **Channels:** Create/delete ticket channels, manage permissions per user
## Healthcheck
An Express server runs on the port defined by `DISCORD_ONLY_PORT` (default: `5000`).
```
GET / → "Active"
```
Use this endpoint for uptime monitoring or container health probes. Optional: set `HEALTHCHECK_HOST=127.0.0.1` in `.env` to bind the healthcheck server to localhost only; omit to listen on all interfaces.
- **`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.
---
## Documentation
## Gmail OAuth refresh token
Additional guides and reference docs live in **`docs/`**. See [docs/README.md](docs/README.md) for the full index.
```bash
node get-refresh-token.js
```
| Doc | Description |
|-----|-------------|
| [QUICKSTART](docs/setup/QUICKSTART.md) | Get started in a few minutes: first response, panel, tags, priority |
| [ENV_AND_SECURITY](docs/setup/ENV_AND_SECURITY.md) | Test env workflow, security checklist, agent rules |
| [MONGODB_SETUP](docs/setup/MONGODB_SETUP.md) | MongoDB connection, schemas, and testing |
| [PROJECT_STRUCTURE](docs/setup/PROJECT_STRUCTURE.md) | File and directory layout |
| [PROPOSAL](docs/features/PROPOSAL.md) | Roadmap and possible next steps |
| [PHASE_FEATURES](docs/features/PHASE_FEATURES.md) | Phased feature list and variables |
| [FEATURES_SUMMARY](docs/features/FEATURES_SUMMARY.md) · [NEW_FEATURES](docs/features/NEW_FEATURES.md) | Feature overview and changelog |
| [DISCORD_API_IMPROVEMENTS](docs/api/DISCORD_API_IMPROVEMENTS.md) · [DISCORD_API_VALIDATION](docs/api/DISCORD_API_VALIDATION.md) | Discord API implementation notes |
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
### Slash commands not appearing in Discord
- Commands are registered per-guild on startup. Wait up to one hour for Discord to propagate.
- Verify `DISCORD_APPLICATION_ID` and `DISCORD_GUILD_ID` are correct.
- Restart the bot.
### Gmail polling not working
- Ensure `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, and `REFRESH_TOKEN` are set correctly.
- The refresh token may have expired -- regenerate it via the Google OAuth2 Playground.
- Check that the Gmail API is enabled in your Google Cloud Console project.
### MongoDB connection failures
- Verify `MONGODB_URI` is correct and the database is accessible.
- Run `npm run test-mongodb` from the repo root to test the connection.
- If using MongoDB Atlas, ensure your IP is whitelisted.
- The bot has automatic reconnection -- check logs for retry attempts.
### Tickets not creating
- Check that `TICKET_CATEGORY_ID` points to a valid Discord category.
- Ensure the bot has `Manage Channels` and `View Channel` permissions in that category.
- Review the logging channel for error messages.
### Modal not appearing when clicking "Open Ticket"
- Verify the bot has proper guild permissions.
- Try in a different channel.
- Restart the bot.
| 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
This project builds on or references the following:
| Technology | Description | Links |
|------------|-------------|--------|
| **discord.js** | Node.js library for the Discord API; used for the bot, slash commands, buttons, and embeds. | [discord.js](https://discord.js.org/) · [GitHub](https://github.com/discordjs/discord.js) |
| **Discord Tickets** | Open-source ticket bot; referenced for patterns and feature inspiration (panels, tags, transcripts). | [Discord Tickets](https://discordtickets.app/) · [GitHub](https://github.com/discord-tickets/bot) |
| **Node.js** | JavaScript runtime used to run the bot. | [Node.js](https://nodejs.org/en) |
| **MongoDB** | Database for tickets, transcripts, and persistence (via Mongoose). | [MongoDB](https://www.mongodb.com/) |
| **Express** | HTTP server for the healthcheck endpoint. | [Express](https://expressjs.com/) |
| **Mongoose** | MongoDB ODM used for schemas and connection handling. | [Mongoose](https://mongoosejs.com/) |
| **Google APIs (googleapis)** | Gmail API client for polling and sending email. | [Google APIs Node.js](https://github.com/googleapis/google-api-nodejs-client) |
| 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/) |
---