indifferentketchup e3b3b8d48c refactor handleButton into a dispatch table
Each customId now maps to a named handler in one of two tables:
FREE_BUTTON_HANDLERS (open-ticket panel, tag-delete cancel — no ticket
lookup) or TICKET_BUTTON_HANDLERS (anything fired inside a ticket channel
— the dispatcher does the lookup once before delegating). The dynamic
`confirm_delete_tag::*` id is matched by prefix.

To find a button's logic, search handle<Name>Button or handleTagDelete*.

Other cleanups in the same pass:
- Move findTicketForChannel and runDeferred from handlers/commands.js to
  the new handlers/sharedHelpers.js so both files share one source of
  truth. runDeferred now also calls logError(verb, ...) — was logged ad
  hoc in buttons.js, missing in commands.js. Strictly additive.
- Hoist three inline `require('../services/...')` calls (staffThread,
  pinMessage, debugLog) to top imports.
- Collapse escalate_to_tier2 and escalate_to_tier3 into one
  handleEscalateButton(interaction, ticket) that derives the tier from
  customId. Same for confirm_close / confirm_close_with_email /
  confirm_close_no_email — one handleConfirmCloseRequest deriving
  sendEmail from customId.
- Decompose the 156-line handleConfirmClose into runFinalClose +
  buildTranscriptText + formatDateForTranscript + renderTranscriptHeader
  + dmTranscriptToCreator + postCloseLogEntry. Each piece is testable in
  isolation.
- Decompose handleClaim into applyClaim + applyUnclaim.
- Extract buildOpenTicketModal() and postTicketWelcomeEmbeds() so the
  ticket-creation modal flow is readable top-to-bottom.

No behavior change. handleButton + handleTicketModal exports preserved;
24/24 modules load clean (sharedHelpers.js is the new one).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:57:43 +00:00
2026-04-21 17:24:03 +00:00
2026-04-18 11:10:41 +00:00
2026-04-21 14:32:34 +00:00
2026-03-28 22:40:43 +00:00
2026-04-21 15:30:40 +00:00
2026-05-02 02:15:18 +00:00
2026-05-02 02:15:18 +00:00

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

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