Commit Graph

25 Commits

Author SHA1 Message Date
61e8ea32e1 Post inbound email attachments to the ticket channel
fetchMessageAttachments downloads a Gmail message's downloadable parts as
discord.js file descriptors, skipping parts over Discord's 25 MB ceiling and
capping at 10 files per message. Nameless inline parts (CID screenshots) get a
synthesized name; nameless text/* parts (the email body Gmail serves as an
attachmentId-backed part) are skipped. The poll posts these on both new tickets
and follow-ups, naming any skipped parts so staff know to check Gmail.
2026-06-05 03:08:21 +00:00
e77be9a3e4 Add per-staff metrics: StaffAction event log + /stats command
Event-sourced tracking of staff/ticket lifecycle actions, plus a /stats
command. Foundation for a future tickets-website analytics dashboard.

Data:
- StaffAction model (event log) + Ticket.game / Ticket.closedAt
- STATS_ADMIN_IDS config (who may view others' stats)

Recording (fire-and-forget, idempotent on real state transitions):
- claim, response (channel reply + /response send), escalate, de-escalate,
  transfer, close (4 sites), reopen — each denormalizes ticketType, tier,
  priority, game, requester (senderEmail / creatorId), guildId
- close events carry closerType / resolverId (claimer credit) / wasClaimed;
  transfer carries fromId / toId; reopen stamps resolverId
- conditional close transition helper (atomic open->closed + closedAt) shared
  by all four close paths

Query + command:
- pure period parser (presets + free-text) and stats shaper (per-metric keys)
- command-aware autocomplete dispatch
- /stats: period (autocomplete) + member (admin-gated) + source (all/email/
  discord), ManageMessages + staff-role gated, ephemeral, tier-labeled embed

288+ unit tests; timing/busiest-times data is collected but displayed later.
2026-06-05 02:47:43 +00:00
6bae3e79b1 Gmail folder auto-advance + /forward command
- Reply-cycle auto-advance: staff reply files the thread to "Awaiting Reply",
  a customer response files it to "Needs Response" (new GMAIL_LABEL_AWAITING_REPLY
  / GMAIL_LABEL_NEEDS_RESPONSE labels + autoAdvanceFolder, which only moves
  threads still in the auto-cycle and leaves hand-filed folders alone)
- /forward: forward a ticket's email to another address (handlers/commands/forward.js
  + forward composition in services/gmail.js)
- Tests for the auto-advance cycle; label fixtures updated for the new labels
2026-06-05 02:46:50 +00:00
2ccdbf72aa Email ticketing fixes, comms polish, and .env cleanup
Inbound:
- Gmail poll query is:unread in:inbox (was category:primary, which matched
  nothing on a no-tabs Workspace inbox)

Outbound email:
- Closed/escalation auto-emails editable via TICKET_CLOSE_MESSAGE and new
  TICKET_ESCALATION_EMAIL_MESSAGE; drop the staff signature from closing emails
- Replies quote the customer's latest message (gmail_quote markup so clients
  collapse it), embed custom emoji inline via CID attachment, and strip Discord
  role mentions
- Tagline spacing fix in the company signature

Discord side:
- Suppress all mentions in log + transcript posts (no more pinging on close)
- Drop the staff-role ping from new-ticket and follow-up notifications
- Ticket channels inherit category permissions instead of setting per-channel
  overwrites (removes the Manage Roles requirement)

Gmail folders:
- Folder/label routing (gmailLabels.js) with /folder; close files to Complete

Config:
- Remove ~56 stale .env keys for long-removed features; refresh stale copy

Docs:
- Design specs for folder routing, email-flow toggle, and per-staff metrics
2026-06-04 22:05:20 +00:00
2fab3b97bf Remove dead/stale code, dedup close+escalation paths
Dead/stale removals (grep-confirmed no consumers):
- config: drop 9 unread CONFIG keys (ROLE_TO_PING_ID, SIGNATURE,
  REMINDER_*, RENAME_LOG_CHANNEL_ID, SETTINGS_*); remove their
  ALLOWED_CONFIG_KEYS entries and the orphaned settings-site UI fields
- configSchema: delete unreachable json/string_or_json validators
- models: drop unused ticketTag field
- gmail-poll: remove unused isPollSuspended export
- utils: remove dead htmlToTextWithBlocks/decodeHtmlEntities/BLOCK_TAG_REGEX
- internalApi: remove router._allowedKeys (test it served is gone)
- discord client: drop unused GuildPresences privileged intent
- broccolini-discord: remove dormant /api 503 gate (no /api routes)

Fixes:
- context-menu ticket create now uses makeTicketName('unclaimed', ...)
  instead of the contract-violating ticket-<n> name
- drop write-only pending.userId from both close paths

Dedup / simplify:
- new services/transcript.js shares the transcript text/date/header
  builders between the button and force-close paths (had drifted)
- resolveEscalationCategoryId() replaces 3 copies of the category logic
- ticketChannelOverwrites() shares the create-permission array between
  the two interactive ticket-create paths
- finalizeBody() shares the email-cleanup tail in parseGmailMessage
- getTicketActionRow drops its never-passed options arg;
  sendTicketNotificationEmail drops its always-null subjectLine arg
- hoist invariant guild lookup out of the auto-close/unclaim loops
- drop redundant lastActivity write (and now-dead updateTicketActivity)
- /help lists all current commands and the right-click apps
2026-06-02 19:59:14 +00:00
76279b703a gmail-poll: lock email-ticket channels to staff role only
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.
2026-05-19 18:26:12 +00:00
d0cf8fd915 audit week 2 [VIBE-001]: decompose gmail-poll.js poll()
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.
2026-05-08 20:23:30 +00:00
83b6b4ae0c simplify: rename CONFIG channels, dedup hasStaffRole, drop enforceEmbedLimit
- 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>
2026-05-07 18:45:18 +00:00
840b6bfcf8 simplify: prune dead code, dedup gmail send, drop neutered log stubs
- 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>
2026-05-07 18:37:14 +00:00
410f8b043e fix: add Escalate button to email ticket welcome embed
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.
2026-04-21 19:50:11 +00:00
f3ee27ed7a more mvp strip 2026-04-21 17:24:03 +00:00
636348d824 strip: remove pattern/surge/chat alert monitoring + unused commands
- delete services/{patternChecker,patternStore,surgeChecker,chatAlertChecker,staffNotifications,staffChannel,notificationRegistry,notificationEnabled,staffPresence}.js
- remove /notification, /staffnotification, /tag, /priority
- /escalate: drop action param, always unclaim
- purge PATTERN_*, SURGE_*, CHAT_ALERT_*, STAFF_* env vars from config + .env.example
- drop StaffNotification model
- ~2500 LOC removed
- settings-site /internal/notifications/* endpoints gone (UI will 404 until trimmed)
2026-04-21 15:57:18 +00:00
74d7f49c8d test 2026-04-21 14:32:34 +00:00
33b1f276c6 audit 2026-04-20 18:05:36 +00:00
21618efbad security hardening 2026-04-18 11:10:41 +00:00
indifferentketchup
a1cd67fd73 fix 2026-04-10 08:57:55 -05:00
indifferentketchup
22897475dc queue 2026-04-09 09:50:00 -05:00
indifferentketchup
7fff9192b4 queue 2026-04-09 09:49:19 -05:00
indifferentketchup
4d53ef179f gmail poll 2026-04-08 08:24:13 -05:00
indifferentketchup
3bf799c5e7 change gmail poll 2026-04-07 10:50:44 -05:00
indifferentketchup
69c247ed1b huge changes 2026-04-07 01:43:06 -05:00
indifferentketchup
c5d7539677 staff notifications 2026-04-06 23:53:32 -05:00
indifferentketchup
1496a96274 Dynamic overflow categories 2026-03-28 20:55:36 -05:00
samkintop
29a13768f7 Sync broccolini-bot: rename from zammad, docs in docs/, security gitignore, remove zammad deps
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 02:56:00 -06:00
root
519788c633 Initial commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 08:22:19 -06:00