# retoor <retoor@molodetz.nl>
from .._shared import endpoint, field
from devplacepy.database.moderation import (
CONSENT_KINDS,
MODERATION_ACTIONS,
REPORTABLE_TARGETS,
REPORT_REASONS,
REPORT_STATUSES,
)
REPORT_TARGETS = list(REPORTABLE_TARGETS)
REASON_KEYS = list(REPORT_REASONS)
CONSENT_KEYS = list(CONSENT_KINDS)
SAMPLE_REPORT = {
"uid": "REPORT_UID",
"target_type": "post",
"target_uid": "POST_UID",
"target_url": "/posts/a-post",
"reason": "harassment",
"reason_label": "Harassment or bullying",
"detail": "Repeated personal attacks in the thread.",
"severity": "warn",
"status": "open",
"origin": "member",
"categories": [],
"created_at": "2026-01-05T10:00:00+00:00",
"resolved_at": "",
"reporter_name": "alice",
"owner_name": "bob",
"report_count": 2,
}
GROUP = {
"slug": "moderation",
"title": "Reporting & Moderation",
"intro": """
# Reporting & Moderation
Every externally visible surface on DevPlace is reportable through one polymorphic
endpoint, and every report lands in one queue with one state machine. The reason
list is served by `GET /reports/reasons`, so a native client renders the same
dialog the web UI does.
DevPlace commits to reviewing every report within the window published on the
[content moderation](/docs/content-moderation.html) page. Filing a report always
returns an acknowledgement carrying that window.
The moderation endpoints under `/admin/moderation` are administrator-only and are
subject to the admin seniority rule: a junior administrator cannot action a more
senior one.
Every endpoint follows the shared [Conventions & Errors](/docs/conventions.html) (auth, content
negotiation, pagination, status codes); see [Authentication](/docs/authentication.html) for the
four ways to sign requests.
""",
"endpoints": [
endpoint(
id="report-reasons",
method="GET",
path="/reports/reasons",
title="List report reasons",
summary="The reason keys a report may be filed under, with their labels.",
auth="public",
sample_response={
"reasons": [{"key": "harassment", "label": "Harassment or bullying"}],
"severities": ["info", "warn", "critical"],
},
),
endpoint(
id="report-create",
method="POST",
path="/reports/{target_type}/{target_uid}",
title="Report content",
summary="File a report against any user-generated surface.",
auth="user",
encoding="form",
destructive=False,
params=[
field(
"target_type",
"path",
"enum",
True,
"post",
"The kind of content being reported.",
REPORT_TARGETS,
),
field(
"target_uid",
"path",
"string",
True,
"POST_UID",
"UID of the reported item.",
),
field(
"reason",
"form",
"enum",
True,
"harassment",
"Why the content breaks the guidelines.",
REASON_KEYS,
),
field(
"detail",
"form",
"string",
False,
"",
"Free text for the moderator, up to 2000 characters.",
),
],
notes=[
"A second report on the same target by the same reporter updates the "
"open report instead of creating a duplicate.",
"You cannot report your own content.",
],
sample_response={
"ok": True,
"redirect": "/reports/mine",
"data": {
"uid": "REPORT_UID",
"status": "open",
"severity": "warn",
"sla_hours": 24,
},
},
),
endpoint(
id="reports-mine",
method="GET",
path="/reports/mine",
title="List your reports",
summary="The reports you filed and the outcome of each.",
auth="user",
params=[
field(
"status",
"query",
"enum",
False,
"open",
"Filter by report status.",
list(REPORT_STATUSES),
),
field("page", "query", "integer", False, "1", "Page number."),
],
sample_response={
"reports": [SAMPLE_REPORT],
"pagination": {"page": 1, "total": 1, "total_pages": 1},
"status": "",
},
),
endpoint(
id="admin-moderation",
method="GET",
path="/admin/moderation",
title="The moderation queue",
summary="Reported content awaiting a decision, oldest open first.",
auth="admin",
params=[
field(
"status",
"query",
"enum",
False,
"open",
"Filter by report status.",
list(REPORT_STATUSES),
),
field("page", "query", "integer", False, "1", "Page number."),
],
sample_response={
"reports": [SAMPLE_REPORT],
"counts": {"open": 1, "acknowledged": 0, "actioned": 0, "dismissed": 0},
"sla": {
"sla_hours": 24,
"oldest_open_hours": 1.5,
"breached": 0,
"within_sla": True,
},
},
),
endpoint(
id="admin-moderation-report",
method="GET",
path="/admin/moderation/{uid}",
title="Read one report",
summary="One report with its decisions and the author's history.",
auth="admin",
params=[
field("uid", "path", "string", True, "REPORT_UID", "Report UID."),
],
sample_response={
"report": SAMPLE_REPORT,
"actions": [],
"history": [],
"available_actions": list(MODERATION_ACTIONS),
"can_remove": True,
},
),
endpoint(
id="admin-moderation-status",
method="POST",
path="/admin/moderation/{uid}/status",
title="Set a report status",
summary="Move a report through the triage state machine.",
auth="admin",
encoding="form",
params=[
field("uid", "path", "string", True, "REPORT_UID", "Report UID."),
field(
"status",
"form",
"enum",
True,
"acknowledged",
"New status.",
list(REPORT_STATUSES),
),
],
sample_response={"ok": True, "redirect": "/admin/moderation/REPORT_UID"},
),
endpoint(
id="admin-moderation-decide",
method="POST",
path="/admin/moderation/{uid}/decide",
title="Decide a report",
summary="Apply a moderation decision and notify the affected user.",
auth="admin",
encoding="form",
destructive=True,
params=[
field("uid", "path", "string", True, "REPORT_UID", "Report UID."),
field(
"action",
"form",
"enum",
True,
"dismiss",
"The decision to apply.",
list(MODERATION_ACTIONS),
),
field(
"reason",
"form",
"string",
False,
"",
"Reason shown to the affected user.",
),
field("notes", "form", "string", False, "", "Internal notes."),
field(
"duration_hours",
"form",
"integer",
False,
"24",
"Suspension length in hours.",
),
],
notes=[
"A report already resolved by another moderator answers 409.",
"Content removal is unavailable for targets that have no removal "
"path (direct messages, accounts, workspaces, polls, assistant "
"output); act on the account instead.",
],
sample_response={"ok": True, "redirect": "/admin/moderation/REPORT_UID"},
),
endpoint(
id="admin-user-suspend",
method="POST",
path="/admin/users/{uid}/suspend",
title="Suspend an account",
summary="Suspend an account for a fixed period with a stated reason.",
auth="admin",
encoding="form",
destructive=True,
params=[
field("uid", "path", "string", True, "USER_UID", "User UID."),
field("reason", "form", "string", False, "", "Reason shown to the user."),
field(
"duration_hours",
"form",
"integer",
False,
"24",
"Suspension length in hours.",
),
],
notes=[
"A suspended account can still read, still see why, and still delete "
"itself, but cannot create content.",
],
sample_response={"ok": True, "redirect": "/admin/users"},
),
endpoint(
id="admin-user-lift",
method="POST",
path="/admin/users/{uid}/lift",
title="Lift a restriction",
summary="Clear a suspension or ban and restore the account.",
auth="admin",
encoding="form",
params=[field("uid", "path", "string", True, "USER_UID", "User UID.")],
sample_response={"ok": True, "redirect": "/admin/users"},
),
endpoint(
id="admin-user-ban",
method="POST",
path="/admin/users/{uid}/ban",
title="Ban an account",
summary="Permanently close an account and revoke every credential.",
auth="admin",
encoding="form",
destructive=True,
params=[
field("uid", "path", "string", True, "USER_UID", "User UID."),
field("reason", "form", "string", False, "", "Reason shown to the user."),
],
sample_response={"ok": True, "redirect": "/admin/users"},
),
endpoint(
id="auth-accept-terms-page",
method="GET",
path="/auth/accept-terms",
title="The terms acceptance page",
summary="The Terms of Service version in force and the version this account accepted.",
auth="user",
sample_response={"terms_version": "1", "accepted_version": ""},
),
endpoint(
id="auth-accept-terms",
method="POST",
path="/auth/accept-terms",
title="Accept the terms",
summary="Record acceptance of the Terms of Service version in force.",
auth="user",
encoding="form",
notes=[
"A member whose accepted version is behind the version in force is "
"redirected here on any mutating request. Reading, the docs, the "
"safety controls and account deletion are never blocked.",
],
sample_response={"ok": True, "redirect": "/feed", "data": {"terms_version": "1"}},
),
endpoint(
id="profile-consent",
method="POST",
path="/profile/{username}/consent",
title="Grant or withdraw a consent",
summary="Change one consent on your own account. Withdrawal takes effect at once.",
auth="user",
encoding="form",
params=[
field("username", "path", "string", True, "USERNAME", "Your own username."),
field(
"kind",
"form",
"enum",
True,
"ai_third_party",
"Consent to change.",
CONSENT_KEYS,
),
field("granted", "form", "enum", True, "1", "1 grants, 0 withdraws.", ["1", "0"]),
],
notes=[
"Withdrawing `ai_third_party` makes the AI gateway refuse every call "
"that would send your own content to the provider, whatever the "
"per-feature preference says.",
"Withdrawing `activity_recording` stops presence writes; you simply "
"appear offline.",
"Only the account holder can change a consent. An administrator "
"reads the record but never grants or withdraws it for someone else.",
],
sample_response={
"ok": True,
"redirect": "/profile/USERNAME?tab=privacy",
"data": {"kind": "ai_third_party", "state": "granted"},
},
),
endpoint(
id="profile-mature-content",
method="POST",
path="/profile/{username}/mature-content",
title="Set the mature-content preference",
summary="Show or hide content labelled mature for your own account.",
auth="user",
encoding="form",
params=[
field("username", "path", "string", True, "USERNAME", "Your own username."),
field(
"mature_opt_in",
"form",
"enum",
True,
"1",
"1 shows mature content, 0 hides it.",
["1", "0"],
),
],
notes=[
"Only the account holder can change this preference. An administrator "
"reads the privacy tab but never sets it for someone else.",
],
sample_response={
"ok": True,
"redirect": "/profile/USERNAME?tab=privacy",
"data": {"mature_opt_in": True},
},
),
endpoint(
id="profile-delete",
method="GET",
path="/profile/{username}/delete",
title="Account deletion page",
summary="What deletion removes, what is retained, and the grace window.",
auth="user",
params=[
field("username", "path", "string", True, "USERNAME", "Your own username."),
],
sample_response={
"username": "USERNAME",
"grace_hours": 24,
"removed": ["Your account record, username, email address and password"],
"retained": ["Append-only audit and moderation records"],
},
),
endpoint(
id="profile-delete-confirm",
method="POST",
path="/profile/{username}/delete",
title="Delete your account",
summary="Permanently delete your account and personal data.",
auth="user",
encoding="form",
destructive=True,
params=[
field("username", "path", "string", True, "USERNAME", "Your own username."),
field("password", "form", "string", True, "PASSWORD", "Your account password."),
],
notes=[
"Only the account holder can delete an account; an administrator uses "
"a ban instead.",
"Sessions and tokens are revoked and the profile is anonymised "
"immediately; the deletion event is purged after the grace window.",
],
sample_response={
"ok": True,
"redirect": "/",
"data": {"stamp": "2026-01-05T10:00:00+00:00", "rows": 42, "grace_hours": 24},
},
),
endpoint(
id="workspaces-index",
method="GET",
path="/workspaces/index",
title="Published workspace index",
summary="Every workspace published to the public ingress, with its link.",
auth="public",
params=[field("page", "query", "integer", False, "1", "Page number.")],
notes=[
"The project-derived `description` and `project_url` come back empty "
"unless you may view the workspace's project, so a private project "
"never leaks its title or description through this public listing.",
],
sample_response={
"workspaces": [
{
"uid": "INSTANCE_UID",
"name": "demo",
"slug": "demo",
"owner_uid": "USER_UID",
"url": "{{ base }}/p/demo",
"description": "A demo workspace.",
"owner": "alice",
"maturity": "general",
"project_url": "/projects/demo",
}
],
"total": 1,
},
),
],
}