27 KiB
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.
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.
Quick links: Installation · Configuration · Discord Commands · Documentation
Table of Contents
- Features
- Architecture
- Prerequisites
- Installation
- Configuration
- Running the Bot
- Test Environment
- Discord Commands
- Tag System
- Panel System
- Project Structure
- Database Schema
- API Integrations
- Healthcheck
- Documentation
- Troubleshooting
- References
- License
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)
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
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
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
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
/tagcommand for instant use
Account Info Lookup
/accountinfosearches website users by email or Discord ID- Results show linked servers, game details, and user metadata
Analytics & Logging
- In-memory tracking of command usage, button clicks, and errors
/statsshows uptime, interaction counts, and error rate- Configurable logging channel for ticket lifecycle events
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 │
└────────────────────────────────────────────────────────────────────┘
Ticket lifecycle:
- Inbound email -- Gmail poll detects a new unread message, creates a Discord channel and a MongoDB record.
- Staff reply -- A message in the Discord ticket channel is forwarded to the sender via Gmail.
- Close -- A transcript is generated, a closure email is sent, and the Discord channel is deleted.
Prerequisites
| Requirement | Version |
|---|---|
| Node.js | ≥ 18.x |
| npm | ≥ 9.x |
| MongoDB | ≥ 5.x (Atlas or self-hosted) |
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).
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).
Configuration
Create a .env file in the repo root (same directory as this README). All configuration is loaded via environment variables.
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,/paneloptions), restart the bot so it can re-register commands with Discord; otherwise new or updated commands may not appear.
Agent rule: Changes to
.envby an AI/agent must require explicit user confirmation. Prefer proposing changes to.env.testfirst and migrating to.envonly after the user approves. See ENV_AND_SECURITY.md.
Discord
| 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}) |
Google OAuth2 / 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) |
MongoDB
| Variable | Required | Description |
|---|---|---|
MONGODB_URI |
Yes | MongoDB connection string (e.g. mongodb+srv://user:pass@cluster/dbname) |
Branding & Messages
| 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 |
Automation
| 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) |
Ticket Limits & Permissions
| 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 |
Priority Levels
| 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. Discord’s API 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:
GAME_LIST=Project Zomboid, Satisfactory, Palworld, Minecraft, Valheim, ...
Running the Bot
# Start the bot
npm start
# Or directly
node broccolini-discord.js
On startup the bot will:
- Validate required environment variables
- Connect to MongoDB (with automatic reconnection)
- Register all slash commands to the configured guild
- Begin polling Gmail every 30 seconds
- Start background jobs (auto-close, reminders, auto-unclaim)
- Launch an Express healthcheck server
Note: Changing .env requires restarting the bot. Slash commands are registered on startup; if commands don’t 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:
npm run start: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 for the full workflow, security checklist, and agent rules.
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.
Panel System
The panel system allows users to create tickets directly from Discord without sending an email.
- Deploy a panel:
/panel #support title:Need Help? description:Click below to open a ticket! - Users click the Open Ticket button
- A modal form appears asking for subject, description, and priority
- On submission, a ticket channel is created with all the same features as email tickets
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
│
├── 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
│
├── 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)
├── package.json
└── package-lock.json
Database Schema
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 |
API Integrations
Gmail API
- Authentication: OAuth2 with Client ID, Client Secret, and Refresh Token
- Polling:
users.messages.listfor unread messages in the primary inbox - Reading:
users.messages.getto fetch full message content - Sending:
users.messages.sendfor 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.
Documentation
Additional guides and reference docs live in docs/. See docs/README.md for the full index.
| Doc | Description |
|---|---|
| QUICKSTART | Get started in a few minutes: first response, panel, tags, priority |
| ENV_AND_SECURITY | Test env workflow, security checklist, agent rules |
| MONGODB_SETUP | MongoDB connection, schemas, and testing |
| PROJECT_STRUCTURE | File and directory layout |
| PROPOSAL | Roadmap and possible next steps |
| PHASE_FEATURES | Phased feature list and variables |
| FEATURES_SUMMARY · NEW_FEATURES | Feature overview and changelog |
| DISCORD_API_IMPROVEMENTS · DISCORD_API_VALIDATION | Discord API implementation notes |
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_IDandDISCORD_GUILD_IDare correct. - Restart the bot.
Gmail polling not working
- Ensure
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET, andREFRESH_TOKENare 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_URIis correct and the database is accessible. - Run
npm run test-mongodbfrom 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_IDpoints to a valid Discord category. - Ensure the bot has
Manage ChannelsandView Channelpermissions 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.
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 · GitHub |
| Discord Tickets | Open-source ticket bot; referenced for patterns and feature inspiration (panels, tags, transcripts). | Discord Tickets · GitHub |
| Node.js | JavaScript runtime used to run the bot. | Node.js |
| MongoDB | Database for tickets, transcripts, and persistence (via Mongoose). | MongoDB |
| Express | HTTP server for the healthcheck endpoint. | Express |
| Mongoose | MongoDB ODM used for schemas and connection handling. | Mongoose |
| Google APIs (googleapis) | Gmail API client for polling and sending email. | Google APIs Node.js |
License
ISC