QUAL-006 store ticket.creatorId on creation; legacy split-pop returned the
message ID for discord-msg-* tickets, breaking transcript DM, close
log, and channel rename for context-menu-created tickets. Adds the
field to the Ticket schema and writes a one-shot backfill script
(scripts/backfill-creatorId.js, dry-run by default).
QUEUE-001 add enqueueOverwrite + enqueueTopic to services/channelQueue.js
(chain on renameChains alongside enqueueMove). Migrate handleAdd /
handleRemove / handleMove / handleTopic so permissionOverwrites,
setParent, and setTopic no longer race pending renames or sends.
handleMove now uses the existing enqueueMove. Initial overwrites in
handleTicketModal stay inline; channel doesn't exist yet so no race.
DISCORD-001 replace ephemeral: true with flags: MessageFlags.Ephemeral across
broccolini-discord.js, handlers/sharedHelpers.js, handlers/buttons.js,
handlers/commands.js. runDeferred opts now take { flags } directly.
SEC-003 /gmailpoll min interval is 30s. Drop the 5s/10s slash-command
choices and clamp Math.max(30000, ms) in handleGmailPoll for
defense in depth.
QUAL-001 upgrade silent .catch(() => {}) on the lastActivity updateOne in
handlers/messages.js to log via logError, so transient Mongo errors
surface in the debug channel instead of disappearing.
QUAL-002 drop await from logError/logWarn calls in services/staffThread.js
and services/pinMessage.js — fire-and-forget per CLAUDE.md hard rule.
QUAL-003 wrap stray setTimeouts (handleConfirmCloseRequest force-close timer,
runFinalClose channel-delete + overflow-cleanup, checkAutoClose
delete-after-email) in trackTimeout via lazy require so they clear
on shutdown.
Broccolini Bot
A Node.js Discord bot that bridges Gmail ↔ Discord for support ticketing, with MongoDB for state. Built for Indifferent Broccoli (game-server hosting).
- Inbound email → Discord ticket channel.
- Staff messages in that channel → Gmail reply (threaded).
- Discord-originated tickets (panel / context menu) live entirely in Discord.
For an architectural overview, see HOWITWORKS.md. For agent/contributor conventions, see CLAUDE.md.
Quick start
git clone <repo-url>
cd broccolini-bot
npm install
cp .env.example .env
# fill DISCORD_TOKEN, DISCORD_APPLICATION_ID, DISCORD_GUILD_ID, TICKET_CATEGORY_ID,
# ROLE_ID_TO_PING, MONGODB_URI, GOOGLE_CLIENT_ID/SECRET, REFRESH_TOKEN, MY_EMAIL
npm start
Need a Gmail refresh token? node get-refresh-token.js (redirect URI http://localhost:3000/oauth2callback). Probe Mongo with npm run test-mongodb.
Restart the bot after any .env change. Restart also re-registers slash commands.
Deploy (Docker)
docker compose up --build -d
docker logs broccolini --tail 50 -f
Host port 8892 → container 5000 (DISCORD_ONLY_PORT).
Configuration
All config is environment variables loaded by config.js into CONFIG. The full list — with descriptions and defaults — lives in .env.example. Highlights:
| Variable | Notes |
|---|---|
DISCORD_TOKEN / DISCORD_BOT_TOKEN |
Bot token. First non-empty after trim wins. |
DISCORD_APPLICATION_ID, DISCORD_GUILD_ID |
Required for slash command registration. |
TICKET_CATEGORY_ID |
Default category for email tickets. Validated at startup. |
DISCORD_TICKET_CATEGORY_ID |
Category for Discord panel/context tickets (falls back to TICKET_CATEGORY_ID). |
ROLE_ID_TO_PING |
Support role pinged on new tickets. |
MONGODB_URI |
Mongo connection string. |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / REFRESH_TOKEN / MY_EMAIL |
Gmail OAuth + canonical inbox address. |
RENAMER_BOT |
Optional secondary token used for channel renames. |
INTERNAL_API_SECRET / INTERNAL_API_PORT |
Enable the internal config API used by the settings UI. |
Slash commands
| Command | Purpose |
|---|---|
/escalate, /deescalate |
Move ticket between tier 2/3 categories. |
/add, /remove |
Add/remove user from current ticket channel. |
/transfer |
Hand the claim to another staff member. |
/move |
Reparent the channel to another category. |
/force-close, /cancel-close, /closetimer |
Force-close flow with cancellable countdown. |
/topic |
Set channel topic. |
/response |
Saved reply templates (send, create, edit, delete, list). |
/panel |
Post an "Open ticket" panel button (thread / category / both). |
/notifydm |
Toggle DM alerts when a customer replies in your claimed ticket. |
/signature |
Personal email signature (valediction, display name, tagline). |
/staffthread |
Toggle / configure staff-only threads on tickets. |
/pinmessages |
Auto-pin welcome / escalation messages. |
/gmailpoll |
Set the Gmail poll interval at runtime. |
/help |
In-bot summary. |
Plus context menus: Create Ticket From Message, View User Tickets.
Settings UI (optional)
settings-site/ is a separate Express app that talks to the bot's internal config API over the broccoli-net Docker network using INTERNAL_API_SECRET. It is not part of this bot's process. See settings-site/CLAUDE.md.
Troubleshooting
| Symptom | Check |
|---|---|
| Slash commands missing | Correct DISCORD_APPLICATION_ID + DISCORD_GUILD_ID; restart; Discord can take a minute to sync. |
| Gmail not ingesting | REFRESH_TOKEN valid? Auth failure halts polling — re-auth and restart. |
| Mongo errors at startup | MONGODB_URI reachable? npm run test-mongodb to confirm. |
| Channel rename "too quickly" | Discord limit is 2 renames/10 min per channel — the queue serializes; wait it out. |
| Modal/button no response | Bot online + intents enabled; check DEBUGGING_CHANNEL_ID / container logs. |
License
ISC