From df3d43f355f7632ebf79155544d95de7a5392b29 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 29 Oct 2026 07:27:21 -0500 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- README.md | 1 + android/PLAN.md | 83 +++++++++++++++++++++---- website/DEMO_MODE.md | 69 +++++++++++++++++++++ website/USER_SAFETY.md | 138 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 13 deletions(-) create mode 100644 website/USER_SAFETY.md diff --git a/README.md b/README.md index a4b9b1c..924c5db 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ sidecar as a service, and hands you the values the website needs. | [SHARD_VISIBILITY.md](website/SHARD_VISIBILITY.md) | Who sees which shard data — the admin-configurable audience framework | | [TRUSTED_DEVICES_MFA.md](website/TRUSTED_DEVICES_MFA.md) | TOTP two-factor, trusted devices and recovery codes | | [STEAM_SIGNIN.md](website/STEAM_SIGNIN.md) | **Plan, built 2026-10-19** — Sign in with Steam as a built-in provider beside Google and Discord (OpenID 2.0), and a Steam id for modules | +| [USER_SAFETY.md](website/USER_SAFETY.md) | **Plan, 2026-10-29** — report, block and hide on the website and in the app: an account-wide block, a member's own hide and staff's hide for everyone, with module-owned report targets (`MODULE_API` 1.20.0) | | [DEMO_MODE.md](website/DEMO_MODE.md) | **Design, fully decided 2026-10-23** — the public `demo.runicgateway.com`: one flag that locks destructive actions, static logins, and a wipe every couple of hours (website#222) | | [MODERATION_APPEALS.md](website/MODERATION_APPEALS.md) | Moderation actions, content reports and the appeals flow | | [SPAWN_ATLAS.md](website/SPAWN_ATLAS.md) | The bestiary / spawn atlas: what the shard contains, parsed from the shard's own ServUO files — served over the bridge since Protocol 8, so no shared filesystem | diff --git a/android/PLAN.md b/android/PLAN.md index 4e42500..b32ea84 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -1003,20 +1003,46 @@ push, and Play (M6–M8) follow the designed app. for 14 days in a row** before it can apply for production. runicgateway.com `/beta` collects them. - **Open, each waiting for a decision:** + **The three open questions, decided (org lead, 2026-10-29):** - **Content rating.** The questionnaire answered "Social or Communication: moderated chat; users or user-generated content can be reported". The app shows other players' chat (the - Rust feed) but has no report or block action, and Play's user-generated-content policy - expects both where that is declared. Either retake the questionnaire, or add report/block - to the app. - - **Data safety's premise.** The generated notes answer "not collected" almost everywhere, - because the app talks only to a deployment the user chose. The notes call it the answer - most likely to be questioned in review. Declaring user IDs and messages as collected, - encrypted in transit and needed for the app to work, is the conservative alternative. + Rust feed) but had no report or block action, and Play's user-generated-content policy + expects both where that is declared. **Decided: keep the answer and make it true** with + report, block and hide, on the website and in the app. The plan of record is + [`website/USER_SAFETY.md`](../website/USER_SAFETY.md): a block lives on the account and + applies everywhere; a member can hide an item for themself, and staff can hide it for + everyone. In the app it goes on chat lines. The team forum in the app is planned as M21. + - **Data safety's premise.** The generated notes answered "not collected" almost everywhere, + because the app talks only to a deployment the user chose. **Decided: the conservative, + more accurate answers.** Each kind of data the app sends to the deployment is declared + collected, not shared, encrypted in transit and required for the app to work. That is + what Play counts as collection, since the data leaves the device, whoever runs the server. + runicgateway.com's generator (`scripts/playDataSafety.mjs`, from `src/data/collection.mjs`) + changes to say so after an audit of what the app actually transmits. The published privacy + page keeps saying that *we* receive nothing, which stays true. - **The push foreground service.** Play asks for a declaration (and a video) for the - `dataSync` service that holds the ntfy connection. Google reserves `dataSync` for transfers, - and Android 15 limits it to six hours a day. This brings forward §13's "FCM flavor for - Play?" question. + `dataSync` service that holds the ntfy connection. Google reserves `dataSync` for + transfers, and Android 15 limits it to six hours a day. **Decided: keep UnifiedPush.** The + service changes to the `specialUse` type with a `PROPERTY_SPECIAL_USE_FGS_SUBTYPE` reason, + as ntfy does on Play; `specialUse` has no daily limit. **Push also restarts after a + reboot** when the member had it on and is signed in, which `specialUse` allows from + `BOOT_COMPLETED` and `dataSync` does not. This also fixes a crash: `PushService` does not + handle `onTimeout`, so on Android 15+ with target 36 (`edge` since step 1) the six-hour + cut-off would crash the app. The declaration's video is recorded on the emulator (turn push + on, the ongoing notification, a push arriving), and the org lead uploads it. §13's "FCM + flavor for Play?" is therefore answered **no**. + + **The order from here** (org lead, 2026-10-29): + 1. **Push** (`edge`): `specialUse` and the restart on boot. One PR. + 2. **Data safety** (runicgateway.com): the audit and the generator. One PR. + 3. **Report, block and hide** across `website`, `Module-Rust`, the kit and this app + (`USER_SAFETY.md` §7). + 4. **The cutover**, `edge` → `main`, once 1-3 have landed, so the first Play bundle carries + all of it. **Before it merges, the org lead enrolls the existing keystore as the app-signing + key** in Play Console (App integrity, "use my own key"). `PLAY_SERVICE_ACCOUNT_JSON` is + already set, so the release run will call Play, and the signing choice is permanent. On the + draft app, CI's commit is refused as expected; the first bundle is then uploaded by hand + (step 2 above). 10. **M9 — Native SSO login** (post-v1, additive; independent of M8): in-app "Sign in with Google / Discord" via the **Mobile SSO Authorization Bridge** (§4.2). **Backend-first**, mirroring M7's split: @@ -1828,9 +1854,39 @@ push, and Play (M6–M8) follow the designed app. just as wrong. A maintenance error kind, read from that body, is the likely fix. It is outside M20 and waits for a decision. +22. **M21 — Teams and the team forum in the app** (post-v1; planned 2026-10-29, **not scheduled**). + The org lead asked for it with M8's user-safety decision (`../website/USER_SAFETY.md` S4). It + revives the deferred *Platform Teams in the app* below, whose revival trigger (Team pages, + roster and forums on the website) has long since been met. + + **Scope:** + - **A Teams entry** in the menu: the member's own Teams (`GET /player/teams`) and the public + list (`GET /public/teams`), then a Team's page (`/public/teams/:slug`, its members and + activity). + - **The forum**, for a Team the member can read (`GET /player/teams/:slug/access`): threads + (`…/forum/threads`), a thread's posts (`…/forum/threads/:id`), reply and new thread + (`POST …/threads`, `…/threads/:id/posts`), and editing their own post (`PATCH …/posts/:id`). + Leaders get pin, lock and hide through the existing `…/moderate` routes. + - **Report, hide for me and block** on every thread and post, as on chat lines + (`USER_SAFETY.md` §4). + - **Deep links** for the four Team push streams, which today open the app and no more, and the + per-Team mute list (`GET|PUT /auth/me/notifications/teams`; its DTO field has no default, + see the trap noted below). + - **Uploads: view only** to begin with. Sending a file from the phone is a later decision, + because uploads carry the operator's liability acknowledgement (`TEAMS.md` §5.5). + + **What it changes for Play:** members can then post from the app, so the data-safety + declaration gains "user-generated content: collected" and the content rating stays as + answered. runicgateway.com's `collection.mjs` gains the row in the same change. + + **Before it starts:** USER_SAFETY.md's build order is done (the report/block/hide routes M21 + reuses), and the questions it will raise (offline drafts, uploads, the order of the Teams tab + in the menu) go to the org lead first. + ### Deferred (not a milestone) -- **Platform Teams in the app** — **deferred 2026-08-17, no app work scheduled.** The website is +- **Platform Teams in the app** — **revived as M21 on 2026-10-29** (above); the text below is the + 2026-08-17 deferral, kept for its reasoning. **Deferred 2026-08-17, no app work scheduled.** The website is building Teams as a core platform primitive (pages, roster, activity feed, forums, notifications — [`../website/TEAMS.md`](../website/TEAMS.md)), and the app has no story for any of it. Deferred deliberately rather than scheduled: this milestone list is already behind, and adding a large new @@ -2147,7 +2203,8 @@ it is a UX convenience, not a v1 requirement — deferred at M3, descoped at M6; No backend publish token — **decided** (the content-free-tickle design does not require one; optional `NTFY_PUBLISH_TOKEN` is honored if ever set). - FCM flavor: build it for the Play release or ship Play on UnifiedPush too? Decide at M8. (The M7 - Part 2 `PushTransport` seam keeps this swap cheap.) + Part 2 `PushTransport` seam keeps this swap cheap.) **Decided at M8 (2026-10-29): UnifiedPush on + Play too**, with the service moved to the `specialUse` type. - Deep-link / share targets for wiki pages and posts (share/open-in-app). *Notification-tap* deep-links are **resolved** for M7 Part 2 (stream→screen map, work item 7). - iOS: none planned (this is the Android-only choice); revisit only if cross-platform is later diff --git a/website/DEMO_MODE.md b/website/DEMO_MODE.md index f1dd823..df1ce47 100644 --- a/website/DEMO_MODE.md +++ b/website/DEMO_MODE.md @@ -498,6 +498,7 @@ down. | DM29 | (step-5 walk, W2, 2026-10-28) `SECRET_WORDS` also hides harmless fields such as "Allow Master Key" and "TC Auth Only". Narrow it? | **No, leave it as is** | | DM30 | (step 6, 2026-10-28) How does runicgateway.com `/beta` publish the logins? | **Live from the demo.** `/beta` reads this site's `GET /public/settings` `demo.logins` on its server, caches it for a minute, and links to `/account/login` when the demo does not answer (runicgateway.com D82) | | DM31 | (step 6) runicgateway.com's "See it running" links were UO routes, which 404 here. What do they do? | **Per game.** A deep link names its game, and that site's mounted `demoGames` (`["rust"]` for this demo) says which games' links show. Rust's point at `/rust` and `/rust/servers/demo?tab=…`, so that site now quotes the fixture's server id `demo` (runicgateway.com D83) | +| DM32 | (2026-10-29) The moderator tour shows empty queues, and other surfaces are empty too (§11). Which get seeded? | **All four of §11's groups:** moderation history, Teams with their forum and reports, event history with the calendar, and Rust player stats | DNS and HTTPS for `demo.runicgateway.com` come from an external reverse proxy, which the org lead is setting up. The app sits behind it on plain HTTP; core already trusts one proxy hop for @@ -647,3 +648,71 @@ through that proxy. locks). The org lead types the password, ticks the full-access attestation and saves. The closed track does not exist yet (Test and release shows *Get started* for closed and internal testing), so there is no opt-in URL for `betaOptInUrl`, and `/beta`'s first gate stays shut. +7. **Demo content** (DM32, §11): the seed's two passes and the rig's clans. Planned 2026-10-29. + One PR in `website` (core seed, the wipe's second pass), one in `Module-Rust` (`onDemoSeed` + stats and feed lines, into `edge`), a new baseline, and a walk of every surface §11 names after + the next wipe. + +## 11. Demo content (DM32) + +**Why.** A read-only count of the live demo on 2026-10-29 found these surfaces empty after a wipe: +every moderation queue (recent actions, filter hits, spam hits, member events, appeals, reports), +Teams (public, the player's own, staff's review queue), event runs and history, the public calendar +(the three seeded events are never scheduled), `demo-player`'s inbox, and on the Rust side the +leaderboard, wipes, clans and the events feed. Only the four demo accounts exist. A reviewer touring +as `demo-moderator` finds nothing to moderate. + +### 11.1 Two seed passes + +Some of it can be written the moment the site is up. The rest depends on the rig: Teams are +**module-authoritative** and re-synced every 15 minutes (`TEAMS.md`), so a Team written straight +into core would be archived by the next sync, and the Rust stats need the wipe the rig is on. So: + +- **Pass 1, `seed:demo`, as today**: accounts, settings, content, events, and the new core rows + below that need nothing from the rig. +- **Pass 2, `seed:demo --after-rig`**, a new step in `demo-wipe.yml` after the rig reports in and + the first Teams sync has run: Team forum content, reports, Rust stats and feed lines. It waits for + the Teams it names, with a time limit, and fails the wipe if they never appear, as the rig smoke + check does. + +The rig's wipe id comes from the restored world save, so it is the same on every wipe +(`w-20261005T165054Z` today). Pass 2 reads it from the server state rather than the fixture, so a +new baseline with a new map still works. + +### 11.2 What each group seeds + +- **Moderation history** (pass 1, core). Discord-shaped rows for six invented members: a few + `mod_actions` (warn, timeout, ban), `warnings`, `filter_hits` and `spam_hits`, join and leave + `member_events`, staff `mod_notes` (one admin-only), and three `appeals`: one pending to claim, + one under review, one resolved. Times are relative to the seed, so the 24 h / 7 d / 30 d counts + are never zero. There is no bot on the demo, so approving an appeal records the reversal as + failed. That is the shipped behaviour when the bot is down, and the appeal still closes. +- **Teams, forum and reports.** Two or three clans **in the rig's baseline** (the Clans plugin's + data, which already reports an empty board), with stand-in members and `demo-player`'s stand-in + Steam ID in one, so sync brings them in as Teams. One clan's name contains a reserved term, so it + lands in staff's review queue. Pass 2 then writes forum threads and posts in them, and two open + reports. Once report, block and hide exist (`USER_SAFETY.md`), it also writes reports on chat + lines. +- **Event history and the calendar** (pass 1, core). Two finished runs of the seeded events, with + their steps, log, participants (stand-ins, and `demo-player` through its link) and published + results, so the history and results pages fill and `demo-player`'s inbox has the results + notification. The three seeded events are scheduled in the coming days, so the public calendar + shows them. +- **Rust player stats** (pass 2, Module-Rust `onDemoSeed`). About ten stand-in players with wipe + stats (kills, deaths, NPC kills, playtime, gather totals, weapon kills) on the rig's current + wipe, an earlier wipe for the Wipes tab, and a day of feed lines (kills, joins, chat), so the + leaderboard, profiles and feed are populated. The online list stays honest at zero. + +### 11.3 Open questions + +1. **Stand-in Steam IDs.** Every 17-digit `7656119…` number can belong to a real person, and these + would appear on public leaderboards. *Recommended:* check each candidate against Steam's public + profile page before using it, keep only IDs with no profile, and list them in the fixture with + that check recorded. `demo-player`'s existing stand-in gets the same check. +2. **Invented Discord IDs.** The moderation pages show Discord user ids. *Recommended:* synthetic + ids from a block well below any issued since 2016, named in the fixture as demo data. Nothing on + the demo links them to Discord. +3. **Names.** *Recommended:* invented gamer tags ("Rusty_Pete", "Scrap Baron", …), never a real + person's name, and nothing offensive even in the seeded filter hits, which show a placeholder + word. + diff --git a/website/USER_SAFETY.md b/website/USER_SAFETY.md new file mode 100644 index 0000000..4a1276b --- /dev/null +++ b/website/USER_SAFETY.md @@ -0,0 +1,138 @@ +# 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).