# 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,
            },
        ),
    ],
}
