banyaro/backend/routes/friends.py
rene 692e6f9378 Sprint 14: Freunde-Seite mit Profildaten (Avatar, Wohnort, Bio, Erfahrungs-Badge)
- Backend: friends-API liefert jetzt bio, wohnort, erfahrung, social_link,
  profil_sichtbarkeit, avatar_url für friends/search/incoming
- Frontend: User-Cards (Suche + Freundesliste) zeigen Avatar-Foto (statt
  Buchstaben-Kreis wenn avatar_url vorhanden), Wohnort mit Pin-Icon,
  Bio-Vorschau (2 Zeilen, max 120 Zeichen, bei private ausgeblendet) und
  Erfahrungs-Badge neben dem Namen
- Profil-Modal erweitert um Wohnort, Erfahrung, vollständige Bio und Social-Link
2026-04-17 09:23:28 +02:00

199 lines
6.7 KiB
Python

"""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,
{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,
(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 u.name LIKE ?
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)
)
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.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}