"""BAN YARO — Hunde-Profil Routes""" import os import uuid from fastapi import APIRouter, Depends, HTTPException, UploadFile, File from pydantic import BaseModel from typing import Optional from database import db from auth import get_current_user from routes.push import send_push_to_user router = APIRouter() MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") class DogCreate(BaseModel): name: str rasse: Optional[str] = None geburtstag: Optional[str] = None geschlecht: Optional[str] = None gewicht_kg: Optional[float] = None chip_nr: Optional[str] = None bio: Optional[str] = None is_public: bool = False class DogUpdate(BaseModel): name: Optional[str] = None rasse: Optional[str] = None geburtstag: Optional[str] = None geschlecht: Optional[str] = None gewicht_kg: Optional[float] = None chip_nr: Optional[str] = None bio: Optional[str] = None is_public: Optional[bool] = None @router.get("") async def list_dogs(user=Depends(get_current_user)): with db() as conn: own = conn.execute( "SELECT *, NULL AS shared_by, NULL AS share_role FROM dogs WHERE user_id=? ORDER BY id", (user["id"],) ).fetchall() shared = conn.execute( """SELECT d.*, u.name AS shared_by, ds.role AS share_role FROM dog_shares ds JOIN dogs d ON d.id = ds.dog_id JOIN users u ON u.id = ds.owner_id WHERE ds.shared_with_id = ? AND ds.accepted_at IS NOT NULL""", (user["id"],) ).fetchall() guest_rows = conn.execute(""" SELECT d.*, ss.id AS sub_id, ss.valid_until AS sitting_until, u.name AS owner_name, NULL AS shared_by, NULL AS share_role FROM sitting_subscriptions ss JOIN dogs d ON d.id = ss.dog_id JOIN users u ON u.id = ss.owner_id WHERE ss.sitter_id = ? AND ss.valid_until >= date('now') """, (user["id"],)).fetchall() result = [] for r in own: d = dict(r) d["is_guest"] = False result.append(d) for r in shared: d = dict(r) d["is_guest"] = False result.append(d) for r in guest_rows: d = dict(r) d["is_guest"] = True result.append(d) return result @router.post("") async def create_dog(data: DogCreate, user=Depends(get_current_user)): with db() as conn: conn.execute( """INSERT INTO dogs (user_id, name, rasse, geburtstag, geschlecht, gewicht_kg, chip_nr, bio, is_public) VALUES (?,?,?,?,?,?,?,?,?)""", (user["id"], data.name, data.rasse, data.geburtstag, data.geschlecht, data.gewicht_kg, data.chip_nr, data.bio, int(data.is_public)) ) dog = conn.execute( "SELECT * FROM dogs WHERE user_id=? ORDER BY id DESC LIMIT 1", (user["id"],) ).fetchone() return dict(dog) @router.get("/{dog_id}") async def get_dog(dog_id: int, user=Depends(get_current_user)): with db() as conn: dog = conn.execute( "SELECT * FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"]) ).fetchone() if not dog: raise HTTPException(404, "Hund nicht gefunden.") return dict(dog) @router.patch("/{dog_id}") async def update_dog(dog_id: int, data: DogUpdate, user=Depends(get_current_user)): fields = {k: v for k, v in data.model_dump().items() if v is not None} if not fields: raise HTTPException(400, "Keine Änderungen angegeben.") set_clause = ", ".join(f"{k}=?" for k in fields) values = list(fields.values()) + [dog_id, user["id"]] with db() as conn: conn.execute( f"UPDATE dogs SET {set_clause} WHERE id=? AND user_id=?", values ) dog = conn.execute( "SELECT * FROM dogs WHERE id=?", (dog_id,) ).fetchone() return dict(dog) @router.delete("/{dog_id}", status_code=204) async def delete_dog(dog_id: int, user=Depends(get_current_user)): with db() as conn: conn.execute( "DELETE FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"]) ) @router.post("/{dog_id}/photo") async def upload_photo( dog_id: int, file: UploadFile = File(...), user=Depends(get_current_user) ): # Hund gehört dem User? with db() as conn: dog = conn.execute( "SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"]) ).fetchone() if not dog: raise HTTPException(404, "Hund nicht gefunden.") # Datei immer als JPEG speichern (HEIC/PNG/WebP → kompatibel für alle Browser) import io from PIL import Image try: import pillow_heif pillow_heif.register_heif_opener() except ImportError: pass content = await file.read() try: from PIL import ImageOps img = Image.open(io.BytesIO(content)) img = ImageOps.exif_transpose(img) # EXIF-Orientierung anwenden img = img.convert("RGB") buf = io.BytesIO() img.save(buf, format="JPEG", quality=90) content = buf.getvalue() except Exception: pass # Fallback: Originaldaten speichern filename = f"dog_{dog_id}_{uuid.uuid4().hex[:8]}.jpg" path = os.path.join(MEDIA_DIR, "dogs", filename) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "wb") as f: f.write(content) foto_url = f"/media/dogs/{filename}" with db() as conn: conn.execute("UPDATE dogs SET foto_url=? WHERE id=?", (foto_url, dog_id)) return {"foto_url": foto_url} class PhotoPosition(BaseModel): zoom: float = 1.0 offset_x: float = 0.0 offset_y: float = 0.0 @router.patch("/{dog_id}/photo-position") async def update_photo_position(dog_id: int, pos: PhotoPosition, user=Depends(get_current_user)): with db() as conn: updated = conn.execute( "UPDATE dogs SET foto_zoom=?, foto_offset_x=?, foto_offset_y=? WHERE id=? AND user_id=?", (pos.zoom, pos.offset_x, pos.offset_y, dog_id, user["id"]) ).rowcount if not updated: raise HTTPException(404, "Hund nicht gefunden.") return {"ok": True} @router.delete("/{dog_id}/photo", status_code=204) async def delete_photo(dog_id: int, user=Depends(get_current_user)): with db() as conn: row = conn.execute( "SELECT foto_url FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"]) ).fetchone() if not row: raise HTTPException(404, "Hund nicht gefunden.") if row["foto_url"]: path = os.path.join(MEDIA_DIR, row["foto_url"].lstrip("/media/")) if os.path.exists(path): os.remove(path) with db() as conn: conn.execute( "UPDATE dogs SET foto_url=NULL, foto_zoom=1.0, foto_offset_x=0.0, foto_offset_y=0.0 WHERE id=?", (dog_id,) ) # ------------------------------------------------------------------ # Fähigkeiten / Kommandos (für Profil + öffentliche Seite) # ------------------------------------------------------------------ def _parse_exercise_name(exercise_id: str) -> str: """grundkommandos_Hier__Komm → 'Hier / Komm'""" parts = exercise_id.split("_", 1) if len(parts) < 2: return exercise_id return parts[1].replace("__", " / ").replace("_", " ") def _load_skills(conn, dog_id: int, user_id: int) -> list: """Gibt Übungen mit Status 'sitzt' oder 'meistens' zurück, die mit diesem Hund trainiert wurden.""" rows = conn.execute( """ SELECT ep.exercise_id, ep.status, (SELECT ts.exercise_name FROM training_sessions ts WHERE ts.user_id = ep.user_id AND ts.dog_id = ? AND ts.exercise_id = ep.exercise_id ORDER BY ts.datum DESC, ts.created_at DESC LIMIT 1) AS exercise_name FROM exercise_progress ep WHERE ep.user_id = ? AND ep.status IN ('sitzt', 'meistens') AND EXISTS (SELECT 1 FROM training_sessions ts2 WHERE ts2.user_id = ep.user_id AND ts2.dog_id = ? AND ts2.exercise_id = ep.exercise_id) ORDER BY ep.status DESC, ep.exercise_id """, (dog_id, user_id, dog_id) ).fetchall() return [ { "exercise_id": r["exercise_id"], "exercise_name": r["exercise_name"] or _parse_exercise_name(r["exercise_id"]), "status": r["status"], "tab": r["exercise_id"].split("_")[0], } for r in rows ] @router.get("/{dog_id}/skills") async def get_dog_skills(dog_id: int, user=Depends(get_current_user)): uid = user["id"] with db() as conn: dog = conn.execute( "SELECT id, user_id FROM dogs WHERE id=? AND (user_id=? OR id IN (SELECT dog_id FROM sitting_access WHERE friend_id=? AND expires_at > datetime('now')))", (dog_id, uid, uid) ).fetchone() if not dog: raise HTTPException(404, "Hund nicht gefunden.") return _load_skills(conn, dog_id, dog["user_id"]) # Öffentliches Profil (für NFC-Tag, kein Login nötig) @router.get("/public/{dog_id}") async def public_dog_profile(dog_id: int): with db() as conn: dog = conn.execute( """SELECT d.id, d.name, d.rasse, d.geburtstag, d.foto_url, d.bio, d.user_id, u.name as besitzer_name FROM dogs d JOIN users u ON d.user_id=u.id WHERE d.id=? AND d.is_public=1""", (dog_id,) ).fetchone() if not dog: raise HTTPException(404, "Profil nicht gefunden oder nicht öffentlich.") skills = _load_skills(conn, dog_id, dog["user_id"]) result = dict(dog) result.pop("user_id", None) result["skills"] = skills return result class FoundReport(BaseModel): message: Optional[str] = None kontakt: Optional[str] = None # Gefunden-Meldung (kein Login nötig) @router.post("/public/{dog_id}/found") async def report_found(dog_id: int, data: FoundReport = FoundReport()): with db() as conn: row = conn.execute( """SELECT d.id, d.name, d.user_id FROM dogs d WHERE d.id=? AND d.is_public=1""", (dog_id,) ).fetchone() if not row: raise HTTPException(404, "Profil nicht gefunden oder nicht öffentlich.") dog_name = row["name"] user_id = row["user_id"] body = data.message.strip() if data.message and data.message.strip() \ else "Jemand hat deinen Hund gefunden. Öffne die App für Details." if data.kontakt and data.kontakt.strip(): body += f" Kontakt: {data.kontakt.strip()}" send_push_to_user(user_id, { "title": f"🐾 {dog_name} wurde gefunden!", "body": body, "data": {"page": "diary", "found": True}, "tag": f"found-{dog_id}", }) return {"ok": True}