guild.channels.create in findOrCreateTicketChannel previously had no
permissionOverwrites — newly created email-ticket channels inherited
whatever the parent category granted. If the category ever had @everyone
View Channel allowed (or undefined → default-allow), every server member
could read every email ticket.
Add explicit overrides on creation:
- @everyone (guild.id): deny ViewChannel
- ROLE_ID_TO_PING: allow ViewChannel + SendMessages + ReadMessageHistory
(gated on ROLE_ID_TO_PING being set — empty string skips the entry
rather than creating a malformed overwrite).
Email tickets have no Discord creator (the customer reaches the bot via
email, not as a guild member) so the only "allow" entry is the staff
role. Modal-created and context-menu-created tickets already set
creator+role overrides on creation; this change brings the third path
into line.
Pairs with category-level Discord config: TICKET_CATEGORY_ID and the
ESCALATED2/3 categories should still deny @everyone and allow
ROLE_ID_TO_PING at the category level for defense in depth.
Split the original 309-line poll() into single-responsibility helpers and a
thin orchestrator. No behavior change — every Gmail API call, Discord call,
DB write, and log line stays in the same order with the same arguments.
Helpers extracted (module-private):
- locateGuild(client) — DISCORD_GUILD_ID lookup with fallback warning.
- parseGmailMessage(email) — header parsing, body decode, dual cleanup
(firstBody for new-ticket message, followupBody for thread append).
- findOrCreateTicketChannel(guild, parsed, number) — category resolution
+ channel.create with the existing two-stage error handling.
- linkPreviousTranscripts(ticketChan, threadId, client) — best-effort
prior-transcript link on reopen.
- markGmailMessageRead(gmail, msgRef) — wraps the batchModify call used
in five places across the original.
- oauthSuspendIfPermanent(err, client) — invalid_grant/invalid_client
classify, suspend polling, clear interval, DM admin once. Returns bool.
poll() is now the orchestrator: list → locate guild → for each message,
parse → look up existing ticket → branch (append-followup vs new-ticket
flow) → mark read. The new-ticket branch stays inline in poll() per the
"keep poll() as orchestration" intent.
- Rename CONFIG.TRANSCRIPT_CHAN -> CONFIG.TRANSCRIPT_CHANNEL_ID and
CONFIG.LOG_CHAN -> CONFIG.LOGGING_CHANNEL_ID across 9 callsites so
CONFIG keys match their .env names — no more "grep .env, find nothing"
for new readers
- Replace handlers/commands.js#hasStaffRole with utils.js#isStaff
(was a verbatim copy)
- Delete utils.js#enforceEmbedLimit and its 2 callsites; both inputs are
bounded well under the 6000-char Discord embed cap, so the trim was
defensive code that never fired
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove no-op log stubs (logGmail, logAutomation, logSecurity, logSystem)
and ~17 callsites; dead counters in tickets.js and gmail-poll.js go too
- Dedup three near-identical Gmail send paths into sendThreadedEmail helper
- Drop dead Mongoose fields: broccoliniTicketId, lastSyncedBroccoliniArticleId,
renameCount, renameWindowStart, reminderSent, staffChannelId,
unclaimedRemindersSent, lastMessageAuthorIsStaff
- Drop dead config fields and their .env.example entries
- Inline api/botClient.js (3-line wrapper, 2 callers)
- Trim unused exports across utils.js, tickets.js, configSchema.js, debugLog.js
- Fix handlers/messages.js to use isStaff() — old partial check ignored
ADDITIONAL_STAFF_ROLES, so those members were treated as customers
- Drop unused deps p-queue + dotenv-expand; move mongodb to devDependencies
Net: -583 LOC source + -57 LOC lockfile. All 23 modules load clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gmail-poll.js was hand-rolling its own Close+Claim-only action row.
Replaced with getTicketActionRow({ escalationTier: 0 }) — same helper
panel-created tickets use. Email and Discord tickets now show identical
buttons on creation: Close, Claim, Escalate.