"""BAN YARO — Playdate-Matching""" import logging from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import Optional from database import db from auth import get_current_user from math_utils import haversine_km router = APIRouter() logger = logging.getLogger(__name__) def _calc_alter(geburtstag: Optional[str]) -> Optional[str]: """Gibt lesbares Alter zurück z.B. '2 Jahre' oder '5 Monate'.""" if not geburtstag: return None try: from datetime import date geb = date.fromisoformat(geburtstag[:10]) today = date.today() monate = (today.year - geb.year) * 12 + (today.month - geb.month) if today.day < geb.day: monate -= 1 if monate < 0: return None if monate < 24: return f"{monate} {'Monat' if monate == 1 else 'Monate'}" jahre = monate // 12 return f"{jahre} {'Jahr' if jahre == 1 else 'Jahre'}" except Exception: return None # ------------------------------------------------------------------ # Schemas # ------------------------------------------------------------------ class ListingUpsert(BaseModel): dog_id: int lat: float lon: float ort_name: Optional[str] = None radius_km: int = 10 beschreibung: Optional[str] = None class RequestCreate(BaseModel): to_dog_id: int nachricht: Optional[str] = None class RequestPatch(BaseModel): status: str # accepted | declined # ------------------------------------------------------------------ # Helpers — Konversation für Playdate öffnen (ohne Freundschaftspflicht) # ------------------------------------------------------------------ def _ensure_conversation(conn, user_a: int, user_b: int) -> int: a, b = (min(user_a, user_b), max(user_a, user_b)) existing = conn.execute( "SELECT id FROM conversations WHERE user_a_id=? AND user_b_id=?", (a, b) ).fetchone() if existing: return existing["id"] cur = conn.execute( "INSERT INTO conversations (user_a_id, user_b_id) VALUES (?,?)", (a, b) ) return cur.lastrowid # ------------------------------------------------------------------ # Routes # ------------------------------------------------------------------ @router.get("/nearby") async def nearby(lat: float, lon: float, radius: int = 10, user=Depends(get_current_user)): uid = user["id"] with db() as conn: rows = conn.execute(""" SELECT pl.id AS listing_id, pl.lat, pl.lon, pl.ort_name, pl.beschreibung, d.id AS dog_id, d.name AS dog_name, d.rasse, d.geburtstag, d.foto_url, d.geschlecht FROM playdate_listings pl JOIN dogs d ON d.id = pl.dog_id WHERE pl.aktiv = 1 AND pl.user_id != ? """, (uid,)).fetchall() result = [] for r in rows: dist = haversine_km(lat, lon, r["lat"], r["lon"]) if dist <= radius: result.append({ "listing_id": r["listing_id"], "dog_id": r["dog_id"], "dog_name": r["dog_name"], "rasse": r["rasse"], "alter": _calc_alter(r["geburtstag"]), "geschlecht": r["geschlecht"], "foto_url": r["foto_url"], "ort_name": r["ort_name"], "beschreibung": r["beschreibung"], "entfernung_km": round(dist, 1), }) result.sort(key=lambda x: x["entfernung_km"]) return result @router.put("/listing", status_code=200) async def upsert_listing(data: ListingUpsert, user=Depends(get_current_user)): uid = user["id"] with db() as conn: # Sicherstellen dass der Hund dem User gehört dog = conn.execute( "SELECT id FROM dogs WHERE id=? AND user_id=?", (data.dog_id, uid) ).fetchone() if not dog: raise HTTPException(404, "Hund nicht gefunden.") existing = conn.execute( "SELECT id FROM playdate_listings WHERE dog_id=?", (data.dog_id,) ).fetchone() if existing: conn.execute(""" UPDATE playdate_listings SET lat=?, lon=?, ort_name=?, radius_km=?, beschreibung=?, aktiv=1, updated_at=datetime('now') WHERE dog_id=? """, (data.lat, data.lon, data.ort_name, data.radius_km, data.beschreibung, data.dog_id)) return {"ok": True, "id": existing["id"]} else: cur = conn.execute(""" INSERT INTO playdate_listings (dog_id, user_id, lat, lon, ort_name, radius_km, beschreibung) VALUES (?,?,?,?,?,?,?) """, (data.dog_id, uid, data.lat, data.lon, data.ort_name, data.radius_km, data.beschreibung)) return {"ok": True, "id": cur.lastrowid} @router.delete("/listing/{dog_id}", status_code=200) async def deactivate_listing(dog_id: int, user=Depends(get_current_user)): uid = user["id"] with db() as conn: row = conn.execute( "SELECT id FROM playdate_listings WHERE dog_id=? AND user_id=?", (dog_id, uid) ).fetchone() if not row: raise HTTPException(404, "Inserat nicht gefunden.") conn.execute( "UPDATE playdate_listings SET aktiv=0, updated_at=datetime('now') WHERE dog_id=?", (dog_id,) ) return {"ok": True} @router.get("/my-listing/{dog_id}") async def my_listing(dog_id: int, user=Depends(get_current_user)): uid = user["id"] with db() as conn: row = conn.execute( """SELECT id, dog_id, lat, lon, ort_name, radius_km, beschreibung, aktiv FROM playdate_listings WHERE dog_id=? AND user_id=?""", (dog_id, uid) ).fetchone() if not row: return None return dict(row) @router.post("/request", status_code=201) async def create_request(data: RequestCreate, user=Depends(get_current_user)): uid = user["id"] with db() as conn: # Eigenen Hund ermitteln — nimm den ersten aktiven Hund des Users own_dog = conn.execute( "SELECT id FROM dogs WHERE user_id=? ORDER BY id LIMIT 1", (uid,) ).fetchone() if not own_dog: raise HTTPException(400, "Du hast noch keinen Hund eingetragen.") from_dog_id = own_dog["id"] # Zielhund + Besitzer prüfen target = conn.execute( "SELECT d.id, d.user_id FROM dogs d WHERE d.id=?", (data.to_dog_id,) ).fetchone() if not target: raise HTTPException(404, "Zielhund nicht gefunden.") if target["user_id"] == uid: raise HTTPException(400, "Du kannst nicht dir selbst eine Anfrage schicken.") to_user_id = target["user_id"] # Doppelte Anfrage verhindern existing = conn.execute( "SELECT id, status FROM playdate_requests WHERE from_dog_id=? AND to_dog_id=?", (from_dog_id, data.to_dog_id) ).fetchone() if existing: if existing["status"] == "pending": raise HTTPException(409, "Du hast bereits eine offene Anfrage an diesen Hund.") # Alte abgelehnte Anfrage: löschen und neu anlegen conn.execute( "DELETE FROM playdate_requests WHERE id=?", (existing["id"],) ) cur = conn.execute(""" INSERT INTO playdate_requests (from_dog_id, to_dog_id, from_user_id, to_user_id, nachricht) VALUES (?,?,?,?,?) """, (from_dog_id, data.to_dog_id, uid, to_user_id, data.nachricht)) request_id = cur.lastrowid # Chat-Konversation anlegen (ohne Freundschaftspflicht) conv_id = _ensure_conversation(conn, uid, to_user_id) # Erste Nachricht mit Kontext senden intro = f"Hallo! Ich habe eine Playdate-Anfrage für unsere Hunde geschickt." if data.nachricht: intro += f" Meine Nachricht: {data.nachricht}" conn.execute(""" INSERT INTO direct_messages (conversation_id, sender_id, text) VALUES (?,?,?) """, (conv_id, uid, intro)) conn.execute( "UPDATE conversations SET last_msg_at=datetime('now') WHERE id=?", (conv_id,) ) try: from routes.push import send_push_to_user send_push_to_user(to_user_id, { "title": "Playdate-Anfrage", "body": f"{user['name']} möchte ein Treffen vereinbaren!", "type": "playdate_request", "tag": f"playdate-{request_id}", "data": {"page": "playdate"}, }) except Exception: pass return {"ok": True, "request_id": request_id, "conversation_id": conv_id} @router.get("/requests") async def list_requests(user=Depends(get_current_user)): uid = user["id"] with db() as conn: incoming = conn.execute(""" SELECT pr.id, pr.status, pr.nachricht, pr.created_at, pr.from_user_id, uf.name AS from_user_name, df.name AS from_dog_name, df.rasse AS from_dog_rasse, df.foto_url AS from_dog_foto, df.geburtstag AS from_dog_geburtstag, dt.name AS to_dog_name FROM playdate_requests pr JOIN users uf ON uf.id = pr.from_user_id JOIN dogs df ON df.id = pr.from_dog_id JOIN dogs dt ON dt.id = pr.to_dog_id WHERE pr.to_user_id = ? ORDER BY pr.created_at DESC """, (uid,)).fetchall() outgoing = conn.execute(""" SELECT pr.id, pr.status, pr.nachricht, pr.created_at, pr.to_user_id, ut.name AS to_user_name, dt.name AS to_dog_name, dt.rasse AS to_dog_rasse, dt.foto_url AS to_dog_foto, df.name AS from_dog_name FROM playdate_requests pr JOIN users ut ON ut.id = pr.to_user_id JOIN dogs dt ON dt.id = pr.to_dog_id JOIN dogs df ON df.id = pr.from_dog_id WHERE pr.from_user_id = ? ORDER BY pr.created_at DESC """, (uid,)).fetchall() def _enrich(rows, direction): result = [] for r in rows: d = dict(r) d["direction"] = direction if direction == "incoming": d["alter"] = _calc_alter(d.get("from_dog_geburtstag")) result.append(d) return result return { "incoming": _enrich(incoming, "incoming"), "outgoing": _enrich(outgoing, "outgoing"), } @router.patch("/requests/{req_id}", status_code=200) async def patch_request(req_id: int, data: RequestPatch, user=Depends(get_current_user)): uid = user["id"] if data.status not in ("accepted", "declined"): raise HTTPException(400, "Status muss 'accepted' oder 'declined' sein.") with db() as conn: req = conn.execute( "SELECT * FROM playdate_requests WHERE id=? AND to_user_id=?", (req_id, uid) ).fetchone() if not req: raise HTTPException(404, "Anfrage nicht gefunden.") if req["status"] != "pending": raise HTTPException(409, "Anfrage wurde bereits beantwortet.") conn.execute( "UPDATE playdate_requests SET status=? WHERE id=?", (data.status, req_id) ) conv_id = None if data.status == "accepted": conv_id = _ensure_conversation(conn, uid, req["from_user_id"]) try: from routes.push import send_push_to_user verb = "angenommen" if data.status == "accepted" else "abgelehnt" send_push_to_user(req["from_user_id"], { "title": f"Playdate {verb}!", "body": f"{user['name']} hat deine Anfrage {verb}.", "type": "playdate_response", "tag": f"playdate-{req_id}", "data": {"page": "playdate"}, }) except Exception: pass return {"ok": True, "conversation_id": conv_id}