/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:
| Kind | Meaning |
|---|---|
Warning | A record of an event. It never expires — there is no state to lapse. |
Mute | The 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
| Name | Type | Default | Meaning |
|---|---|---|---|
user | UUID | — | Only the sanctions of this account. |
section | UUID | — | Only the sanctions scoped to this section. An identifier, not a key. |
active | boolean | false | true keeps only the sanctions in force at the moment of the request. |
offset | integer | 0 | — |
limit | integer | threads_per_page | Clamped 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
| Status | Body | When |
|---|---|---|
| 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
| Field | Type | Meaning |
|---|---|---|
user | user reference | Who is sanctioned. {"type": "User", "id": …}, or a username or email reference. |
section | section id reference or absent | Omit for a forum-wide sanction. |
kind | string | "Warning" or "Mute". |
expires_at | timestamp or absent | When a timed mute lapses. Must be absent for a warning. |
reason | string | Shown to the sanctioned user. |
internal_note | string | For 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
| Status | Body | When |
|---|---|---|
| 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
| Status | Body | When |
|---|---|---|
| 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"} | — |