Files

248 lines
15 KiB
Markdown
Raw Permalink Normal View History

# Community-Admin Moderation Plan
**Goal (Tyler, 2026-07-01):** let community admins moderate their own communities.
Co-authored by Eva + Wren. Grounded against `buzz-1321-review @ 86d6388` and
`RESEARCH/NOSTR_CONTENT_REPORTING_MODERATION.md`. Re-verified against fresh
`REPOS/buzz-moderation-ref` @ main `1f5ba5b` (2026-07-07).
## 0. DECISIONS LOCKED (Tyler, 2026-07-07, #buzz-moderation thread e2a91af6)
All open calls below are now decided. The UX was refined by Eva + Wren to a
co-signed 9/10+ (Wren's line-cited corner-check: event `295db04e`; final
presentation: event `4273365f`).
1. **Roles: owner + admin only. No Moderator tier for now.** (Tyler, event `559f838f`.)
2. **Reporter identities visible to mods** in the queue (never to the reported author).
3. **Relay-key resolution DM** (Tyler, event `34de7dac`): on report resolution the
relay key authors a regular nostr message stored in the DB. Container (Wren-verified):
relay/moderation key creates/reuses a two-party DM channel `{mod key, user}` via the
participant-hash-idempotent DM model (`buzz-db/src/dm.rs`), emits 39000 discovery
(`hidden`, `t=dm`, `p` tags), inserts relay-signed kind-9 with `h=<dm_channel_id>`.
One DM thread per user per community; relay publishes kind-0 "{Community} Moderation"
identity. `moderation_reports` persists `reporter_pubkey`. Same primitive carries
actioned-author notices and timeout/ban issuance. Non-replyable v1; replies (v2)
route to mod queue as appeals.
4. **Ban/timeout gate at the auth seam** (Tyler, event `34de7dac`): evaluated after
NIP-42 verify, before pubkey allowlist / `enforce_relay_membership`
(`handlers/auth.rs` — gate goes immediately after ~L91). Banned ⇒
`OK false "blocked: you are banned from this community"` + immediate WS close, zero
further processing (`blocked:` is the NIP-01 standardized prefix; there is no
`banned:` prefix; AUTH must be answered with `OK` per NIP-42). Live ban kill:
cluster-wide disconnect-by-pubkey over Redis pub-sub; `CLOSED "blocked: ..."` per
active sub, then socket close (needs a close-by-pubkey API on `ConnectionManager`).
NIP-OA: gate checks the authenticating pubkey always + owner pubkey when present —
owner ban cascades to agents; agent ban is agent-only; audit records matched
principal, client never learns which. Timeout is a write-block, not a connection
block: `OK false "restricted: you are timed out until <ts>"`, desktop disables
composer with countdown chip. No silent write-drops.
## 1. Where this sits — two moderation layers, not one
Discord's published model (safety/our-approach-to-content-moderation) is explicitly
**two-layer**, and Buzz should mirror it:
| Layer | Discord | Buzz | Owner | This plan |
|-------|---------|------|-------|-----------|
| **Platform safety** | Trust & Safety team: CSAM image-hashing → NCMEC, ML network detection, human investigations | Relay operator: sha-match in S3, NIP-86 ban/takedown, NIP-62 vanish | Block / relay operator | **Adjacent** — Fizz's `MODERATION_SAFETY_SKETCH.md` owns it. We cite, don't duplicate. |
| **Community moderation** | Server owners + volunteer mods: AutoMod keyword/spam filters, Warning System | Community admins/mods: NIP-29 admin actions, report queue, in-community bans | Community admins | **THIS PLAN.** |
The severe-safety class (CSAM) is never delegated to community admins — it's a
platform-level hard-removal + legal-report path. Community moderation is the
subjective, per-community rule enforcement layer on top.
## 2. What PR #1321 already gives us (the foundation)
#1321 is **not** a moderation PR — it's the multi-tenant substrate that makes
per-community moderation *safe*:
- Every scoped row carries a **server-resolved `community_id`** (never caller-supplied).
- `TenantContext` is minted only on the host-resolution path and threaded through
every scoped DB read + Redis publish — a proven cross-community isolation fence.
**Implication:** community-admin moderation authority is naturally tenant-scoped, but
only if target resolution also stays inside the fence. An admin of community A can
never reach community B's content when the query lists/deletes/bans with
`for_community(A)`. The dangerous edge is a new moderation signal whose *target* is a
bare event id, address, pubkey, or blob hash. Therefore every new moderation table gets
`community_id`, every new action takes `&TenantContext`, and every report/label target
is resolved under `tenant.community()` before it enters a queue or action row. No new
isolation primitive needed; reuse #1321's — but do not add any global target lookup in
the report/label path.
## 3. What already exists (build-on, don't rebuild)
From `crates/buzz-relay/src/handlers/side_effects.rs` + `buzz-core`:
- **NIP-29 admin kinds**, authorized in `validate_admin_event`:
- `9000` put-user (add / assign role), `9001` remove-user (kick),
`9002` edit-metadata, `9005` delete-event, `9008` delete-group,
`9021`/`9022` join/leave request.
- **Role model** (`buzz-core/src/channel.rs`): `Owner > Admin > Member > Guest`, plus
`Bot`. `is_elevated()` = Owner|Admin gates elevated-role grants. Agent-owner
delegation lets the owning human act.
- **39000-series** group-state mirrors (metadata/admins/members/roles).
- **NIP-51 mute list** (kind:10000) constant exists — user-level, client-advisory.
## 4. The gaps (what this plan adds)
### Gap A — No distinct Moderator role
Today the only elevated tier is `Admin` (can manage members + settings). Discord's
model leans on a **volunteer-moderator** tier that can act on content/users but
*cannot* reconfigure the community or manage other mods.
**Recommendation after corner-check:** ship v1 **Admin-only for community-wide
moderation**, with an explicit capability helper, and defer a distinct `Moderator`
role until product needs delegated volunteer moderation.
Why not add `Moderator` in v1:
- Community-wide authority lives in the tenant-scoped `relay_members` table, not in
channel `MemberRole`. Adding `Moderator` to `MemberRole` would migrate channel-local
DB enum/API/UI/projection surfaces without actually creating tenant-wide authority.
- Adding `moderator` to `relay_members.role` is possible, but still touches admin
commands, membership announcements, UI, downgrade paths, and tests. That is extra
surface before we have the basic report/delete/ban loop working.
- NIP-29 interop is fine either way because roles are relay-defined/arbitrary, but
clients that only reconstruct channel `39001` admins will not understand a
community-wide moderator unless the relay explains/enforces it. A relay-signed
tombstone is clearer than silently adding every community admin/mod to every group.
- A capability helper now makes v2 cheap: later `moderator` is a role-to-capability
mapping, not an authorization rewrite.
V1 capability grid:
| Capability | Community owner | Community admin | Channel owner/admin | Member |
|------------|:---:|:---:|:---:|:---:|
| Delete any message in community (9005) | ✅ | ✅ | channel only | own only |
| Remove/kick user (9001) | ✅ | ✅ | channel only | self |
| Ban user from community | ✅ | ✅ | ❌ | ❌ |
| Timeout/mute user in-community | ✅ | ✅ | ❌ | ❌ |
| Resolve reports (queue actions) | ✅ | ✅ | optional channel-view | ❌ |
| Assign/revoke community Admin | ✅ | ❌ | ❌ | ❌ |
| Edit community-level settings | ✅ | ✅ | ❌ | ❌ |
| Delete community / hard-delete channel (9008) | ✅ | ❌ | channel owner only where already allowed | ❌ |
Open call for Tyler: **is a distinct Moderator tier required in v1?** My reviewed lean
is **no**: ship Admin-only, but structure authorization around capabilities so adding
`moderator` later is a narrow extension. If Tyler wants Discord-like volunteer mods on
day one, add `moderator` first to `relay_members.role` (tenant-level), advertise it in
`39003`, and only then decide whether channel `MemberRole` also needs a `Moderator`.
### Gap B — No NIP-56 reports (the report button)
**The single biggest missing primitive.** kind:1984 is referenced only as a CLI
filter example; there is no ingest, queue, or UI. Without it, mods have nothing to
act *on* — they can only delete what they personally see.
Pipeline (all tenant-scoped via #1321's fence; target resolution is the sharp edge):
```
client "Report" → kind:1984 event (p + e/x tags, report-type, free-text)
│ ingest: validate_report_event (new, in side_effects.rs)
▼
moderation_reports table (community_id, target_event/pubkey/blob,
reporter, type, reason, created_at, status=Open)
│ aggregate by target; weight trusted reporters (mods > members)
▼
per-community moderation queue (buzz-cli / desktop admin surface)
│ mod resolves: dismiss | delete (9005) | remove (9001) |
│ ban | mute | escalate-to-platform
▼
moderation_actions table (audit) + relay-signed tombstone (see Gap D)
```
Target-resolution rules to keep the tenant fence closed:
- `e` target: look up the event only with `tenant.community()`; infer `channel_id`
from that row. If missing in this tenant, reject or store as an unresolved report —
never search other tenants by event id.
- `x` blob target: resolve through tenant-scoped media references, e.g.
`(community_id, sha256)` or `(community_id, event_id, sha256)`. A bare SHA-256 can
be shared across tenants and must not grant cross-tenant visibility/action.
- `p`-only target: treat as a community-local report about that pubkey in the current
tenant. It cannot imply a platform/global ban.
- NIP-32 labels (`1985`) in v2 follow the same rule: labels are advisory inputs until
their target is resolved under the current `TenantContext`.
Key policy (from NIP-56 + the RESEARCH doc): **relays should not auto-moderate on
random-user reports** — they're gameable. Reports are a triage inbox, not an
auto-takedown trigger. Exception: trusted-admin reports of CSAM/illegal class can
fast-path to platform quarantine (Fizz's layer), not to community-admin discretion.
### Gap C — No persistent ban / in-community mute
`9001` remove-user is a kick — nothing stops re-join on an open channel. Add:
- `community_bans` table (`community_id`, pubkey, actor, reason, expires_at NULL=perm).
- Enforce at join (`9021`/`9000` self-add) and at event ingest for the community.
- **Mute/timeout** = time-boxed write-block (can read, cannot post) — a softer tool
than ban, matches Discord's timeout. Same table, `muted_until` column.
### Gap D — No "Warning System" (why-was-this-removed)
Discord tells users *why* content was actioned. Buzz already has the right primitive:
`handle_delete_event_side_effect` soft-deletes the target, then emits a relay-signed
kind `40099` system message with `type: "message_deleted"`, `actor`, and
`target_event_id`. Extend that rather than inventing a new 48000-range event for v1.
Recommended shape:
- **Public/in-context tombstone:** relay-signed kind `40099`, rendered as "Removed by a
community moderator" without exposing removed content. Add safe fields:
`action_id`, `target_event_id`, `target_author`, `reason_code`, optional sanitized
`public_reason`, and maybe `actor` if moderator identity is intentionally visible.
- **Private author notice:** separate p-gated/DM notification to the actioned user with
the reason, community rule, and appeal/restore path. This is the closer Discord
Warning System analog.
- **Internal action/audit row:** full moderator, report ids, reporter identities,
evidence, and unsafe details stay admin-only.
- **NIP-32 label:** useful in v2 as an advisory/scanner signal, not the authoritative
enforcement record. Labels can be consumed by clients; tombstones explain relay
actions.
## 5. Standards to adopt vs. skip (from RESEARCH doc, cited to nostr-protocol/nips)
| NIP | Adopt? | Role in this plan |
|-----|--------|-------------------|
| **NIP-29** groups (9000–9022) | **Yes — already have it.** Extend authorization for community Admin + ban. | Relay-enforced community roles + admin actions. |
| **NIP-56** reports (1984) | **Yes — new.** Gap B. | User report button; interop-standard so other clients work. |
| **NIP-32** labels (1985) | **v2.** | Distributed/automated moderation output; feeds queue as a signal. Client-advisory blur/hide. |
| **NIP-51** mute (10000) | **Yes (exists).** | *User-level* self-mute — client-advisory, orthogonal to community moderation. Keep distinct from Gap C's community mute. |
| **NIP-86** relay-admin API | **Platform layer** (Fizz). | Operator ban/takedown at event/pubkey/IP grain. Not community-admin scope. |
| **NIP-62** vanish (kind:62) | **Platform layer.** | GDPR right-to-erasure; MUST fully delete + block rebroadcast. Legal, not community. |
| **NIP-72** moderated communities | **Skip.** | Deprecated upstream in favor of NIP-29 (README marks it unrecommended). |
| **NIP-09** deletion (kind:5) | Keep (exists). | Author self-delete only; advisory. Not a moderation tool for others' content. |
## 6. Phased rollout
**Phase 1 — community-admin MVP (this plan's core):**
1. Add a community moderation capability helper and extend `validate_admin_event` for
community `owner|admin` authority (Admin-only v1 unless Tyler explicitly wants
`moderator`).
2. NIP-56 kind:1984 ingest → tenant-scoped `moderation_reports`, with target
resolution under `TenantContext`.
3. `moderation_actions` audit table + extended relay-signed kind `40099` tombstone for removals.
4. `community_bans` (+ mute/timeout column); enforce at join + ingest.
5. `buzz-cli moderation` queue commands (list/resolve/ban/mute) for admins.
**Phase 2 — UX + trust weighting:**
6. Desktop/mobile Report button + moderation queue surface.
7. Trusted-reporter weighting; report aggregation by target.
8. Warning-System notifications to actioned users.
**Phase 3 — distributed signals:**
9. NIP-32 labels (1985) as a queue input + client-advisory hide.
10. Cross-community shared blocklists (opt-in), platform-layer coordination.
## 7. Open calls for Tyler — ALL DECIDED 2026-07-07, see §0
- **Moderator role in v1, or Admin-only first?** (§4 Gap A — reviewed lean: Admin-only v1 with capability seams.)
- **Ban grain:** per-community only, or should a community ban be able to request a
platform-level ban for severe cases? (Escalation path to Fizz's layer.)
- **Report visibility:** are reports mod-only, or does a reporter see resolution?
- **Warning System shape:** reviewed lean is relay-signed kind `40099` tombstone + private author notice; NIP-32 label is v2/advisory (§4 Gap D).
## Sources
- `buzz-1321-review @ 86d6388`: `crates/buzz-core/src/kind.rs`,
`crates/buzz-core/src/channel.rs`, `crates/buzz-relay/src/handlers/side_effects.rs`.
- PR #1321 description (multi-tenant `community_id` / `TenantContext`).
- `RESEARCH/NOSTR_CONTENT_REPORTING_MODERATION.md` (cited to nostr-protocol/nips).
- Discord: safety/our-approach-to-content-moderation (2024-03-15).
- Fizz's `docs/MODERATION_SAFETY_SKETCH.md` (platform media-safety layer, adjacent).
- Wren corner-check: `PLANS/COMMUNITY_MODERATION_CORNER_CHECK_WREN.md`.