"""BAN YARO — Freundschaften""" import logging from fastapi import APIRouter, Depends, HTTPException from database import db from auth import get_current_user router = APIRouter() logger = logging.getLogger(__name__) def _dogs_subquery(): """JSON-Array der Hunde eines Users als Subquery.""" return """( SELECT json_group_array(json_object( 'id', d.id, 'name', d.name, 'rasse', d.rasse, 'foto_url',d.foto_url )) FROM dogs d WHERE d.user_id = u.id )""" @router.get("/") async def list_friends(user=Depends(get_current_user)): uid = user["id"] dogs_sq = _dogs_subquery() with db() as conn: friends = conn.execute(f""" SELECT f.id, f.status, f.created_at, CASE WHEN f.requester_id=? THEN f.addressee_id ELSE f.requester_id END AS friend_id, u.name AS friend_name, u.bio, u.wohnort, u.erfahrung, u.social_link, u.profil_sichtbarkeit, u.avatar_url, u.is_founder, u.is_partner, u.founder_number, {dogs_sq} AS dogs_json FROM friendships f JOIN users u ON u.id = CASE WHEN f.requester_id=? THEN f.addressee_id ELSE f.requester_id END WHERE (f.requester_id=? OR f.addressee_id=?) AND f.status='accepted' ORDER BY u.name """, (uid, uid, uid, uid)).fetchall() incoming = conn.execute(f""" SELECT f.id, f.created_at, u.name AS requester_name, u.id AS requester_id, u.avatar_url, {dogs_sq} AS dogs_json FROM friendships f JOIN users u ON u.id=f.requester_id WHERE f.addressee_id=? AND f.status='pending' ORDER BY f.created_at DESC """, (uid,)).fetchall() outgoing = conn.execute(""" SELECT f.id, f.created_at, u.name AS addressee_name, u.id AS addressee_id FROM friendships f JOIN users u ON u.id=f.addressee_id WHERE f.requester_id=? AND f.status='pending' ORDER BY f.created_at DESC """, (uid,)).fetchall() import json def _parse(rows): result = [] for r in rows: d = dict(r) if d.get("dogs_json"): try: d["dogs"] = json.loads(d["dogs_json"]) except Exception: d["dogs"] = [] else: d["dogs"] = [] d.pop("dogs_json", None) result.append(d) return result return { "friends": _parse(friends), "incoming": _parse(incoming), "outgoing": [dict(r) for r in outgoing], } @router.get("/search") async def search_users(q: str = "", user=Depends(get_current_user)): if len(q.strip()) < 2: return [] uid = user["id"] import json with db() as conn: rows = conn.execute(""" SELECT u.id, u.name, u.bio, u.wohnort, u.erfahrung, u.social_link, u.profil_sichtbarkeit, u.avatar_url, u.is_founder, u.is_partner, u.founder_number, (SELECT json_group_array(json_object('name', d.name, 'rasse', d.rasse)) FROM dogs d WHERE d.user_id=u.id AND d.is_public=1) AS dogs_json FROM users u WHERE u.id != ? AND norm(u.name) LIKE norm(?) AND u.profil_sichtbarkeit != 'private' AND NOT EXISTS ( SELECT 1 FROM friendships f WHERE (f.requester_id=? AND f.addressee_id=u.id) OR (f.requester_id=u.id AND f.addressee_id=?) ) LIMIT 20 """, (uid, f"%{q.strip()}%", uid, uid)).fetchall() result = [] for r in rows: d = dict(r) try: d["dogs"] = json.loads(d["dogs_json"]) if d.get("dogs_json") else [] except Exception: d["dogs"] = [] d.pop("dogs_json", None) result.append(d) return result @router.post("/request/{target_id}", status_code=201) async def send_request(target_id: int, user=Depends(get_current_user)): uid = user["id"] if uid == target_id: raise HTTPException(400, "Du kannst dich nicht selbst als Freund hinzufügen.") with db() as conn: if not conn.execute("SELECT 1 FROM users WHERE id=?", (target_id,)).fetchone(): raise HTTPException(404, "Nutzer nicht gefunden.") existing = conn.execute(""" SELECT id, status FROM friendships WHERE (requester_id=? AND addressee_id=?) OR (requester_id=? AND addressee_id=?) """, (uid, target_id, target_id, uid)).fetchone() if existing: if existing["status"] == "accepted": raise HTTPException(400, "Ihr seid bereits befreundet.") raise HTTPException(400, "Anfrage bereits vorhanden.") conn.execute( "INSERT INTO friendships (requester_id, addressee_id) VALUES (?,?)", (uid, target_id) ) # In-App Benachrichtigung + Badge try: with db() as conn: conn.execute( """INSERT INTO notifications (user_id, type, title, body, data) VALUES (?, 'friend_request', 'Neue Freundschaftsanfrage', ?, ?)""", (target_id, f"{user['name']} möchte dein Freund sein.", '{"page":"friends"}') ) except Exception: pass try: from routes.push import send_push_to_user send_push_to_user(target_id, { "title": "Neue Freundschaftsanfrage", "body": f"{user['name']} möchte dein Freund sein.", "type": "friend_request", "data": {"page": "friends"}, }) except Exception: pass return {"ok": True} @router.post("/{friendship_id}/accept") async def accept_request(friendship_id: int, user=Depends(get_current_user)): uid = user["id"] with db() as conn: f = conn.execute( "SELECT * FROM friendships WHERE id=? AND addressee_id=? AND status='pending'", (friendship_id, uid) ).fetchone() if not f: raise HTTPException(404, "Anfrage nicht gefunden.") conn.execute( "UPDATE friendships SET status='accepted', updated_at=datetime('now') WHERE id=?", (friendship_id,) ) return {"ok": True} @router.post("/{friendship_id}/decline") async def decline_request(friendship_id: int, user=Depends(get_current_user)): uid = user["id"] with db() as conn: f = conn.execute(""" SELECT id FROM friendships WHERE id=? AND (addressee_id=? OR requester_id=?) AND status='pending' """, (friendship_id, uid, uid)).fetchone() if not f: raise HTTPException(404, "Anfrage nicht gefunden.") conn.execute("DELETE FROM friendships WHERE id=?", (friendship_id,)) return {"ok": True} @router.get("/activity") async def get_activity(user=Depends(get_current_user)): """Aggregierter Aktivitäts-Feed der Freunde (max. 30 Einträge, neueste zuerst).""" import json uid = user["id"] with db() as conn: # Alle akzeptierten Freunde ermitteln friend_rows = conn.execute(""" SELECT CASE WHEN requester_id=? THEN addressee_id ELSE requester_id END AS fid FROM friendships WHERE (requester_id=? OR addressee_id=?) AND status='accepted' """, (uid, uid, uid)).fetchall() friend_ids = [r["fid"] for r in friend_rows] if not friend_ids: return [] ph = ",".join("?" * len(friend_ids)) # Tagebuch-Einträge der Freunde # Gesundheitseinträge der Freunde (nur Typ + Datum, kein Inhalt) health_rows = conn.execute(f""" SELECT 'health' AS type, u.id AS user_id, u.name AS user_name, u.avatar_url, d.name AS dog_name, d.foto_url AS dog_foto, h.created_at FROM health h JOIN dogs d ON d.id = h.dog_id JOIN users u ON u.id = d.user_id WHERE d.user_id IN ({ph}) ORDER BY h.created_at DESC LIMIT 15 """, friend_ids).fetchall() # Gassi-Treffen der Freunde walk_rows = conn.execute(f""" SELECT 'walk' AS type, w.id AS entry_id, u.id AS user_id, u.name AS user_name, u.avatar_url, NULL AS dog_name, NULL AS dog_foto, w.titel AS text, w.created_at FROM walks w JOIN users u ON u.id = w.user_id WHERE w.user_id IN ({ph}) ORDER BY w.created_at DESC LIMIT 15 """, friend_ids).fetchall() # Forum-Beiträge der Freunde (öffentliche Threads) forum_rows = conn.execute(f""" SELECT 'forum' AS type, ft.id AS entry_id, u.id AS user_id, u.name AS user_name, u.avatar_url, NULL AS dog_name, NULL AS dog_foto, ft.titel AS text, ft.created_at FROM forum_threads ft JOIN users u ON u.id = ft.user_id WHERE ft.user_id IN ({ph}) ORDER BY ft.created_at DESC LIMIT 15 """, friend_ids).fetchall() # Neue Hunde (angelegt in den letzten 30 Tagen) new_dog_rows = conn.execute(f""" SELECT 'new_dog' AS type, u.id AS user_id, u.name AS user_name, u.avatar_url, d.name AS dog_name, d.foto_url AS dog_foto, d.created_at FROM dogs d JOIN users u ON u.id = d.user_id WHERE d.user_id IN ({ph}) AND d.created_at >= datetime('now', '-30 days') ORDER BY d.created_at DESC LIMIT 15 """, friend_ids).fetchall() _ICON = { "diary": "book-open", "health": "heart", "walk": "paw-print", "new_dog": "dog", "forum": "push-pin", } _TEXT = { "health": "Hat einen Gesundheitseintrag hinzugefügt", "new_dog": "Hat einen neuen Hund eingetragen", } items = [] for row in [*health_rows, *walk_rows, *forum_rows, *new_dog_rows]: d = dict(row) t = d["type"] items.append({ "type": t, "entry_id": d.get("entry_id"), "user_id": d["user_id"], "user_name": d["user_name"], "avatar_url": d.get("avatar_url"), "dog_name": d.get("dog_name"), "dog_foto": d.get("dog_foto"), "text": _TEXT.get(t) or (d.get("text") or ""), "created_at": d["created_at"], "icon": _ICON[t], }) # Zusammenführen und nach created_at absteigend sortieren, max. 30 items.sort(key=lambda x: x["created_at"] or "", reverse=True) return items[:50] @router.delete("/{friend_user_id}") async def remove_friend(friend_user_id: int, user=Depends(get_current_user)): uid = user["id"] with db() as conn: conn.execute(""" DELETE FROM friendships WHERE status='accepted' AND ((requester_id=? AND addressee_id=?) OR (requester_id=? AND addressee_id=?)) """, (uid, friend_user_id, friend_user_id, uid)) return {"ok": True}