5.9 KiB
kver API
Alle Endpunkte liegen unter /api und antworten ausschließlich mit JSON.
Außerhalb von /api liefert das Backend nur Seiten und Dateien des Frontends.
Konventionen
Erfolg oder Fehler steht im HTTP-Status. 2xx heißt Erfolg, alles andere
Fehler. Einen 200 mit Fehler im Body gibt es nicht. Fehler haben immer diese
Form:
{ "error": { "code": "entry.not_found", "message": "Beitrag nicht gefunden",
"field": "content", "fields": { "user": "…" } } }
code: stabil, maschinenlesbar (<bereich>.<sache>), für Logikmessage: deutsche Meldung für Menschenfield/fields: optional, betroffene Formularfelder (fieldsnur bei der Registrierung, die alle Feldfehler auf einmal meldet)
| Status | Bedeutung |
|---|---|
400 |
Eingabe ungültig |
401 |
nicht angemeldet / falsche Zugangsdaten |
403 |
angemeldet, aber nicht erlaubt (fremder Beitrag, falsches Passwort, TOR, fremder Origin) |
404 |
Objekt oder Endpunkt existiert nicht (api.not_found) |
405 |
Endpunkt existiert, aber nicht mit dieser Methode (api.method_not_allowed) |
409 |
Konflikt (Nutzername vergeben) |
413 |
Upload zu groß |
429 |
zu viele Anfragen (rate.limited) oder Repost-Cooldown |
500 |
Serverfehler (internal, Ursache nur im Log) |
Requests sind application/x-www-form-urlencoded bzw. multipart/form-data
(Uploads). Zeitstempel sind Unix-Sekunden.
Auth läuft über das Cookie session (HttpOnly, SameSite=Lax). Geschützte
Endpunkte antworten ohne gültige Session mit 401 auth.required.
CSRF: Nicht-GET-Requests mit fremdem Origin-Header werden mit
403 csrf.bad_origin abgewiesen.
Rate-Limits (pro IP): Login und Registrierung 20/min.
Auth
| Methode | Pfad | Auth | Request | Erfolg |
|---|---|---|---|---|
| POST | /api/auth/login |
– | user, pass, timeout (s, Default 1 Tag, max. 1 Jahr) |
200 {"status":"ok"} + Cookie; bereits angemeldet: 200 {"status":"already_logged_in"} |
| POST | /api/auth/newuser |
– | user (3–32 Zeichen), pass1 (min. 10), pass2 |
201 {"username"} |
| POST | /api/auth/logout |
– | – | 200 {"status":"ok"}, löscht Cookie |
| GET | /api/auth/sessioninfo |
✓ | – | 200 {"uid","created_at","expires","description"}, ohne Session 401 |
Login-Fehler: 401 auth.bad_credentials (gleich für unbekannten Nutzer und
falsches Passwort).
Beiträge
Ein Beitrag (Entry):
{ "pid": 123456789, "uid": 42, "created_at": 1700000000, "content": "…",
"filepath": "static/media/…", "reply_to": 0, "reply_count": 3,
"last_activity": 1700000500, "deleted": 0, "bump_count": 1,
"last_bump": 1700000400, "username": "max", "avatar": "static/media/…" }
In Hauptfeed und replies des Threads trägt jeder Beitrag mit Antworten
zusätzlich latest: den jüngsten nicht gelöschten Beitrag aus seinem ganzen
Teilbaum (nach created_at, höchstens 50 Ebenen tief). skipped zählt die
Beiträge auf dem Pfad dazwischen, gelöschte eingeschlossen (0 = direkte
Antwort). Ohne passenden Nachfahren fehlt das Feld.
"latest": { "entry": Entry, "skipped": 2 }
| Methode | Pfad | Auth | Request | Erfolg |
|---|---|---|---|---|
| GET | /api/entry/feed/{page} |
– | – | 200 [Entry] mit latest: Threads nach letzter Aktivität, 20 pro Seite, Seite 0–100, [] am Ende |
| GET | /api/entry/{pid}/thread |
– | – | 200 {"entry", "ancestors": [Entry], "replies": [Entry]}; ancestors Root zuerst, replies (max. 100) nach letzter Aktivität, mit latest |
| GET | /api/entry/{pid}/votes |
– | – | 200 {"pid","left","right","selected"} (selected: left/right/none) |
| POST | /api/entry/create |
✓ | multipart: content (max. 1000), file (optional), reply_to (optional) |
201 {"pid","content","filepath","reply_to"} |
| POST | /api/entry/{pid}/edit |
✓ Autor | content |
200 {"pid","content"} |
| POST | /api/entry/{pid}/delete |
✓ Autor | – | 200 {"status":"deleted"} (Soft-Delete, Thread bleibt) |
| POST | /api/entry/{pid}/vote |
✓ | mode = left/right |
200 wie votes. Gleiche Stimme erneut zieht sie zurück |
| POST | /api/entry/{pid}/bump |
✓ | – | 200 {"pid","bump_count","last_bump","last_activity","retry_after"} |
Beim Bump wächst der Cooldown mit jedem Repost um einen Tag. Im Cooldown
antwortet der Endpunkt mit 429 entry.bump_cooldown. Dazu kommen der Header
Retry-After und im Body retry_after und bump_count (neben error).
TOR-Anfragen auf create werden mit 403 tor.blocked abgewiesen.
Nutzer
| Methode | Pfad | Auth | Request | Erfolg |
|---|---|---|---|---|
| GET | /api/u/{username}/info |
– | – | 200 {"uid","username","created_at","avatar"} |
| GET | /api/u/{username}/feed/{page} |
– | – | 200 [Entry]: alle Beiträge inkl. Antworten, chronologisch |
| GET | /api/user/info |
✓ | – | 200 {"uid","username","created_at","last_login","avatar"} |
| POST | /api/user/rename |
✓ | user |
200 {"username"}; vergeben: 409 user.name_taken |
| POST | /api/user/avatar |
✓ | multipart: avatar |
200 {"avatar"} |
| POST | /api/user/delete |
✓ | pass1 |
200 {"status":"deleted"}, löscht Konto, Beiträge, Votes, Sessions; falsches Passwort: 403 auth.bad_password |
Statistik
| Methode | Pfad | Erfolg |
|---|---|---|
| GET | /api/stats |
200 {"users","entries"} |
| GET | /api/stats/detail |
200 {"users","entries","toplevel","replies","votes":{"left","right"},"impressions_total","visitors_total","daily":[…],"top_asns":[…]} |
Seiten (kein JSON)
/, /e/{pid}, /u/{username} liefern die HTML-Seiten des Frontends und
zählen anonymisierte Impressions. /static/* enthält Uploads und Dokumente,
alles andere kommt aus web/.
Datenbankschema
Postgres; maßgeblich ist die Konstante schema in db.go.