feat(safety): report, block and hide chat lines (MODULE_API 1.21.0)
Some checks failed
PR Checks / client-build (pull_request) Successful in 51s
PR Checks / frozen-manifest (pull_request) Successful in 53s
PR Checks / server-tests (pull_request) Failing after -17s

USER_SAFETY.md step 3, the module half of report, block and hide.

- server/safety.js registers the rust.chat report target (snapshot for one
  reader, null when they could not have seen the line; staff's hide and
  unhide) and the steam game identity (ownersOf from rust_account_links).
- The events feed filters chat per reader, in the query: lines from a
  blocked account's Steam ids, the reader's unlinked Steam-ID blocks and the
  lines they hid. Signed-in readers get canBlock on each chat line (false for
  their own and for staff-linked players). Lines staff hid for everyone are
  left out for everyone but staff, who get them with hidden: true.
- rust_events gains hidden_at and hidden_by (ALTER ... IF NOT EXISTS).
- POST /admin/rust/chat/:id/hide and /unhide (admin, moderator), logged.
- The feed's chat lines get a menu: Report, Hide for me, Block player,
  and for staff Hide for everyone / Show to everyone again.
- coreApi ^1.21.0; ci/core-ref.json pinned to the website#233 merge;
  routes.manifest.json and swagger-fragment.json regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-30 09:04:08 -05:00
parent 1fb685fa0f
commit dd9b8b2711
24 changed files with 1182 additions and 25 deletions

View File

@@ -103,17 +103,31 @@ async function viewerLevel(req) {
* decided the rung: a banned account is nobody, whatever its token names.
*/
async function viewer(req) {
const nobody = { level: 'public', userId: null }
try {
const claimed = req.user || core.auth.getUserFromRequest(req)
if (!claimed || claimed.id == null) return nobody
if (!claimed || claimed.id == null) return { level: 'public', userId: null }
return await viewerById(claimed.id)
} catch (err) {
log.warn('could not resolve the viewer; treating them as anonymous', { error: err.message })
return { level: 'public', userId: null }
}
}
const user = await core.users.getById(claimed.id)
/**
* The same re-read, for a caller that has an account id and no request: core's
* report-target `snapshot`, which is handed `viewer.userId` (USER_SAFETY.md
* §4.2). The role core passes beside it is the session's; this reads the row.
*/
async function viewerById(userId) {
const nobody = { level: 'public', userId: null }
if (userId == null) return nobody
try {
const user = await core.users.getById(userId)
if (!user) return nobody
if (user.status && user.status !== 'active') return nobody
const level = user.role === 'admin' || user.role === 'moderator' ? 'staff' : 'signed_in'
return { level, userId: user.id != null ? user.id : claimed.id }
return { level, userId: user.id != null ? user.id : userId }
} catch (err) {
log.warn('could not resolve the viewer; treating them as anonymous', { error: err.message })
return nobody
@@ -154,11 +168,22 @@ async function presenceFor(serverId) {
*/
async function canSeePresence(req, serverId) {
try {
const [level, required] = await Promise.all([viewerLevel(req), presenceFor(serverId)])
return { visible: meets(level, required), level, required }
const [who, required] = await Promise.all([viewer(req), presenceFor(serverId)])
return { visible: meets(who.level, required), level: who.level, required, userId: who.userId }
} catch (err) {
log.warn('could not resolve presence visibility; withholding it', { server: serverId, error: err.message })
return { visible: false, level: 'public', required: DEFAULT_PRESENCE }
return { visible: false, level: 'public', required: DEFAULT_PRESENCE, userId: null }
}
}
/** `canSeePresence` for an account id rather than a request (a report's snapshot). */
async function canSeePresenceAs(userId, serverId) {
try {
const [who, required] = await Promise.all([viewerById(userId), presenceFor(serverId)])
return { visible: meets(who.level, required), level: who.level, required, userId: who.userId }
} catch (err) {
log.warn('could not resolve presence visibility; withholding it', { server: serverId, error: err.message })
return { visible: false, level: 'public', required: DEFAULT_PRESENCE, userId: null }
}
}
@@ -314,9 +339,11 @@ module.exports = {
normalise,
viewerLevel,
viewer,
viewerById,
fleetPresence,
presenceFor,
canSeePresence,
canSeePresenceAs,
describe,
update,
}