2026-03-28 20:55:36 -05:00
2026-02-17 21:49:58 -06:00
2026-03-28 20:07:17 -05:00
2026-02-22 19:37:48 -06:00
2026-03-28 20:55:36 -05:00
2026-02-17 21:49:58 -06:00
2026-02-22 19:38:39 -06:00
2026-03-28 20:55:36 -05:00
2026-02-10 08:22:19 -06:00
2026-02-22 19:05:08 -06:00
2026-02-17 21:49:58 -06:00
2026-03-28 20:55:36 -05:00
2026-03-28 20:55:36 -05:00
2026-02-22 20:05:34 -06:00
2026-02-17 21:49:58 -06:00
2026-03-28 20:55:36 -05:00
2026-02-10 08:22:19 -06:00
2026-03-28 22:40:43 +00:00
2026-03-28 22:40:43 +00:00
2026-02-10 08:22:19 -06:00
2026-03-28 22:39:05 +00:00
2026-02-22 19:05:08 -06:00
2026-03-28 20:55:36 -05:00
2026-03-28 20:55:36 -05:00
2026-03-28 18:39:00 -05:00
2026-03-28 20:55:36 -05:00

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

Description
No description provided
Readme 2.4 MiB
Languages
JavaScript 85.2%
CSS 8.1%
HTML 6.6%