From 4f0c282f3f3c0a36f4762311b768d8e6b1862cd1 Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 19 Jul 2026 03:34:26 +0000 Subject: [PATCH] docs(website): add moderation appeals + Discord reversal (Phase 6c/6d) --- website/MODERATION_APPEALS.md | 128 ++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 website/MODERATION_APPEALS.md diff --git a/website/MODERATION_APPEALS.md b/website/MODERATION_APPEALS.md new file mode 100644 index 0000000..318f022 --- /dev/null +++ b/website/MODERATION_APPEALS.md @@ -0,0 +1,128 @@ +# Runic Gateway Website — Moderation Appeals (Phase 6c/6d) + +> Website feature branch: **`feature/moderation-appeals`**. Builds on the moderation +> dashboard (Phase 6a/6b) and the Discord bot's `mod_actions` log. Companion to +> [website-README.md](website-README.md) (overview) and +> [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (base API contract). + +## 1. Overview + +A player whose linked Discord identity was **banned** or **muted** — an action +recorded in the bot's `mod_actions` log — can open an **appeal** from the player +portal and track its status. Staff (**admin** or **moderator** role) work the +appeal from an **appeals queue** in the admin moderation section: claim it, then +resolve it **approved** or **denied** with a written staff response. + +When staff **approve** a ban/mute appeal, the website makes a best-effort call to +the Discord bot's internal API to actually lift the ban / clear the timeout in +Discord, and the bot posts a mod-log embed ("Appeal approved"). This is +**best-effort**: if the bot is unreachable the appeal still resolves as approved, +the reversal is recorded as failed, and staff can reverse the sanction manually in +Discord. + +Only **ban** and **mute** actions are appealable — the sanctions that have an +ongoing effect. Warnings/kicks and similar one-shot actions are not. + +## 2. Ownership & eligibility + +- **`appeals` is a server-owned table** — only the website reads/writes it. It + references the bot-owned `mod_actions` log by a plain id column + (`mod_action_id`); there is **no hard cross-owner foreign key** between the two + databases, so the reference is validated in application code (same pattern as + the rest of the uo-link / bot integration, where the two services never share a + live FK). +- **Eligibility** — the appellant must be a **logged-in player** whose linked + Discord identity (`user_identities`, `provider = 'discord'`) matches the + `mod_actions` row's target. A player cannot open an appeal for someone else's + action, and an unlinked player has nothing eligible to appeal. +- **One active appeal per action** — only one `pending` / `under_review` appeal is + allowed for a given `mod_action_id` at a time; a second attempt while one is + already open is rejected. + +## 3. Appeal lifecycle + +``` +pending ──▶ under_review ──▶ approved + └─▶ denied + +pending ──▶ withdrawn +under_review ──▶ withdrawn +``` + +- **`pending`** — submitted by the player, not yet claimed. +- **`under_review`** — claimed by a staffer (the claiming admin/moderator is + stamped on the row). +- **`approved`** / **`denied`** — resolved by staff with an optional + `staff_response`. Approving a ban/mute appeal triggers the Phase 6d reversal + (§5). +- **`withdrawn`** — the player pulled the appeal back before it was resolved. + +`reversal_status` (only meaningful on an approved ban/mute appeal) is one of +`none` (not attempted / not applicable), `done`, or `failed`. No new +`mod_actions` row is written for a reversal — it modifies the *original* action's +standing rather than logging a new one. + +## 4. API — player (role: `player`) + +Base `/api/v1/player/appeals`. + +| Method | Path | Purpose | +|---|---|---| +| GET | `/player/appeals` | The caller's own appeals. | +| GET | `/player/appeals/eligible` | The caller's ban/mute actions with no active appeal (empty if they have no linked Discord identity). | +| POST | `/player/appeals` | Open an appeal — `{ mod_action_id, submitted_text }`. `403` if the action isn't the caller's, `400` if the action isn't a ban/mute, `409` if one is already open for it. | +| POST | `/player/appeals/:id/withdraw` | Withdraw an appeal that hasn't been resolved yet. | + +## 5. API — staff (role: `admin` or `moderator`) + +Base `/api/v1/admin/moderation/appeals`, alongside the existing moderation +section. + +| Method | Path | Purpose | +|---|---|---| +| GET | `/admin/moderation/appeals?status=&limit=&offset=` | The queue. Defaults to `pending` + `under_review`; pass `status=all` or a specific status to filter. | +| GET | `/admin/moderation/appeals/:id` | One appeal. | +| POST | `/admin/moderation/appeals/:id/claim` | `pending` → `under_review`, stamping the claiming staffer. | +| POST | `/admin/moderation/appeals/:id/resolve` | `{ status: 'approved' \| 'denied', staff_response? }`. On an approved ban/mute, triggers the Discord reversal (§6). | +| GET | `/admin/moderation/user/:discordId/appeals` | A user's appeals — shown as a tab on the per-user moderation history page. | + +`resolve` returns a `reversal` object describing what happened: + +```jsonc +{ + "reversal": { + "attempted": true, + "ok": true, + "reversal_status": "done", // "none" | "done" | "failed" + "bot_status": 200, + "error": null + } +} +``` + +## 6. Auto-reversal (Phase 6d) + +On `resolve` with `status: 'approved'` against a ban/mute appeal, the website +calls the Discord bot's internal API: + +``` +POST /internal/mod-reverse +``` + +— gated by the same shared-secret scheme as the existing `/internal/announce` +call. The bot lifts the ban / clears the timeout for the target and posts an +"Appeal approved" embed to its mod log. + +The call is **best-effort**: the appeal resolution itself always completes +(the appeal is marked `approved` and the staff response is saved) regardless of +whether the bot answers. If the bot is down or the call otherwise fails, +`reversal_status` is recorded as `failed` and staff are expected to reverse the +sanction by hand in Discord; the `reversal` object in the `resolve` response +surfaces `ok: false` and an `error` so the UI can flag it. Denied appeals never +attempt a reversal. + +--- + +See [website-README.md](website-README.md) for the moderation dashboard's place +in the wider site, and [BACKEND_DESIGN.md](BACKEND_DESIGN.md) for the base API +conventions (auth, error shapes, response codes) these endpoints follow.