"""Freunde: Anfrage schicken, annehmen, ablehnen, entfernen. Freunde sind die Grundlage fürs Teilen: Vorratslisten und Kanban-Projekte lassen sich nur mit Freunden teilen, auf der Reise-Karte erscheinen nur Städte von Freunden. Administratoren gelten diese Einschränkungen nicht. """ from fastapi import APIRouter, Depends, HTTPException from .auth import current_user from .common import now_iso from .db import transaction router = APIRouter() MIN_SEARCH = 2 def _pair(a: int, b: int) -> tuple: return (a, b) if a < b else (b, a) def friend_ids(conn, user_id: int) -> set: """IDs aller angenommenen Freunde.""" rows = conn.execute( "SELECT user_a, user_b FROM friendships WHERE accepted = 1 AND (user_a = ? OR user_b = ?)", (user_id, user_id) ) return {r["user_b"] if r["user_a"] == user_id else r["user_a"] for r in rows} def shareable(conn, user: dict) -> list: """Konten, mit denen man teilen darf: Freunde – Administratoren alle (außer sich selbst).""" rows = conn.execute("SELECT id, display_name FROM users WHERE id != ? ORDER BY display_name COLLATE NOCASE", (user["id"],)) allowed = None if user["isAdmin"] else friend_ids(conn, user["id"]) return [{"id": r["id"], "name": r["display_name"]} for r in rows if allowed is None or r["id"] in allowed] def check_shareable(conn, user: dict, ids, keep=()) -> None: """Neue Mitglieder müssen Freunde sein (Admins: beliebig). keep: schon eingetragene Mitglieder dürfen bleiben.""" allowed = {u["id"] for u in shareable(conn, user)} | set(keep) if any(i not in allowed for i in ids): raise HTTPException(400, "Teilen geht nur mit Freunden – bitte zuerst unter „Freunde“ hinzufügen.") def _person(row) -> dict: return {"id": row["id"], "name": row["display_name"], "username": row["username"]} def _state(conn, user_id: int) -> dict: rows = conn.execute( """SELECT u.id, u.username, u.display_name, f.accepted, f.requested_by FROM friendships f JOIN users u ON u.id = CASE WHEN f.user_a = ? THEN f.user_b ELSE f.user_a END WHERE f.user_a = ? OR f.user_b = ? ORDER BY u.display_name COLLATE NOCASE""", (user_id, user_id, user_id), ).fetchall() return { "friends": [_person(r) for r in rows if r["accepted"]], "incoming": [_person(r) for r in rows if not r["accepted"] and r["requested_by"] != user_id], "outgoing": [_person(r) for r in rows if not r["accepted"] and r["requested_by"] == user_id], } @router.get("/api/freunde") def get_friends(user: dict = Depends(current_user)): with transaction() as conn: return _state(conn, user["id"]) @router.get("/api/freunde/suche") def search_users(q: str = "", user: dict = Depends(current_user)): """Konten nach Name oder Benutzername suchen (ab 2 Zeichen), mit Stand der Freundschaft.""" q = q.strip() if len(q) < MIN_SEARCH: return [] like = "%" + q.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%" with transaction() as conn: rows = conn.execute( """SELECT id, username, display_name FROM users WHERE id != ? AND (display_name LIKE ? ESCAPE '\\' OR username LIKE ? ESCAPE '\\') ORDER BY display_name COLLATE NOCASE LIMIT 20""", (user["id"], like, like), ).fetchall() state = _state(conn, user["id"]) status = {p["id"]: s for s, key in (("freund", "friends"), ("angefragt", "outgoing"), ("eingeladen", "incoming")) for p in state[key]} return [{**_person(r), "status": status.get(r["id"])} for r in rows] @router.post("/api/freunde/{other_id}") def befriend(other_id: int, user: dict = Depends(current_user)): """Anfrage schicken – oder eine offene Anfrage der anderen Person annehmen.""" if other_id == user["id"]: raise HTTPException(400, "Mit sich selbst befreundet sein geht nicht.") a, b = _pair(user["id"], other_id) with transaction() as conn: if not conn.execute("SELECT 1 FROM users WHERE id = ?", (other_id,)).fetchone(): raise HTTPException(404, "Konto nicht gefunden.") row = conn.execute("SELECT * FROM friendships WHERE user_a = ? AND user_b = ?", (a, b)).fetchone() if not row: conn.execute( "INSERT INTO friendships (user_a, user_b, requested_by, accepted, created_at) VALUES (?, ?, ?, 0, ?)", (a, b, user["id"], now_iso()), ) elif not row["accepted"] and row["requested_by"] != user["id"]: conn.execute("UPDATE friendships SET accepted = 1 WHERE user_a = ? AND user_b = ?", (a, b)) return _state(conn, user["id"]) @router.delete("/api/freunde/{other_id}") def unfriend(other_id: int, user: dict = Depends(current_user)): """Anfrage zurückziehen oder ablehnen bzw. Freundschaft beenden.""" a, b = _pair(user["id"], other_id) with transaction() as conn: conn.execute("DELETE FROM friendships WHERE user_a = ? AND user_b = ?", (a, b)) return _state(conn, user["id"])