Files
docs/website/USER_SAFETY.md
wtclaude df3d43f355 docs: plan report/block/hide, decide M8's open items, plan demo content
- website/USER_SAFETY.md (new): report, block and hide on the website and
  in the app, S1-S5 decided by the org lead, five questions open.
- android/PLAN.md: M8's three open items decided (keep the rating and add
  report/block/hide; conservative data safety; UnifiedPush on specialUse
  with restart on boot), the order to the cutover, and M21 (Teams and the
  team forum in the app, planned, not scheduled).
- website/DEMO_MODE.md: DM32 and section 11, the demo content plan (two
  seed passes; moderation, Teams/forum/reports, event history, Rust stats).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-29 07:27:21 -05:00

139 lines
9.3 KiB
Markdown

# Report, block and hide — user safety on the website and in the app
**Status:** plan of record, 2026-10-29. Nothing built yet. Each phase stops for the org lead
before the next one starts.
**Why now.** Google Play's content-rating questionnaire for the Android app (`android/PLAN.md` M8)
was answered "users or user-generated content can be reported". The app shows other players' chat
(the Rust live feed) and has no way to report it or to stop seeing someone. Play's user-generated
content policy expects both wherever that answer is given. The org lead chose to make the answer
true rather than retake the questionnaire (S1).
## 1. What a member can do
| Action | Who | Where | Effect |
|---|---|---|---|
| **Report** | any signed-in member | a chat line (web + app), a forum thread, post or upload (web; app later, §6) | a row in the existing **Reports** queue (`TEAMS.md` §5.6). Changes nothing on its own |
| **Hide for me** | any signed-in member | the same items | that one item disappears from **their own** view, on every device. No report is filed |
| **Block** | any signed-in member | the author of any of those items | everything that author writes (chat lines and forum content) disappears from the blocker's view, on the website and in the app, on every device. Undone from *Account → Blocked* |
| **Hide for everyone** | staff (and, for their own forum, Team leaders, as today) | the Reports queue, the forum's existing moderation menu, and the item's own menu | the item disappears for everybody except staff, who see it with a *Hidden by staff* marker and can undo it |
A report and a hide are separate on purpose, as `TEAMS.md` §5.6 already says: filing a report must
never be a way to hide something. *Hide for me* is the member's own tool, and *Hide for everyone* is
staff's.
## 2. Decisions (org lead, 2026-10-29)
| # | Question | Answer |
|---|---|---|
| S1 | The questionnaire says chat is reportable, and it is not. Retake it, drop chat from the app, or add report/block? | **Add report, block and hide.** The questionnaire stays as answered |
| S2 | Where does a block live? | **On the account, everywhere.** Saved on the site against the blocker's account, so it applies on the website and in the app, on every device |
| S3 | "Hide": who hides, and from whom? | **Both.** A member can hide an item for themself, and staff can hide it for everyone |
| S4 | The app has no Teams or forum screens. Does the app get the forum? | **Plan it** (§6, `android/PLAN.md` M21). In the app, report/block/hide go on chat lines now |
| S5 | When does the Android `edge` → `main` cutover happen? | **After this feature ships**, so the first Play bundle already has it |
## 3. What exists today
- **Reports.** `content_reports` (`TEAMS.md` §5.6) is generic by design, but
`contentReports.model.js` accepts only `team_forum_thread`, `team_forum_post` and
`team_forum_upload`, and finds the owning Team for each. Filing is
`POST /player/teams/:slug/forum/report`. The queue is `GET /admin/moderation/reports`, and
`POST /admin/moderation/reports/:id/handle` claims, actions or dismisses a report. "Actioned" only
records a decision; it does nothing to the content.
- **Staff hide in the forum.** `team_forum_moderation` already records `hide`/`unhide` (and pin,
lock, delete) by a leader or staff, through `POST …/forum/threads/:id/moderate` and
`…/posts/:id/moderate`.
- **Chat lines.** Each one is a `rust_events` row in Module-Rust (`kind = 'player.chat'`, with an
`id`, `server_id`, `steam_id`, time and raw frame). The website and the app both read them from
the public `GET /public/rust/servers/:id/events`. The app polls it signed out or in.
- **No blocks or per-member hides** exist anywhere.
## 4. Design (recommendations; §5 lists what needs an answer)
### 4.1 Core (`website`)
- **Module-owned report targets.** A new optional module hook, `reportTargets`, declares the types
a module's content can be reported as (for Rust: `rust.chat`). Each type answers two questions for
core: `snapshot(id)` (author, text, time, where it was said), and `hide(id, actor)` /
`unhide(id, actor)`. Core files the report, stores the snapshot and runs the queue. The module owns
the content and its hidden flag. This is a `MODULE_API` minor bump, **1.20.0**. The Integration
Kit re-pins and gains a paragraph.
- **One filing route for every target**: `POST /player/reports {targetType, targetId, reason,
detail?}`. Forum types keep working through the existing forum route, which then calls the same
model.
- **`content_reports.snapshot JSON`**: what the item said when it was reported. Chat lines are
pruned with the rest of `rust_events`, and a queue row must still show what was said after that.
Forum reports fill it too, so an author editing a post after the report cannot change what staff
read.
- **The queue gains *Hide for everyone*** beside actioned and dismissed. It calls the forum's
existing hide or the module's `hide`, and records the report as actioned with that action named.
- **Blocks**: `user_blocks (blocker_user_id, kind 'user'|'game', blocked_user_id NULL, module_id
NULL, external_id NULL, display_name snapshot, created_at)`. A game block names a game identity
(for Rust, a Steam ID), because most people in chat have no site account. When that identity is
linked to an account, blocking either one hides both.
- **Per-member hides**: `user_hidden_items (user_id, target_type, target_id, created_at)`.
- **Routes**: `GET/POST/DELETE /auth/me/blocks`, `GET/POST/DELETE /auth/me/hidden`. Their
OpenAPI annotations and spec are regenerated as usual.
- **Filtering.** Signed-in forum reads drop blocked authors and hidden items on the server. The
public chat feed is shared and cached, so it cannot be filtered per reader on the server. The
website and the app filter it with the member's lists instead, read once per session and kept
current after each block or hide. Module client chunks reach those lists through a new client
module-API call.
- **Web UI.** The forum's thread and post menus gain *Hide for me* and *Block author*, beside the
existing *Report*. A new *Account → Blocked* page lists blocks and undoes them.
### 4.2 Module-Rust
- `reportTargets['rust.chat']` with `snapshot` (the speaker's name and Steam ID, the line, server,
time, and the linked account if there is one) and `hide`/`unhide`.
- **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 live feed's chat lines gain a menu**: *Report*, *Hide for me*, *Block player*, and for
staff *Hide for everyone*.
- On the demo, all of this stays **open**: it is what a Play reviewer tests, and the wipe resets it.
### 4.3 Android app (chat lines only, S4)
- **Long-press a chat line** for the same menu. Report opens a sheet with the six reasons and an
optional detail. A member who is signed out gets *Sign in to report or block*.
- **Account → Blocked** lists blocks and undoes them, mirroring the web page.
- The feed filters with the member's lists, as in §4.1.
## 5. Questions for the org lead
Each has a recommendation; nothing is built until they are answered.
1. **Signed out.** Can a signed-out visitor report a chat line? *Recommended: no.* Reporting, hiding
and blocking all need an account, as forum reports do today. Otherwise the queue is open to
anyone with `curl`. Play needs the mechanism to exist, not to be anonymous.
2. **Blocking someone who has no account.** Can a member block a Steam ID that is linked to nobody?
*Recommended: yes* (§4.1's game block). Almost everyone in a Rust chat feed is unlinked.
3. **Blocking staff.** Can a member block a moderator or admin? *Recommended: yes.* A block only
hides content from the blocker. It changes nothing about staff powers, and it doesn't stop staff
seeing the blocker.
4. **What a block does to the blocked person.** Nothing beyond hiding, *recommended*. There are no
direct messages to stop, and telling someone they were blocked invites the harassment the block
was for.
5. **Staff view of hidden chat.** Do staff still see a line hidden for everyone? *Recommended: yes,
marked*, so a hide can be checked and undone, as the forum already treats its own hidden posts.
## 6. The team forum in the app (planned, not scheduled)
The app has no Teams screens. `android/PLAN.md` M21 plans them: the member's Teams, a Team's page,
the forum's threads and posts (read, reply, new thread), the same report/hide/block menu on posts
and threads, and the forum's push stream. When it ships, the app lets members post content, so the
Play declarations gain "user-generated content: collected". M21 says so.
## 7. Build order
1. **This plan** (docs). Answers to §5 recorded here.
2. **Core** (`website`): §4.1, `MODULE_API` 1.20.0, tests, OpenAPI, `BACKEND_DESIGN.md`,
`MODULE_API.md`, `TEAMS.md` §5.6 amended. One PR.
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.
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
blocked.
6. Then `android/PLAN.md` M8's cutover (S5).