USER_SAFETY.md step 2 (core), with S11-S14 decided while building it. - Reports: POST /player/reports takes any target type, core's forum types or a module's registered ones. One resolver (model/safety/targets.js) answers "what is this item, for this reader" and 404s an item the reader cannot see. Every report stores content_reports.snapshot; the queue shows a module item from it. - Hide for everyone: POST /admin/moderation/reports/:id/hide hides the item through its own moderation (forum hide as staff, or the module's hide) and actions the report with actioned_with = 'hide'. - Blocks (user_blocks) name an item, never a person, and land on the author's account first (S12); an unlinked game identity is blocked as itself and becomes an account block once linked (S14). Staff cannot be blocked; a block on someone later promoted stops applying (S8). - Hide for me (user_hidden_items), /auth/me/blocks and /auth/me/hidden. - Forum reads filter blocked authors and hidden items on the server and carry canBlock per thread and post. - MODULE_API 1.21.0: api.registerReportTargets, api.registerGameIdentities, ctx.safety.forViewer and ctx.safety.staffAmong, so a module filters its own per-viewer feeds on the server (S11) and marks lines canBlock (S13). - Web: Hide for me / Block author in the forum menus, a Blocked section on both account pages, Hide for everyone in the Reports queue. - Tests, OpenAPI, route/guard/engagement manifests regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
72 lines
4.1 KiB
JavaScript
72 lines
4.1 KiB
JavaScript
// /player/reports — file a report about any reportable item (USER_SAFETY.md §4.1).
|
||
//
|
||
// One route for every target: core's forum types and every module's registered
|
||
// types (`rust.chat`, …). The forum's own `/teams/:slug/forum/report` keeps
|
||
// working and goes through the same model.
|
||
//
|
||
// A signed-in account is required (S6), which the player group's gate gives:
|
||
// a queue anybody with `curl` could fill is a queue staff stop reading.
|
||
|
||
const express = require('express')
|
||
const { body } = require('express-validator')
|
||
|
||
const reports = require('../../../model/reports/contentReports.model')
|
||
const validate = require('../../../middleware/validate')
|
||
const { makeLimiter } = require('../../../middleware/rateLimit')
|
||
const log = require('../../../utils/logger')('reports')
|
||
|
||
const reportsRouter = express.Router()
|
||
|
||
// The forum report route's cap, for the forum report route's reason (TEAMS.md
|
||
// §5.6's third rule): a report costs the reporter nothing and costs a staffer
|
||
// attention.
|
||
const reportLimiter = makeLimiter({
|
||
windowMs: 60 * 60 * 1000,
|
||
max: 10,
|
||
label: 'content-report',
|
||
message: 'Too many reports. Please give staff a chance to look at the ones you have raised.',
|
||
})
|
||
|
||
async function createReport(req, res) {
|
||
try {
|
||
const result = await reports.file({
|
||
actor: req.user,
|
||
targetType: req.body.targetType,
|
||
targetId: req.body.targetId,
|
||
reason: req.body.reason,
|
||
detail: req.body.detail,
|
||
})
|
||
if (!result.ok) return res.status(result.status || 400).json({ message: result.error })
|
||
return res.json({ ok: true, reportId: result.reportId })
|
||
} catch (err) {
|
||
if (err.status === 503) return res.status(503).json({ message: 'The game module did not answer. Try again in a moment.' })
|
||
log.error('file report failed', { message: err.message })
|
||
return res.status(500).json({ message: 'Internal Server Error' })
|
||
}
|
||
}
|
||
|
||
reportsRouter.post(
|
||
'/',
|
||
// #swagger.tags = ['Player · Reports']
|
||
// #swagger.summary = 'Report an item to site staff'
|
||
// #swagger.description = 'Files a report about a forum thread, post or upload, or a game module’s item (e.g. `rust.chat`, a Rust chat line; MODULE_API 1.21.0). The reporter must be able to see the item: one they cannot see answers 404, the same as one that does not exist. Filing changes nothing about the item; it opens a row in the staff Reports queue with a snapshot of what the item said, which outlives the item. Reports reach site staff and nobody else.'
|
||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ReportRequest" } } } } */
|
||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||
/* #swagger.responses[200] = { description: 'Raised', content: { "application/json": { schema: { type: 'object', properties: { ok: { type: 'boolean' }, reportId: { type: 'integer' } } } } } } */
|
||
/* #swagger.responses[400] = { description: 'Unknown target type or reason', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||
/* #swagger.responses[404] = { description: 'No such item, or not one this account can see', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||
/* #swagger.responses[409] = { description: 'You already have an open report on this', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||
/* #swagger.responses[429] = { description: 'Too many reports (rate limited)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||
/* #swagger.responses[503] = { description: 'The game module that owns the item did not answer', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||
reportLimiter,
|
||
body('targetType').isString().trim().isLength({ min: 1, max: 32 }),
|
||
body('targetId').isInt({ min: 1 }).toInt(),
|
||
body('reason').isIn(reports.REASONS),
|
||
body('detail').optional().isString().trim().isLength({ max: 500 }),
|
||
validate,
|
||
createReport,
|
||
)
|
||
|
||
module.exports = reportsRouter
|