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

Merged
whitlocktech merged 1 commits from docs/user-safety-rust into main 2026-10-30 09:18:56 -05:00

View File

@@ -1,6 +1,7 @@
# Report, block and hide — user safety on the website and in the app # 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 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 ([`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. amended by S11-S14 (§5) when the core was built.
@@ -111,20 +112,39 @@ staff's.
### 4.2 Module-Rust ### 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 - `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 ID, the line, server, channel, wipe, time, and the linked account if there is one; null when the
see presence on that server) and `hide`/`unhide`. `registerGameIdentities([{ kind: 'steam', … viewer may not see presence on that server, or the line is hidden for everyone and the viewer is
}])` with `ownersOf` reading `rust_account_links`. not staff) and `hide`/`unhide`. `registerGameIdentities([{ kind: 'steam', … }])` with `ownersOf`
- **Hide for everyone** sets a flag on the `rust_events` row (`hidden_at`, `hidden_by`). The events reading `rust_account_links`. A `rust.chat` id is a `rust_events` id whose kind is `player.chat`;
endpoint leaves hidden chat lines out for everybody except staff, who get them marked. any other id is not one, so reporting a death by its id is a 404.
- **The events endpoint filters per reader** (S11) with `ctx.safety.forViewer`: a line is left out - **Hide for everyone** sets a flag on the `rust_events` row (`hidden_at`, `hidden_by`, added by
when its speaker's linked account is blocked, or, for an unlinked speaker, when their Steam ID `ALTER … ADD COLUMN IF NOT EXISTS` so an existing install gets them). The events endpoint leaves
is; and when the reader hid it. Signed-in readers get `canBlock` on each chat line (S13): false hidden chat lines out for everybody except staff, who get them with `hidden: true`. It is set from
when the speaker's linked account is staff (`ctx.safety.staffAmong`) or is the reader. core's Reports queue, or from the line's own menu through `POST /admin/rust/chat/:id/hide`, and
- **The live feed's chat lines gain a menu**: *Report*, *Hide for me*, *Block player* (where cleared only there, through `POST /admin/rust/chat/:id/unhide` (admins and moderators, the two
`canBlock`), and for staff *Hide for everyone*. They call core's `/player/reports`, roles the queue is open to; both written to the activity log as `rust.chat.hide` /
`/auth/me/blocks` and `/auth/me/hidden` through the shared request primitive. `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. - 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) ### 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. (`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 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. 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. 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 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 app; *Hide for everyone* from the queue; a blocked Steam ID that later links to an account stays