Files
Kontrollverlust/notes/api.md
T
irrlichtandClaude Opus 5.5 3d2447fe35 Melden rechts in der Byline jeder Karte, auch anonym
- Melden-Knopf in der Byline aller Beiträge (außer eigenen), Grund-Formular
  darunter; ersetzt den Knopf unter dem fokussierten Beitrag
- /api/entry/{pid}/report öffentlich, 10/min pro IP; ohne Session anonym
  (uid 0), Doppelmeldungen nur für Angemeldete unterdrückt
- Klicks in Formularen öffnen die Karte nicht mehr

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
2026-09-29 18:05:54 +02:00

6.1 KiB
Raw Blame History

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 Logik
  • message: deutsche Meldung für Menschen
  • field / fields: optional, betroffene Formularfelder (fields nur 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/{pid}/report – reason (optional, max. 500) 200 {"status":"reported"}. Ohne Session anonym (uid 0); Angemeldete legen keine zweite offene Meldung an; gelöschte Beiträge 404; 10/min pro IP
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.