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
This commit is contained in:
Claude
2026-04-13 23:58:54 +00:00
parent e21a348b3b
commit 371a464e29

View File

@@ -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.*