Files
Kontrollverlust/notes/api.md
T
irrlichtandClaude Opus 5.5 26b0182391 Linkvorschau für den ersten Link eines Beitrags
Der Server holt zum ersten Link Titel, Seiten- bzw. Kanalname und ein
kleines Vorschaubild (320 px) und speichert das Bild lokal; der Browser
lädt nur von uns, Besucher-IPs gehen nie an die verlinkte Seite.
YouTube über oEmbed (alle Linkformen kanonisch als watch?v=<id>), alle
übrigen Seiten über OpenGraph bzw. <title>. Abruf asynchron in einem
Worker, eine Zeile je Link in link_preview, Auffrischen nach 30 Tagen.

Der Abruf-Client lässt nur öffentliche IPs auf Port 80/443 zu, geprüft
nach DNS-Auflösung und bei jeder Weiterleitung (SSRF). Bestehende
Beiträge: kver link-previews. Neue Abhängigkeit golang.org/x/net, damit
go 1.26. DEV bekommt ein beschreibbares Volume für die Vorschaubilder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
2026-10-06 18:47:21 +02:00

7.3 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, Registrierung und Passwort-Reset 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
POST /api/auth/reset/check – token 200 {"username"}; ungültig/abgelaufen/benutzt: 404 reset.invalid
POST /api/auth/reset – token, pass1 (min. 10), pass2 200 {"username"} + Cookie (eingeloggt); löscht alle alten Sessions, Link verbraucht

Login-Fehler: 401 auth.bad_credentials (gleich für unbekannten Nutzer und falsches Passwort).

Reset-Links erzeugt nur der Admin per CLI (kver reset-link <nutzername>, siehe deploy.md). Sie zeigen auf /reset#<token>, gelten 72 h und einmal; ein neuer Link macht ältere desselben Nutzers ungültig. Das Token steht im Fragment und geht nur im POST-Body an die API, damit es nicht in Logs landet.


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/…" }

Enthält der Text einen Link, trägt der Beitrag preview, sobald der Server die Vorschau des ersten Links geholt hat (asynchron nach dem Anlegen bzw. Bearbeiten; vorher und ohne verwertbare Vorschau fehlt das Feld). url ist die kanonische Adresse (YouTube-Links aller Formen als watch?v=<id>), kind ist youtube oder page, site Kanal- bzw. Seitenname, thumb ein lokales Bild (leer, wenn es keins gibt). Siehe preview.go.

"preview": { "url": "https://www.youtube.com/watch?v=…", "kind": "youtube",
             "title": "…", "site": "…", "thumb": "static/media/preview/….jpg" }

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","yes","no","selected"} (selected: yes/no/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 = yes/no 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":{"yes","no"},"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.