docs(safety): record USER_SAFETY step 3, Module-Rust's chat lines

Rewrites §4.2 to match what was built: server/safety.js, the in-query feed
filter, rust_events.hidden_at / hidden_by, the staff hide and unhide routes
on the line's own menu, and what happens when core's safety reads fail.
Records §7 step 3 as built and walked (28 API rows and a browser pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-30 09:04:29 -05:00
parent f8d46ae956
commit 90ba337047

View File

@@ -1,6 +1,7 @@
# Report, block and hide — user safety on the website and in the app
**Status:** plan of record, 2026-10-29. Step 2 (core) built 2026-10-30 (§7); the rest not yet.
**Status:** plan of record, 2026-10-29. Steps 2 (core) and 3 (Module-Rust, Integration Kit) built
2026-10-30 (§7); the rest not yet.
Each phase stops for the org lead before the next one starts. Built after account deletion
([`ACCOUNT_DELETION.md`](ACCOUNT_DELETION.md)), which is why it is `MODULE_API` 1.21.0. §4 was
amended by S11-S14 (§5) when the core was built.
@@ -111,20 +112,39 @@ staff's.
### 4.2 Module-Rust
All of it is in `server/safety.js`, beside the events route that uses it.
- `registerReportTargets([{ id: 'rust.chat', … }])` with `snapshot` (the speaker's name and Steam
ID, the line, server, time, and the linked account if there is one; null when the viewer may not
see presence on that server) and `hide`/`unhide`. `registerGameIdentities([{ kind: 'steam', …
}])` with `ownersOf` reading `rust_account_links`.
- **Hide for everyone** sets a flag on the `rust_events` row (`hidden_at`, `hidden_by`). The events
endpoint leaves hidden chat lines out for everybody except staff, who get them marked.
- **The events endpoint filters per reader** (S11) with `ctx.safety.forViewer`: a line is left out
when its speaker's linked account is blocked, or, for an unlinked speaker, when their Steam ID
is; and when the reader hid it. Signed-in readers get `canBlock` on each chat line (S13): false
when the speaker's linked account is staff (`ctx.safety.staffAmong`) or is the reader.
- **The live feed's chat lines gain a menu**: *Report*, *Hide for me*, *Block player* (where
`canBlock`), and for staff *Hide for everyone*. They call core's `/player/reports`,
`/auth/me/blocks` and `/auth/me/hidden` through the shared request primitive.
ID, the line, server, channel, wipe, time, and the linked account if there is one; null when the
viewer may not see presence on that server, or the line is hidden for everyone and the viewer is
not staff) and `hide`/`unhide`. `registerGameIdentities([{ kind: 'steam', … }])` with `ownersOf`
reading `rust_account_links`. A `rust.chat` id is a `rust_events` id whose kind is `player.chat`;
any other id is not one, so reporting a death by its id is a 404.
- **Hide for everyone** sets a flag on the `rust_events` row (`hidden_at`, `hidden_by`, added by
`ALTER … ADD COLUMN IF NOT EXISTS` so an existing install gets them). The events endpoint leaves
hidden chat lines out for everybody except staff, who get them with `hidden: true`. It is set from
core's Reports queue, or from the line's own menu through `POST /admin/rust/chat/:id/hide`, and
cleared only there, through `POST /admin/rust/chat/:id/unhide` (admins and moderators, the two
roles the queue is open to; both written to the activity log as `rust.chat.hide` /
`rust.chat.unhide`). Hiding a line the retention sweep has already pruned does nothing, and the
report is still actioned.
- **The events endpoint filters per reader** (S11) with `ctx.safety.forViewer`, **in the query**,
so a page of 100 lines is still 100 long: a line is left out when its Steam ID is linked to an
account the reader blocks, or is one of the reader's own Steam-ID blocks (which core answers only
for ids still unlinked, S14); and when the reader hid it. Only `player.chat` rows are filtered: a
block hides what someone says, and their deaths and connects stay in the feed. Signed-in readers
get `canBlock` on each chat line (S13): false when the line has no Steam ID, or the speaker's
linked account is staff (`ctx.safety.staffAmong`) or is the reader. If the reader's blocks cannot
be read, the feed is served unfiltered for them with a warning in the log, rather than failed;
if who is staff cannot be read, every line says `canBlock: false`.
- **The live feed's chat lines gain a menu** (`components/ChatMenu.jsx`, a `⋯` at the end of each
line): *Report* (the six reasons and an optional detail), *Hide for me*, *Block player* (where
`canBlock`, after a confirmation that says the player is not told), and for staff *Hide for
everyone* or, on a hidden line, *Show to everyone again*. A signed-out reader gets *Sign in to
report or block*. They call core's `/player/reports`, `/auth/me/blocks` and `/auth/me/hidden`
through the shared request primitive, and re-read the feed after anything that changes it.
- On the demo, all of this stays **open**: it is what a Play reviewer tests, and the wipe resets it.
None of these routes is in `demoLocks`.
### 4.3 Android app (chat lines only, S4)
@@ -180,6 +200,29 @@ Play declarations gain "user-generated content: collected". M21 says so.
(`rust.chat`, `ctx.safety`) is exercised by unit tests here and walks with step 3.
3. **Module-Rust** (`edge`): §4.2 plus the core-ref pin. One PR. **Integration-kit**: the re-pin and
a paragraph on `reportTargets`. One PR.
**Built 2026-10-30** as Module-Rust `feature/user-safety` (`coreApi` ^1.21.0, pinned to the
website#233 merge) and Integration-kit `docs/core-1.21.0`. Server and client suites, the import,
bundle, swagger and engagement checks, and the frozen route manifest against a core at the pin
(two new routes, all documented) are green. Walked against a throwaway database on the local
MariaDB with a core on its own port and this branch's module installed, with chat lines and Steam
links written straight into the tables (no game needed), 28 rows over bearer logins: a
signed-out reader got every line and no `canBlock`; a member's `canBlock` was true for unlinked
and linked players and false for their own line and a moderator's; a report reached the queue
with the line's snapshot; a line on a server whose presence is staff-only, a death's id and a
missing id were all 404; blocking an unlinked speaker made a Steam-ID block and took their chat
(not their death) out of that member's feed only; blocking a linked speaker made an account
block; blocking a moderator was `403 staff`; *Hide for me* hid one line for one member; a
Steam-ID block whose id was then linked read back as an account block and kept the line out
(S14); *Hide for everyone* from the queue actioned the report, took the line out for members and
signed-out readers, showed it to staff marked, and made it unreportable; the line's own staff
route unhid and hid it (a player got 403, a missing line 404) and wrote the activity log;
unblocking and *Show them all again* brought the lines back. Then in the browser: the `⋯` menu
signed out (*Sign in to report or block*), as a member (Report with the duplicate-report
refusal and a fresh report, Block with its confirmation, no Block on their own or the
moderator's line), *Account → Blocked* listing the Steam-ID block and both account blocks, and as
a moderator *Hide for everyone* / *Show to everyone again* with the *Hidden by staff* marker and
the Reports queue showing both chat-line reports from their snapshots. No console errors.
4. **Android** (`edge`): §4.3. One PR.
5. **A walk** on the demo after the next wipe: report, hide and block, each on the web and in the
app; *Hide for everyone* from the queue; a blocked Steam ID that later links to an account stays