Files
broccolini-bot/README.md
indifferentketchup ca63ecbcfd readme
2026-04-06 23:56:56 -05:00

25 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 per-staff notification channels (reply alerts with cooldown + unclaimed-ticket digests), saved responses, /tag categorization, claimer emoji in channel names (STAFF_EMOJIS), and automation (auto-close, reminders, auto-unclaim, claim timeout).

Jump to: Features · Quick start · Configuration · Staff notifications · Commands · Project layout


Table of contents


Features

Email → Discord

  • Polls Gmail about every 30 seconds for new unread primary mail (gmail-poll.js).
  • Creates a Discord text channel (or thread, per guild settings) per email ticket, with overflow category support when a category is full.
  • Detects game from subject/body using GAME_LIST and built-in aliases in config.js.
  • 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, claim timeout, 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 notifications (optional)

  • /notification add creates a dedicated text channel per staff member under STAFF_NOTIFICATION_CATEGORY_ID and stores it in StaffNotification (MongoDB).
  • When a non-staff user replies in a ticket claimed by someone who has a notification channel, the bot posts an alert there (subject to per-ticket cooldown hours, configurable via /notification set or admin /staffnotification).
  • A background job runs every 30 minutes and, if UNCLAIMED_REMINDER_THRESHOLDS is set, posts unclaimed ticket digests to those same channels when tickets cross age thresholds.
  • /notifydm toggles optional DM alerts to the claimer on customer reply (separate from the notification channel); stored in StaffSettings.

See Staff notification channels.

Note: Older docs referred to per-staffer mirror channels driven by STAFF_CATEGORIES. In current config.js that map is deprecated and always empty, and createStaffChannel is not called from the claim flow—staffChannelId on tickets is effectively unused. Reply alerts use StaffNotification channels instead.

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 when the relevant API key env vars are set (see .env.example).

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                      │
│                    services/staffNotifications.js                  │
│                           │                                      │
│                           ▼                                      │
│                    MongoDB (Mongoose) ◄── models.js              │
│                                                                  │
│  Express: GET / → "Active" ; optional /api → routes/             │
└─────────────────────────────────────────────────────────────────┘

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+; Docker image uses 20 (Dockerfile).
npm npm install locally; npm ci --omit=dev in Docker.
MongoDB Atlas or self-hosted; MONGODB_URI required at startup.
Discord application Bot token, application ID; 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. On Unix shells: npm run start:test (sets ENV_FILE). On Windows PowerShell: $env:ENV_FILE='.env.test'; node broccolini-discord.js (or set ENV_FILE in the environment your process manager uses). npm run test-mongodb:test has the same ENV_FILE pattern.
  • 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. Names below match .env.example unless noted.

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 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, when used.
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; alias ROLE_TO_PING_ID in code.
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 notifications, claimer display, admin

Variable Description
STAFF_NOTIFICATION_CATEGORY_ID Category where /notification add creates per-staffer notification channels.
STAFF_EMOJIS Comma-separated discordUserId:emoji pairs; used in channel name when a ticket is claimed.
CLAIMER_EMOJI_FALLBACK Emoji if the claimer has no STAFF_EMOJIS entry.
ADMIN_ID Discord user ID allowed to use /staffnotification (override cooldown for another member).
UNCLAIMED_REMINDER_THRESHOLDS Comma-separated hours (e.g. 1,2,4); drives unclaimed ticket alerts into notification channels.

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 (optionally with ENV_FILE / .env.test as above).

Server & optional HTTP 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.

Additional variables for mounting /api (API key, CORS, etc.) are listed in .env.example if you use that integration.

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_ENABLED, CLAIM_TIMEOUT_HOURS.
  • Priority: PRIORITY_ENABLED, DEFAULT_PRIORITY, PRIORITY_*_EMOJI.

Game list

GAME_LIST=comma,separated,names — used for detection/normalization in email handling (plus aliases in config.js).

Thread-style tickets (legacy)

USE_THREADS, THREAD_PARENT_CHANNEL (see .env.example) — optional legacy paths; primary behavior is also governed by GuildSettings.emailRouting (/email-routing: thread | category).


Staff notification channels & reply alerts

When STAFF_NOTIFICATION_CATEGORY_ID is set:

  1. /notification add (with a target member) creates a channel under that category and saves userIdchannelId + default cooldown in StaffNotification.
  2. /notification set hours: (16) updates the cooldown between reply alerts for that users claimed tickets (same ticket keys off gmailThreadId).
  3. /staffnotification (admin, ADMIN_ID) sets cooldown for another staff member.
  4. On messageCreate, if the ticket has a claimerId and the author is not detected as having ROLE_ID_TO_PING, notifyStaffOfReply may post in the claimers notification channel (respecting cooldown).
  5. Every 30 minutes, notifyAllStaffUnclaimed evaluates open tickets with claimedBy: null against UNCLAIMED_REMINDER_THRESHOLDS and posts to all configured notification channels (tracks sent thresholds on the ticket in unclaimedReminderssent).

/notifydm toggles StaffSettings.notifyDm for the invoking user; when enabled, claimers can also receive a DM on customer reply (in addition to any notification channel).


Running the bot (test and Docker)

npm start
# or
node broccolini-discord.js

Test / alternate env file: see Installation for ENV_FILE on Windows vs Unix.

npm run test-mongodb

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. A sample docker-compose.yml exists—adjust ports and env_file for your host (do not copy production-specific bind addresses into new deployments without review).


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 setting: on / off — DM when a non-staff user replies in a ticket you claimed.
/notification Subcommands: set (cooldown hours), add (create notification channel for a member).
/staffnotification Admin only (ADMIN_ID); override another members notification cooldown.
/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 (choices from TICKET_TAGS in config.js).
/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 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.
  • 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 the fixed list in config.js (TICKET_TAGS). 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 (game lists, TICKET_TAGS, STAFF_EMOJIS map, …)
├── db-connection.js           # Mongo connect + require models
├── models.js                  # Mongoose schemas (Ticket, Tag, StaffSettings, StaffNotification, …)
├── 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; notifydm; notification alerts
│   ├── accountinfo.js
│   ├── analytics.js
│   └── setup.js
├── services/
│   ├── gmail.js
│   ├── tickets.js             # Auto-close, reminders, auto-unclaim, naming helpers
│   ├── channelQueue.js        # enqueueRename / enqueueMove
│   ├── staffChannel.js        # Legacy mirror helpers (unused in current claim flow)
│   ├── staffNotifications.js  # Reply alerts + unclaimed reminders
│   ├── staffSettings.js       # notifydm prefs
│   ├── guildSettings.js
│   └── debugLog.js
├── routes/                    # Optional Express `/api` routes
├── api/                       # Bot client accessor for HTTP layer
├── scripts/                   # Maintenance / one-off utilities
├── docs/                      # Deeper guides (setup, security, MongoDB, API notes)
├── Dockerfile
├── docker-compose.yml
├── 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 label, claimerId), legacy staffChannelId, escalation tier, welcomeMessageId, ticketTag, unclaimedReminderssent, 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).
StaffNotification Per-user channelId, cooldownHours for reply/unclaimed alerts.
CloseRequest Pending close workflow if used.
User, Host, DashboardMetrics, ErrorLog Shared / website-era schemas in the same models.js file.

HTTP: healthcheck & optional API

  • GET / → plain text Active (intended for load balancers / Docker HEALTHCHECK).
  • /api/* is registered only after the bot is ready and the optional HTTP API is enabled via env (see .env.example). JSON body parsing is enabled; auth uses a Bearer token from configuration. Route definitions live under routes/ in this repo.

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)
1PASSWORD.md 1Password CLI for npm run start:1p

Development & CI

This repo includes .gitlab-ci.yml with GitLab SAST and secret detection templates. Adjust or extend stages in GitLab as needed for your fork.


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.
Test script env on Windows npm run start:test sets ENV_FILE Unix-style; use PowerShell ENV_FILE + node if the script fails.

References

Technology Link
discord.js v14 discord.js guide
Google APIs (Gmail) googleapis Node
Mongoose mongoosejs.com
Express expressjs.com

License

ISC