Eternaltwin

Home | /api | v1 | forum

/api/v1/forum/sanctions

Warnings and mutes handed out on the forum. Defined in crates/rest/src/forum.rs.

These are forum sanctions. A site-wide ban is a different thing and lives under /api/v1/users/:user_id/sanctions.

A sanction is one of two kinds:

KindMeaning
WarningA record of an event. It never expires — there is no state to lapse.
MuteThe user may not post. May be timed or indefinite.

A sanction is either forum-wide or scoped to one section, and it carries two texts because they have two audiences: reason is shown to the sanctioned user — it is the only way they learn why they cannot post — and internal_note is written for other moderators and is served to them only.

Sanctions are their own collection rather than a sub-resource of the user: a moderator works from the list of what has been handed out, and the same collection answers a user asking why they cannot post. The user query narrows it; the path does not.

deleted_at is when a moderator lifted the sanction; expires_at is when a timed mute stops biting on its own. They are different questions and are kept apart, because an expired mute has nobody to attribute its ending to.

GET /sanctions

Lists sanctions.

Without a user filter this is a moderator's view and a guest is refused: "my own sanctions" is the only reading a guest could be entitled to.

Query parameters

NameTypeDefaultMeaning
userUUID—Only the sanctions of this account.
sectionUUID—Only the sanctions scoped to this section. An identifier, not a key.
activebooleanfalsetrue keeps only the sanctions in force at the moment of the request.
offsetinteger0—
limitintegerthreads_per_pageClamped to 100.

active is a convenience over an instant: a caller asking "who is muted right now" would otherwise have to send its own clock, and a client clock that disagrees with the server's would answer a slightly different question than the one the server enforces. Absent and false mean the same thing — the unfiltered listing, lifted and expired rows included.

Example

GET /api/v1/forum/sanctions?active=true&offset=0&limit=10
Cookie: sid=b8be19ef-2d61-44de-b7d2-9c34ccb8a763
{
  "offset": 0,
  "limit": 10,
  "count": 1,
  "items": [
    {
      "type": "ForumSanction",
      "id": "0f3b9b7e-2c1f-4a49-9a1e-0b7d3f9c0a11",
      "user": {
        "type": "User",
        "id": "28dbb0bf-0fdc-40fe-ae5a-dde193f9fea8",
        "display_name": {"current": {"value": "Alice"}}
      },
      "section": {
        "type": "ForumSection",
        "id": "d6e3a4ae-3b1c-4f3e-9a6b-2c9bdc6b7e10"
      },
      "kind": "Mute",
      "created_at": "2021-02-01T09:00:00.000Z",
      "created_by": {
        "type": "User",
        "id": "9f310484-963b-446b-af69-797feec6813f",
        "display_name": {"current": {"value": "Demurgos"}}
      },
      "deleted_at": null,
      "deleted_by": null,
      "expires_at": "2021-02-08T09:00:00.000Z",
      "reason": "spam",
      "internal_note": "third time"
    }
  ]
}

section is null for a forum-wide sanction. internal_note is omitted entirely from the object when the reader is not a moderator.

Errors

StatusBodyWhen
403{"error": "current actor does not have the permission to read these sanctions"}—
404{"error": "user not found"}user names nothing.
500{"error": "internal error"}—

POST /sanctions

Hands out a sanction. Answers with the created object.

The permission is judged before the target is looked up, so a caller who may not sanction is refused rather than told whether the user they named exists.

Body

FieldTypeMeaning
useruser referenceWho is sanctioned. {"type": "User", "id": …}, or a username or email reference.
sectionsection id reference or absentOmit for a forum-wide sanction.
kindstring"Warning" or "Mute".
expires_attimestamp or absentWhen a timed mute lapses. Must be absent for a warning.
reasonstringShown to the sanctioned user.
internal_notestringFor moderators. Both texts are required; neither may stand in for the other.

Example

POST /api/v1/forum/sanctions
Content-Type: application/json
Cookie: sid=b8be19ef-2d61-44de-b7d2-9c34ccb8a763

{
  "user": {"type": "User", "id": "28dbb0bf-0fdc-40fe-ae5a-dde193f9fea8"},
  "kind": "Mute",
  "expires_at": "2021-02-08T09:00:00.000Z",
  "reason": "spam",
  "internal_note": "third time"
}

Errors

StatusBodyWhen
400{"error": "a warning cannot expire"}expires_at on a Warning: a pair no actor could mean.
401—Credentials were supplied but could not be resolved.
403{"error": "current actor does not have the permission to sanction this user"}—
404{"error": "user to sanction not found"}—
409{"error": "a live mute already covers this scope"}—
500{"error": "internal error"}—

DELETE /sanctions/:sanction_id

Lifts a sanction, and answers with it.

A DELETE and not a PATCH clearing a field: lifting is the one edit a sanction admits. The row survives it — the service closes its period and records who closed it, so deleted_at and deleted_by are filled in on the object that comes back.

Example

DELETE /api/v1/forum/sanctions/0f3b9b7e-2c1f-4a49-9a1e-0b7d3f9c0a11
Cookie: sid=b8be19ef-2d61-44de-b7d2-9c34ccb8a763

Errors

StatusBodyWhen
401—Credentials were supplied but could not be resolved.
403{"error": "current actor does not have the permission to lift this sanction"}A guest is refused before the sanction is looked up, so the endpoint cannot be used to find out which ids exist.
404{"error": "sanction not found"}—
409{"error": "sanction is already lifted"}Somebody else lifted it first. The end state is the one the caller wanted, but answering 200 would credit them with another moderator's act.
500{"error": "internal error"}—