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:
@@ -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.*
|
||||
Reference in New Issue
Block a user