Files
broccolini-bot/README.md

22 KiB
Raw Blame History

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 · Quick start · Configuration · Staff categories · Commands · Project layout


Table of contents


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.

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

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.


Configuration

Configuration is environment variables only, loaded in 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)

npm start
# or
node broccolini-discord.js

Test:

npm run start:test
npm run test-mongodb:test

Docker (see Dockerfile):

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 / 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 / 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 (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 for routes.

Gmail OAuth refresh token

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. Highlights:

Doc Topic
ENV_AND_SECURITY.md Secrets, test env, agent rules
MONGODB_SETUP.md Database
QUICKSTART.md First-time orientation
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
Google APIs (Gmail) googleapis Node
Mongoose mongoosejs.com
Express expressjs.com

License

ISC