Files
docs/website/USER_SAFETY.md
wtclaude 2ab35334d6 docs(website): plan self-service account deletion
ACCOUNT_DELETION.md is the plan of record for the org lead's decision:
a member deletes their own account on the website and in the app, as
Google Play requires of an app that starts accounts.

- One deletion service for the admin route and the member's own route,
  with a confirm step, the last-admin and demo refusals, clearing the
  username snapshots on what the member wrote, and a notice by email.
- MODULE_API 1.20.0: api.onUserDeleted, which runs after the commit and
  can't stop a deletion.
- Screens on the web and in the app; runicgateway.com /delete-account/
  as the address Play is given.
- What the schema already does (59 keys: credentials cascade, content
  detaches), the gaps, six questions, and the build order.

M8's order puts account deletion before report, block and hide, which
moves USER_SAFETY.md's reportTargets to MODULE_API 1.21.0.

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

9.4 KiB

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. Built after account deletion (ACCOUNT_DELETION.md), which is why it is MODULE_API 1.21.0.

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, unless the author is staff (S8) 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 (§5 holds the decisions that shape it)

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.21.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. Further decisions (org lead, 2026-10-29)

# Question Answer
S6 Can a signed-out visitor report a chat line? No. Reporting, hiding and blocking all need an account, as forum reports do today, so the queue is not open to anyone with curl. A signed-out viewer sees Sign in to report or block
S7 Can a member block a game identity (a Steam ID) that is linked to no account? Yes (§4.1's game block). Almost everyone in a Rust chat feed is unlinked
S8 Can a member block staff? No. The block is refused when its target is a staff account (admin, editor or moderator), or a game identity linked to one, and the menu does not offer it on staff content. A game block whose identity later links to a staff account stops applying, and Account → Blocked says why. Hide for me still works on a single staff item
S9 What does a block do to the blocked person? Nothing beyond hiding their content from the blocker. They are not told; there are no direct messages to stop
S10 Do staff still see a chat line hidden for everyone? Yes, marked Hidden by staff, so the 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), with §5's answers.
  2. Core (website): §4.1, MODULE_API 1.21.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).