## Grundlagen (gilt für fast alles) 

  - Basis-Pfad: GET/POST/... /api/v1/...
  - Auth: Authorization: Bearer <token> (Token kommt aus Login). Token werden serverseitig nur als sha256(token) gespeichert; Session ist gültig, wenn nicht revoked und nicht abgelaufen (backend/chatapp/api.py:139).
  - Session-Arten: "normal" und "otp_reset" (/auth/login gibt kind zurück). Mit "otp_reset" sind fast alle Endpoints gesperrt (HTTP 423) – erlaubt sind nur wenige (z.B. /me, Passwort-Reset-Complete, Logout).
  - IDs: uuid4().hex → 32 hex chars (z.B. user_id, conversation_id, message_id, upload_id, attachment_id).
  - Timestamps: int(time.time()) → Unix-Sekunden (created_at, edited_at, …).
  - JSON-Requests: Der Server verlangt bei JSON-Body immer Content-Length; fehlt der Header → HTTP 411 (code="length_required"). Ungültiges JSON → HTTP 400 (code="invalid_json"). Viele Endpoints haben Body-Größenlimits (standard 1_000_000 Bytes; manche abweichend).
  - Antwort-/Fehlerformat (JSON):
      - Erfolg: je Endpoint eigene JSON-Struktur.
      - Fehler: {"error":{"code":"…","message":"…"}} (z.B. 401/403/404/409/423/500).
  - CORS: access-control-allow-origin: *, erlaubte Header: authorization,content-type,x-chunk-sha256, Methoden: GET,POST,PUT,PATCH,DELETE,OPTIONS (backend/chatapp/server.py:94).

  ## Auth & Sessions 

  ### POST /api/v1/auth/register

  - Auth: nein
  - Body (JSON): { "email": string, "username": string, "password": string }
  - Validierung:
      - password max 256 (code="password_too_long").
      - email/username dürfen nicht leer sein (code="invalid_input").
      - Duplikate: HTTP 409 mit code="email_exists" oder code="username_exists".
  - Response: HTTP 201 { "user_id": "<id>" }

  ### POST /api/v1/auth/login

  - Auth: nein
  - Body: { "login": "<email oder username>", "password": "<pass oder OTP>" }
  - Response (normal): HTTP 200 { "token": "<token>", "kind": "normal" }
  - Spezialfall „OTP-Reset“: Wenn users.password_reset_required=1:
      - Passwortfeld wird als OTP interpretiert (Vergleich sha256(otp)).
      - Response: HTTP 200 { "token": "<token>", "kind": "otp_reset", "must_renew_password": true }
      - Fehler: HTTP 423 code="password_reset_required" (kein OTP/benutzt), oder HTTP 423 code="otp_expired".
  - Weitere Fehler: HTTP 401 code="invalid_credentials", HTTP 403 code="banned".

  ### POST /api/v1/auth/logout

  - Auth: ja (auch OTP-Session ok)
  - Body: leer/egal (JSON nicht erforderlich, aber Client sendet typischerweise ohne Body)
  - Response: HTTP 200 { "ok": true } (setzt revoked_at für die Session)

  ### POST /api/v1/auth/reset-password/complete

  - Auth: ja, muss eine Session mit kind="otp_reset" sein
  - Body: { "new_password": string } (max 256)
  - Effekt: setzt neues Passwort, löscht OTP, revoket alle Sessions des Users, erstellt frische "normal"-Session.
  - Response: HTTP 200 { "token": "<token>", "kind": "normal" }
  - Fehler: HTTP 403 code="forbidden" (wenn nicht otp_reset), HTTP 400 code="no_reset_pending".

  ## „Me“ / Settings

  ### GET /api/v1/me

  - Auth: ja (auch OTP-Session ok)
  - Response: HTTP 200
      - { "user": { "id","email","username","is_admin"(0/1),"is_bot"(0/1),"bot_verified"(0/1),"password_reset_required"(0/1),"settings":{"receive_everyone_mentions"(0/1)} }, "session": {"id":"…","kind":"normal|otp_reset"} }

  ### PATCH /api/v1/me/settings

  - Auth: ja (auch OTP-Session ok)
  - Body: { "receive_everyone_mentions": boolean }
  - Response: HTTP 200 { "ok": true }

  ## Conversations

  ### GET /api/v1/conversations

  - Auth: ja (nur normal)
  - Response: HTTP 200 { "conversations": [ { "id","kind"("dm"|"group"),"name", "created_at", "member_count", "last_message_at" } ] }
      - Sortiert nach letztem Message-Timestamp (fallback: created_at).

  ### POST /api/v1/conversations/dm

  - Auth: ja (normal)
  - Body: { "other_user_id": "<id>" }
  - Response:
      - Existiert schon DM zwischen den beiden → HTTP 200 { "conversation_id": "<id>" }
      - Neu angelegt → HTTP 201 { "conversation_id": "<id>" }
  - Fehler: 400 invalid_input (DM zu sich selbst), 404 not_found (User fehlt).

  ### POST /api/v1/conversations/group

  - Auth: ja (normal)
  - Body: { "name": string, "member_ids": [ "<id>", ... ] }
  - Regeln: Name max 100; member_ids muss Liste sein; Creator wird automatisch hinzugefügt; max 25 Mitglieder beim Erstellen.
  - Response: HTTP 201 { "conversation_id": "<id>" }
  - Fehler: 400 missing_field (Name leer), 400 group_too_large, 400 invalid_input (unbekannter/gesperrter User in member_ids).

  ### GET /api/v1/conversations/{conversation_id}

  - Auth: ja (normal) + Mitgliedschaft erforderlich
  - Response: HTTP 200
      - { "conversation": { "id","kind","name","is_admin"(0/1),"is_banned"(0/1) }, "members": [ { "id","username","is_bot","bot_verified","is_admin","is_banned","joined_at" } ] }

  ### POST /api/v1/conversations/{conversation_id}/members

  - Auth: ja (normal) + nur Gruppe + Group-Admin oder global Admin
  - Body: { "user_id": "<id>" }
  - Response: HTTP 200 { "ok": true }
  - Hinweis: Hier wird aktuell kein 25er-Limit erzwungen (nur beim Erstellen).

  ### DELETE /api/v1/conversations/{conversation_id}/members/{user_id}?ban=1

  - Auth: ja (normal) + nur Gruppe + Group-Admin oder global Admin
  - Query: ban optional (1|true|yes)
      - Ohne ban: Mitglied wird entfernt (DELETE Row).
      - Mit ban: Mitglied bleibt in DB, bekommt is_banned=1.
  - Response: HTTP 200 { "ok": true }
  - Fehler: 400 invalid_input (kann sich nicht selbst entfernen).

  ## Messages

  ### GET /api/v1/conversations/{conversation_id}/messages?since=0&limit=50&include_deleted=0

  - Auth: ja (normal) + Mitgliedschaft
  - Query:
      - since (Unix-Sekunden, default 0), es gilt created_at > since (strict).
      - limit (default 50, max 200)
      - include_deleted (wenn wahr, liefert auch gelöschte; Admins sehen gelöschte sowieso)
  - Response: HTTP 200 { "messages": [ { "id","conversation_id","sender_id","kind","text","attachment","is_disappearing","created_at","edited_at","deleted_at","deleted_by","contains_everyone_mention","reactions" } ] }
      - kind: "text"|"image"|"file"|"voice"
      - attachment: null oder { "id","filename","content_type","size","sha256_hex","url":"/api/v1/attachments/<id>" }
      - reactions: [ { "emoji": string, "count": number, "mine": boolean } ]
  - Wichtig wegen Sekundenauflösung: Clients sollten typischerweise since = last_created_at - 1 verwenden, sonst kann man Messages verpassen, die in derselben Sekunde entstanden sind (der Web-Client macht das so).

  ### POST /api/v1/conversations/{conversation_id}/messages

  - Auth: ja (normal) + Mitgliedschaft
  - Body (max ~2MB):
      - Text: { "kind":"text", "text": string, "is_disappearing"?: bool }
      - Mit Attachment: { "kind":"image"|"file"|"voice", "attachment_id":"<id>", "is_disappearing"?: bool }
  - Effekt: schreibt Message, setzt contains_everyone_mention=1, wenn Group + Text enthält substring "@everyone", queued Push-Events.
  - Response: HTTP 201 { "message_id":"<id>" }

  ### PATCH /api/v1/messages/{message_id}

  - Auth: ja (normal)
  - Body: { "text": string }
  - Regeln: nur Sender, nur kind="text", nicht gelöscht.
  - Response: HTTP 200 { "ok": true }
  - Fehler: 409 conflict (gelöscht), 403 forbidden, 400 invalid_input (nicht Text), 404 not_found.

  ### DELETE /api/v1/messages/{message_id}

  - Auth: ja (normal)
  - Regeln: Sender oder global Admin; in Gruppen auch Group-Admin. Löscht nicht „hart“, sondern setzt deleted_at/deleted_by + Audit.
  - Response: HTTP 200 { "ok": true } (auch wenn schon gelöscht)

  ### POST /api/v1/messages/{message_id}/reactions

  - Auth: ja (normal) + Mitgliedschaft in der Conversation der Message
  - Body: { "emoji": string, "action": "add"|"remove" } (emoji wird auf 16 Zeichen gekappt)
  - Response: HTTP 200 { "ok": true }

  ## Uploads & Attachments (Chunked, ohne multipart)

  Default-Limits aus backend/chatapp/config.py:34:

  - CHATAPP_MAX_ATTACHMENT_BYTES default 1_000_000_000 (≈ 1GB)
  - CHATAPP_UPLOAD_CHUNK_BYTES default 50 MiB (50*1024*1024)

  ### POST /api/v1/uploads

  - Auth: ja (normal)
  - Body: { "filename": string, "total_size": int, "overall_sha256_hex": "<64 hex>", "content_type"?: string, "chunk_size"?: int, "num_chunks"?: int }
  - Regeln: chunk_size darf max upload_chunk_bytes sein; num_chunks muss exakt ceil(total_size/chunk_size) sein.
  - Response: HTTP 201 { "upload_id":"<id>", "chunk_size": int, "num_chunks": int }

  ### GET /api/v1/uploads/{upload_id}

  - Auth: ja (normal), nur Owner
  - Response: HTTP 200 { "upload": { "id","state","created_at","finalized_at" }, "received_chunks":[0,2,3,...] }

  ### PUT /api/v1/uploads/{upload_id}/chunks/{chunk_index}

  - Auth: ja (normal), nur Owner
  - Headers: X-Chunk-Sha256: <64 hex> und Content-Length: <bytes>
  - Body: rohe Chunk-Bytes (max chunk_size)
  - Response: HTTP 200 { "ok": true } oder { "ok": true, "dedup": true } wenn Chunk schon identisch vorhanden
  - Fehler: 400 hash_mismatch, 409 conflict (Chunk existiert mit anderer Hash / Upload nicht aktiv / missing chunks später), 413 payload_too_large.

  ### POST /api/v1/uploads/{upload_id}/finalize

  - Auth: ja (normal), nur Owner
  - Effekt: prüft Vollständigkeit, setzt Gesamt-Hash/Size durch Reassemble, schreibt Attachment in Storage, markiert Upload finalized.
  - Response: HTTP 200 { "attachment_id":"<id>", "url":"/api/v1/attachments/<id>" }
  - Fehler: 409 missing_chunks / conflict, 400 hash_mismatch.

  ### GET /api/v1/attachments/{attachment_id}?download=1

  - Auth: ja (normal)
  - Berechtigung: Admin immer; sonst nur wenn Attachment in einer Message in einer Conversation hängt, in der der User aktuell einen conversation_members-Eintrag hat.
  - Response: Datei-Stream mit Content-Type, Content-Disposition (inline für Images außer download=1, sonst attachment).

  ## Push (nur Registrierung + Queueing)

  Beim Message-Send werden Push-Events in DB queued (Tabelle push_events), es gibt aber keinen API-Endpoint zum Ausliefern (Worker out-of-scope).

  ### POST /api/v1/push/unifiedpush/register

  - Auth: ja (normal)
  - Body: { "endpoint": string }
  - Response: HTTP 201 { "device_id":"<id>" }

  ### POST /api/v1/push/webpush/subscribe

  - Auth: ja (normal)
  - Body: { "endpoint": string, "p256dh": string, "auth": string }
  - Response: HTTP 201 { "subscription_id":"<id>" }

  Push-Payload (queued) enthält mindestens: { "preview": "<max 12 chars bei Text>", "conversation_id":"…", "message_id":"…" } und optional "mention":"everyone" für Empfänger, die receive_everyone_mentions=1 gesetzt haben.

  ## Admin API

  Alles außer Bootstrap: Auth erforderlich und is_admin=1.

  ### POST /api/v1/admin/bootstrap

  - Auth: nein
  - Body: { "email","username","password" }
  - Nur möglich wenn noch kein Admin existiert, sonst 403 bootstrap_closed.
  - Response: HTTP 201 { "ok": true }

  ### GET /api/v1/admin/users

  - Response: { "users": [ { "id","email","username","is_admin","is_bot","bot_verified","is_banned","ban_reason","warned_reason","warned_at","created_at" } ] }

  ### POST /api/v1/admin/users/{user_id}/ban

  - Body: { "banned"?: boolean (default true), "reason"?: string }
  - Effekt: setzt Ban-Flag; bei Ban werden alle Sessions des Users revoked.
  - Response: { "ok": true }

  ### POST /api/v1/admin/users/{user_id}/warn

  - Body: { "reason": string } (required, nicht leer)
  - Response: { "ok": true }

  ### POST /api/v1/admin/users/{user_id}/otp-reset

  - Body: {} (wird ignoriert)
  - Response: { "otp":"<32 chars>", "expires_at": <unix seconds> }

  ### GET /api/v1/admin/groups

  - Response: { "groups": [ { "id","kind","name","created_at","member_count" } ] } (nur kind='group')

  ### GET /api/v1/admin/logs?limit=200

  - limit: 1..500
  - Response: { "logs": [ { "id","level","event","created_at","details"?:object|null } ] }

  ### GET /api/v1/admin/metrics

  - Response: { "msg_per_min": int, "upload_bytes_per_min": int, "active_users": int, "online_devices": int, "counters":[{"key","value","updated_at"}] }

  ### POST /api/v1/admin/bots / POST /api/v1/admin/bots/{bot_id}/verify

  - Create Body: { "email","username","password" } → { "bot_id":"<id>" }
  - Verify → { "ok": true }

  ### Integrations

  - GET /api/v1/admin/integrations → { "integrations":[ { "id","kind","name","bot_user_id","target_conversation_id","enabled","config","created_at","updated_at" } ] }
  - POST /api/v1/admin/integrations Body: { "kind":"twitch|modrinth","name","bot_user_id","target_conversation_id","config", "enabled"?:bool } → { "integration_id":"<id>" }
  - PATCH /api/v1/admin/integrations/{integration_id} Body: { "enabled"?:bool, "config"?:any } → { "ok": true }
  - POST /api/v1/admin/integrations/{integration_id}/test-fire Body: { "text"?: string } → erstellt Bot-Textmessage → { "message_id":"<id>" }
