344 lines
12 KiB
Python
344 lines
12 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,
|
|
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}
|