From 371a464e290294a6195d577b310d0df303a6b48c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 23:58:54 +0000 Subject: [PATCH] bridge: Add RES for Arbiter Discord role automation Distills 2 rounds of Gemini consultation into actionable spec for Code. Architecture locked: module singleton, concurrent startup, ephemeral replies, edit-in-place embed, feature-flag welcome cutover. Addresses REQ-2026-04-13-arbiter-discord-roles.md Filed by Chronicler #86, resolved by Chronicler #87 --- .../RES-2026-04-13-arbiter-discord-roles.md | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/code-bridge/responses/RES-2026-04-13-arbiter-discord-roles.md diff --git a/docs/code-bridge/responses/RES-2026-04-13-arbiter-discord-roles.md b/docs/code-bridge/responses/RES-2026-04-13-arbiter-discord-roles.md new file mode 100644 index 0000000..979c6f6 --- /dev/null +++ b/docs/code-bridge/responses/RES-2026-04-13-arbiter-discord-roles.md @@ -0,0 +1,134 @@ +# Architectural Response: Arbiter Native Discord Role Management + +**Date:** 2026-04-13 +**RE:** REQ-2026-04-13-arbiter-discord-roles.md +**From:** Chronicler #87 +**Gemini Consultations:** 2 rounds (Round 1 + Round 2, April 13, 2026) +**Full docs:** `firefrost-operations-manual/docs/consultations/gemini-arbiter-discord-roles-round-1-2026-04-13.md` + `...-round-2-...md` + +--- + +## Decision Summary + +All architecture locked via two rounds of Gemini review. Build with confidence. + +--- + +## Architecture Decisions + +### 1. Process Structure +**Single process.** Keep the discord.js Client inside `arbiter-3` alongside Express. +- No PM2, no separate processes — overkill at current scale +- Re-evaluate only if we hit 50+ servers + +### 2. Startup Order +**Concurrent, non-blocking.** +``` +app.listen() → client.login() +``` +Do NOT await `client.ready` before starting Express. If a Pterodactyl webhook fires before the Gateway is ready, return `503 Service Unavailable` (or `202 Accepted`), log a warning, and let the caller retry. + +### 3. Shared Client — Module Singleton +Create `src/discord/client.js` — a module-level singleton exported and required wherever needed. + +```javascript +const { Client, GatewayIntentBits } = require('discord.js'); + +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildMessages, + ] +}); + +module.exports = client; +``` + +`src/routes/webhook.js` and any other route files simply `require('../discord/client')` and call methods directly. + +### 4. The get-roles Message +**Persistent embed, edit-in-place, 404 → repost fallback.** +- Store the message ID in the database +- On `/createserver` or `/delserver`: `PATCH` the existing message +- If Discord returns 404 (message deleted by accident): `POST` a new message and update the DB with the new ID + +### 5. Button Interaction Handling (3-Second Window) +Immediately acknowledge with type `6` (Deferred Update Message), then process async: + +```javascript +// Step 1: Acknowledge instantly +res.json({ type: 6 }); + +// Step 2: Process in background +// assign/remove role via REST +// Step 3: Send ephemeral confirmation +await rest.patch( + Routes.webhookMessage(appId, interactionToken, '@original'), + { body: { content: '✅ Role updated!', flags: 64 } } +); +``` + +### 6. Per-User Role State UX (Holly's Question — Answered) +**Ephemeral replies only. Do not change button styles.** + +Button styles on a standard channel message are global — changing a button green makes it green for EVERYONE. Keep buttons neutral (PRIMARY blue or SECONDARY gray) permanently. Use ephemeral replies (flag 64) for per-user confirmation: + +```json +{ "content": "✅ You now have the Farm Crossing 6 role!", "flags": 64 } +``` + +To toggle: read `interaction.member.roles` from the interaction payload. If role ID present → remove. If absent → add. + +**No role reconciliation needed at cutover** — Discord is the source of truth. Existing Carlbot-assigned roles carry over invisibly. + +### 7. Welcome Messages (guildMemberAdd) +Arbiter's REST-only interactions endpoint does NOT receive Gateway events. The discord.js client handles this. + +Use a **feature flag** on the `guildMemberAdd` listener for zero-overlap Carlbot cutover (see migration sequence below). + +### 8. Rate Limit Handling +**Native retry on 429.** No queue needed at current scale. discord.js handles this automatically. + +--- + +## Carlbot Cutover Sequence (Week of April 20) + +**Do NOT run Arbiter buttons and Carlbot reactions in parallel** — confuses the community. + +### Reaction Roles → Button Roles +1. Deploy Arbiter's new role logic to production (`systemctl restart arbiter-3`) +2. Disable Carlbot's Reaction Roles module in its dashboard +3. Delete the old Carlbot `#get-roles` message entirely +4. Trigger Arbiter's initial `POST` of the button message (via script or admin endpoint) + +### Welcome Message Cutover (Zero-Overlap) +1. Deploy Arbiter with `guildMemberAdd` listener **disabled** (feature flag = false) +2. Disable Carlbot's Welcome module in its dashboard +3. Enable the `guildMemberAdd` listener (feature flag = true) +4. `systemctl restart arbiter-3` + +If someone joins during the ~5s restart window they may miss a welcome. That is vastly preferable to double-welcoming. + +--- + +## Timeline + +- **Now through April 15:** Build and test in a hidden admin-only channel. Carlbot stays untouched. +- **Week of April 20:** Execute cutover sequences above. + +--- + +## Files to Create/Modify + +| File | Action | +|------|--------| +| `src/discord/client.js` | **NEW** — module singleton | +| `src/index.js` | Modify startup: concurrent `app.listen()` + `client.login()` | +| `src/routes/webhook.js` | Add get-roles message update on server create/delete | +| `src/routes/interactions.js` | **NEW** — handle button interactions, ephemeral replies | +| Database | Add `discord_roles_message_id` to config/settings table | + +--- + +*Architecture validated by Gemini (2 rounds). Build with confidence, Code.*