diff --git a/ci/bundle.json b/ci/bundle.json index d82dba8..f7c49ac 100644 --- a/ci/bundle.json +++ b/ci/bundle.json @@ -61,6 +61,7 @@ "permSync.js", "raidableBases.js", "router", + "safety.js", "sidecarClient.js", "titleSync.js", "worldEvents.js" diff --git a/ci/core-ref.json b/ci/core-ref.json index 6db419d..6c6f1a3 100644 --- a/ci/core-ref.json +++ b/ci/core-ref.json @@ -1,6 +1,6 @@ { - "$comment": "The core this module is proved against. MODULE_API.md §5.3: the frozen-manifest job clones RunicGateway/website at this exact ref, drops this module in as modules/rust and runs CORE's own routeManifest.js — nothing else can answer whether the URLs the module claims are the URLs it actually serves, because a mount prefix is a string in server/index.js and a documented path is a string in a JSON file, and whether those name the same URL is a fact about a running core. It also answers the blind spot phase 1 had to check by hand: core mounts several routes at the TIER ROOT (/status, /version), which the loader's collision probe cannot see, so /rust being free is asserted here by a core rather than by a reading. Pinned rather than tracking a branch on purpose: core moves for reasons that have nothing to do with this module, and a bump is then a deliberate commit saying which core the module was last proved against, instead of an unexplained red X on someone else's PR. Bump it, regenerate routes.manifest.json, and commit both together. This module needs MODULE_API 1.10.0 (module.json's coreApi is ^1.10.0), which the Event System cutover put on `main` — so unlike Module-uo, which spent the Event System window pinned to `edge`, this repo starts pinned to `main` and should stay there unless it comes to depend on a contract member that has not shipped yet. **2026-09-27, protocol 13 step 2:** this module calls ctx.events.expired (MODULE_API 1.11.0, PLAN_FIXES D183), so coreApi is ^1.11.0. It was pinned to website#209's branch head while that PR was open — a 1.10.0 core refuses to load it — and is back on `main` at the sha #209 merged as. **2026-10-12, protocol 14 (PLAN_REDESIGNS §2):** this module registers an event template and declares param durations, a lease value example and option notes (MODULE_API 1.13.0), so coreApi is ^1.13.0. It was pinned to website#212's branch head while that PR was open, and is back on `main` at the sha #212 merged as (the same tree). **2026-10-12, PLAN_REDESIGNS §12 (permission rewards):** this module declares a param with multiple: true (MODULE_API 1.14.0), so coreApi is ^1.14.0. It was pinned to website#213's branch head while that PR was open, and is back on `main` at the sha #213 merged as (the same tree). **2026-10-15, PLAN_REDESIGNS §14.3.4 (Dangerous Treasures):** the Start a Dangerous Treasures event step answers DT's warm-up with retryAfterMs (MODULE_API 1.15.0, D371), so coreApi is ^1.15.0, pinned to `main` at the sha website#215 merged as. **2026-10-19, Steam sign-in (PLAN_REDESIGNS §15, D392/D396):** this module registers onIdentityLinking and onIdentityLinked (MODULE_API 1.17.0), so coreApi is ^1.17.0. Pinned to the head of website feat/steam-signin while that PR is open — a 1.16.0 core refuses to load it — and is now on `edge` at the sha website#218 merged as. That is `edge`, not `main`, because #218 went into `edge` for the in-progress cutover. **2026-10-22, the cutover (PLAN_LAST_FIXES stage 6):** website#220 put `edge` on `main`, so this is back on `main` at the sha #220 merged as. That tree adds the last fixes' core half (website#219, no MODULE_API bump), whose only route changes are core's own event routes, so routes.manifest.json does not change. **2026-10-26, the public demo (DEMO_MODE.md §3.4, §3.5, DM18):** this module declares demoLocks, reads ctx.demoMode, registers onDemoSeed (MODULE_API 1.18.0) and gives its two console-command actions demoRefuses (1.19.0), so coreApi is ^1.19.0. It was pinned to the head of website feature/demo-step-refusal (website#224) while that PR was open — a 1.18.0 core never asks demoRefuses — and is back on `main` at the sha #224 merged as (the same tree, so routes.manifest.json does not change).", + "$comment": "The core this module is proved against. MODULE_API.md §5.3: the frozen-manifest job clones RunicGateway/website at this exact ref, drops this module in as modules/rust and runs CORE's own routeManifest.js — nothing else can answer whether the URLs the module claims are the URLs it actually serves, because a mount prefix is a string in server/index.js and a documented path is a string in a JSON file, and whether those name the same URL is a fact about a running core. It also answers the blind spot phase 1 had to check by hand: core mounts several routes at the TIER ROOT (/status, /version), which the loader's collision probe cannot see, so /rust being free is asserted here by a core rather than by a reading. Pinned rather than tracking a branch on purpose: core moves for reasons that have nothing to do with this module, and a bump is then a deliberate commit saying which core the module was last proved against, instead of an unexplained red X on someone else's PR. Bump it, regenerate routes.manifest.json, and commit both together. This module needs MODULE_API 1.10.0 (module.json's coreApi is ^1.10.0), which the Event System cutover put on `main` — so unlike Module-uo, which spent the Event System window pinned to `edge`, this repo starts pinned to `main` and should stay there unless it comes to depend on a contract member that has not shipped yet. **2026-09-27, protocol 13 step 2:** this module calls ctx.events.expired (MODULE_API 1.11.0, PLAN_FIXES D183), so coreApi is ^1.11.0. It was pinned to website#209's branch head while that PR was open — a 1.10.0 core refuses to load it — and is back on `main` at the sha #209 merged as. **2026-10-12, protocol 14 (PLAN_REDESIGNS §2):** this module registers an event template and declares param durations, a lease value example and option notes (MODULE_API 1.13.0), so coreApi is ^1.13.0. It was pinned to website#212's branch head while that PR was open, and is back on `main` at the sha #212 merged as (the same tree). **2026-10-12, PLAN_REDESIGNS §12 (permission rewards):** this module declares a param with multiple: true (MODULE_API 1.14.0), so coreApi is ^1.14.0. It was pinned to website#213's branch head while that PR was open, and is back on `main` at the sha #213 merged as (the same tree). **2026-10-15, PLAN_REDESIGNS §14.3.4 (Dangerous Treasures):** the Start a Dangerous Treasures event step answers DT's warm-up with retryAfterMs (MODULE_API 1.15.0, D371), so coreApi is ^1.15.0, pinned to `main` at the sha website#215 merged as. **2026-10-19, Steam sign-in (PLAN_REDESIGNS §15, D392/D396):** this module registers onIdentityLinking and onIdentityLinked (MODULE_API 1.17.0), so coreApi is ^1.17.0. Pinned to the head of website feat/steam-signin while that PR is open — a 1.16.0 core refuses to load it — and is now on `edge` at the sha website#218 merged as. That is `edge`, not `main`, because #218 went into `edge` for the in-progress cutover. **2026-10-22, the cutover (PLAN_LAST_FIXES stage 6):** website#220 put `edge` on `main`, so this is back on `main` at the sha #220 merged as. That tree adds the last fixes' core half (website#219, no MODULE_API bump), whose only route changes are core's own event routes, so routes.manifest.json does not change. **2026-10-26, the public demo (DEMO_MODE.md §3.4, §3.5, DM18):** this module declares demoLocks, reads ctx.demoMode, registers onDemoSeed (MODULE_API 1.18.0) and gives its two console-command actions demoRefuses (1.19.0), so coreApi is ^1.19.0. It was pinned to the head of website feature/demo-step-refusal (website#224) while that PR was open — a 1.18.0 core never asks demoRefuses — and is back on `main` at the sha #224 merged as (the same tree, so routes.manifest.json does not change). **2026-10-30, account deletion (ACCOUNT_DELETION.md §4.4):** this module registers onUserDeleting (MODULE_API 1.20.0), so coreApi is ^1.20.0, pinned to `main` at the sha website#232 merged as. **2026-10-30, report, block and hide (USER_SAFETY.md §4.2):** this module registers a rust.chat report target and the steam game identity, and reads ctx.safety (MODULE_API 1.21.0), so coreApi is ^1.21.0, pinned to `main` at the sha website#233 merged as.", "repo": "https://gitea.whitlocktech.com/RunicGateway/website.git", - "ref": "69998ec54854d46681def92985a2b299ee3582a6", - "refName": "website main @ website#224 merge, MODULE_API 1.19.0" + "ref": "f13b8fe1fec9dc330a985430b8bd4f67c9ec26c5", + "refName": "website main @ website#233 merge, MODULE_API 1.21.0" } diff --git a/client/src/api.js b/client/src/api.js index 66cf202..40d9a19 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -350,6 +350,21 @@ export const adminUserBackpacks = { req(`/admin/users/${encodeURIComponent(userId)}/rust/backpacks${query({ serverId, steamId })}`), } +// ── report, block and hide (USER_SAFETY.md §4.2) ─────────────────────────── +// Core's routes, not this module's: the Reports queue, a member's blocks and +// their "Hide for me" list are core's and hold for every module and the forum. +// Each names an ITEM — `{ targetType: 'rust.chat', targetId }` — never a person; +// core asks `server/safety.js` who said it. The last two are this module's own: +// staff's hide for everyone, set and cleared where the line lives. +export const safety = { + report: ({ targetType, targetId, reason, detail }) => + req('/player/reports', { method: 'POST', body: { targetType, targetId, reason, detail } }), + block: ({ targetType, targetId }) => req('/auth/me/blocks', { method: 'POST', body: { targetType, targetId } }), + hide: ({ targetType, targetId }) => req('/auth/me/hidden', { method: 'POST', body: { targetType, targetId } }), + hideForEveryone: (lineId) => req(`/admin/rust/chat/${encodeURIComponent(lineId)}/hide`, { method: 'POST' }), + unhideForEveryone: (lineId) => req(`/admin/rust/chat/${encodeURIComponent(lineId)}/unhide`, { method: 'POST' }), +} + // ── core's public event calendar ─────────────────────────────────────────── // The one CORE route this module reads, and core published `runId` on it for // exactly this: a map marker holds a run id and nothing else, and the calendar is @@ -382,6 +397,7 @@ export default { adminUserPermissions, adminUserEconomics, adminUserBackpacks, + safety, coreEvents, BASE, } diff --git a/client/src/components/ChatMenu.jsx b/client/src/components/ChatMenu.jsx new file mode 100644 index 0000000..7f2816d --- /dev/null +++ b/client/src/components/ChatMenu.jsx @@ -0,0 +1,198 @@ +// ── A chat line's menu: report, hide, block (USER_SAFETY.md §4.2) ────────── +// +// One small button at the end of each chat line in the feed, opening a row of +// actions under it. What it offers is `lib/safety.js`'s `chatActions`; what it +// does is core's (report, block, Hide for me) or this module's admin route +// (staff's hide for everyone). After anything that changes what the reader +// sees, the feed is asked again: the server is what filters it (S11), so the +// honest way to show a block is to re-read the feed, never to drop the line here. +// +// The wording follows core's forum menu, so a member reads one set of sentences +// wherever they report or block from. + +import { useState } from 'react' +import { Link } from 'react-router-dom' +import { useAuth } from '../core.js' +import api from '../api.js' +import { CHAT_TARGET, REPORT_REASONS, chatActions } from '../lib/safety.js' + +export default function ChatMenu({ line, onChanged }) { + const { user } = useAuth() + const actions = chatActions(line, user) + const [mode, setMode] = useState('closed') // closed | open | report | block | done + const [reason, setReason] = useState('abuse') + const [detail, setDetail] = useState('') + const [message, setMessage] = useState(null) + const [error, setError] = useState(null) + const [busy, setBusy] = useState(false) + + if (!actions) return null + + // Every move between the menu's states drops the last error: a refusal from + // one action ("you have already reported this") is not about the next. + const go = (next) => { setError(null); setMode(next) } + + const target = { targetType: CHAT_TARGET, targetId: line.id } + const name = (line.frame && line.frame.name) || 'this player' + + const run = async (call, { refetch = true, done = null } = {}) => { + setBusy(true) + setError(null) + try { + await call() + if (done) { + setMessage(done) + setMode('done') + } else { + setMode('closed') + } + if (refetch) await onChanged() + } catch (err) { + setError(err.message || 'Could not do that') + } finally { + setBusy(false) + } + } + + const toggle = ( + + ) + + if (mode === 'closed') return toggle + + return ( + <> + {toggle} +
+ {mode === 'done' &&

{message}

} + + {mode === 'open' && actions.signIn && ( +

+ Sign in + {' '}to report or block. +

+ )} + + {mode === 'open' && !actions.signIn && ( +
+ {actions.report && ( + + )} + {actions.hide && ( + + )} + {actions.block && ( + + )} + {actions.staff === 'hide' && ( + + )} + {actions.staff === 'unhide' && ( + + )} +
+ )} + + {mode === 'report' && ( +
{ + e.preventDefault() + run(() => api.safety.report({ ...target, reason, detail: detail || undefined }), { + refetch: false, + done: 'Reported to site staff.', + }) + }} + style={{ display: 'grid', gap: 8 }} + > + {/* A report changes nothing about the line, and says so: a member who + expects it to vanish and watches it stay will report it again. */} +

+ This goes to site staff. Reporting does not hide or change anything — it asks a + staffer to look. +

+ +