"""BAN YARO — Direktnachrichten""" import logging import os import uuid from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, UploadFile, File from pydantic import BaseModel, Field from database import db from auth import get_current_user router = APIRouter() logger = logging.getLogger(__name__) MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") CHAT_DIR = os.path.join(MEDIA_DIR, "chat") ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"} MAX_UPLOAD_BYTES = 10 * 1024 * 1024 # 10 MB ONLINE_MINUTES = 3 def _conv_key(a: int, b: int): """Normalisiert Konversations-User-IDs: user_a < user_b.""" return (min(a, b), max(a, b)) @router.get("/conversations") async def list_conversations(user=Depends(get_current_user)): uid = user["id"] online_cutoff = (datetime.utcnow() - timedelta(minutes=ONLINE_MINUTES)).isoformat() with db() as conn: rows = conn.execute(""" SELECT c.id, c.last_msg_at, CASE WHEN c.user_a_id=? THEN c.user_b_id ELSE c.user_a_id END AS partner_id, CASE WHEN c.user_a_id=? THEN ub.name ELSE ua.name END AS partner_name, CASE WHEN c.user_a_id=? THEN ub.last_seen ELSE ua.last_seen END AS partner_last_seen, (SELECT text FROM direct_messages WHERE conversation_id=c.id AND is_deleted=0 ORDER BY created_at DESC LIMIT 1) AS last_text, (SELECT COUNT(*) FROM direct_messages WHERE conversation_id=c.id AND sender_id != ? AND is_deleted=0 AND created_at > COALESCE( CASE WHEN c.user_a_id=? THEN c.a_read_at ELSE c.b_read_at END, '1970-01-01' ) ) AS unread_count FROM conversations c JOIN users ua ON ua.id=c.user_a_id JOIN users ub ON ub.id=c.user_b_id WHERE c.user_a_id=? OR c.user_b_id=? ORDER BY COALESCE(c.last_msg_at, c.created_at) DESC """, (uid, uid, uid, uid, uid, uid, uid)).fetchall() result = [] for r in rows: d = dict(r) last_seen = d.get("partner_last_seen") d["partner_online"] = bool(last_seen and last_seen >= online_cutoff) result.append(d) return result class StartConvModel(BaseModel): partner_id: int @router.post("/conversations", status_code=201) async def start_conversation(data: StartConvModel, user=Depends(get_current_user)): uid = user["id"] if uid == data.partner_id: raise HTTPException(400, "Du kannst dir selbst keine Nachrichten schicken.") a, b = _conv_key(uid, data.partner_id) with db() as conn: f = conn.execute(""" SELECT 1 FROM friendships WHERE ((requester_id=? AND addressee_id=?) OR (requester_id=? AND addressee_id=?)) AND status='accepted' """, (uid, data.partner_id, data.partner_id, uid)).fetchone() if not f: raise HTTPException(403, "Ihr seid noch keine Freunde.") existing = conn.execute( "SELECT id FROM conversations WHERE user_a_id=? AND user_b_id=?", (a, b) ).fetchone() if existing: return {"conversation_id": existing["id"]} cur = conn.execute( "INSERT INTO conversations (user_a_id, user_b_id) VALUES (?,?)", (a, b) ) return {"conversation_id": cur.lastrowid} @router.get("/conversations/{conv_id}") async def get_messages(conv_id: int, offset: int = 0, limit: int = 50, user=Depends(get_current_user)): uid = user["id"] online_cutoff = (datetime.utcnow() - timedelta(minutes=ONLINE_MINUTES)).isoformat() now_iso = datetime.utcnow().isoformat() with db() as conn: conv = conn.execute( "SELECT * FROM conversations WHERE id=? AND (user_a_id=? OR user_b_id=?)", (conv_id, uid, uid) ).fetchone() if not conv: raise HTTPException(404, "Konversation nicht gefunden.") partner_id = conv["user_b_id"] if conv["user_a_id"] == uid else conv["user_a_id"] partner = conn.execute( "SELECT name, last_seen FROM users WHERE id=?", (partner_id,) ).fetchone() # Ungelesene Nachrichten des Partners als gelesen markieren conn.execute(""" UPDATE direct_messages SET read_at=? WHERE conversation_id=? AND sender_id!=? AND read_at IS NULL """, (now_iso, conv_id, uid)) msgs = conn.execute(""" SELECT m.id, m.sender_id, m.text, m.is_deleted, m.created_at, m.media_url, m.media_type, m.read_at, u.name AS sender_name FROM direct_messages m JOIN users u ON u.id=m.sender_id WHERE m.conversation_id=? ORDER BY m.created_at ASC LIMIT ? OFFSET ? """, (conv_id, limit, offset)).fetchall() partner_last_seen = partner["last_seen"] if partner else None partner_online = bool(partner_last_seen and partner_last_seen >= online_cutoff) return { "conversation_id": conv_id, "partner_id": partner_id, "partner_name": partner["name"] if partner else "Unbekannt", "partner_online": partner_online, "messages": [dict(m) for m in msgs], } class SendMsgModel(BaseModel): text: str = Field(..., min_length=1, max_length=2000) @router.post("/conversations/{conv_id}/messages", status_code=201) async def send_message(conv_id: int, data: SendMsgModel, user=Depends(get_current_user)): uid = user["id"] text = data.text.strip() if not text: raise HTTPException(400, "Nachricht darf nicht leer sein.") with db() as conn: conv = conn.execute( "SELECT * FROM conversations WHERE id=? AND (user_a_id=? OR user_b_id=?)", (conv_id, uid, uid) ).fetchone() if not conv: raise HTTPException(404, "Konversation nicht gefunden.") partner_id = conv["user_b_id"] if conv["user_a_id"] == uid else conv["user_a_id"] cur = conn.execute(""" INSERT INTO direct_messages (conversation_id, sender_id, text) VALUES (?,?,?) """, (conv_id, uid, text)) msg_id = cur.lastrowid conn.execute( "UPDATE conversations SET last_msg_at=datetime('now') WHERE id=?", (conv_id,) ) try: from routes.push import send_push_to_user preview = text[:100] + ("…" if len(text) > 100 else "") send_push_to_user(partner_id, { "title": f"Nachricht von {user['name']}", "body": preview, "type": "chat_message", "tag": f"chat-{conv_id}", "data": {"page": "chat", "conversation_id": conv_id}, }) except Exception: pass return {"id": msg_id, "ok": True} @router.post("/conversations/{conv_id}/upload", status_code=201) async def upload_photo(conv_id: int, file: UploadFile = File(...), user=Depends(get_current_user)): uid = user["id"] if file.content_type not in ALLOWED_IMAGE_TYPES: raise HTTPException(400, "Nur JPEG, PNG, GIF und WebP erlaubt.") data = await file.read() if len(data) > MAX_UPLOAD_BYTES: raise HTTPException(400, "Datei zu groß (max. 10 MB).") os.makedirs(CHAT_DIR, exist_ok=True) ext = os.path.splitext(file.filename or "")[1] or ".jpg" filename = f"{uuid.uuid4().hex}{ext}" path = os.path.join(CHAT_DIR, filename) with open(path, "wb") as f: f.write(data) media_url = f"/media/chat/{filename}" media_type = file.content_type with db() as conn: conv = conn.execute( "SELECT * FROM conversations WHERE id=? AND (user_a_id=? OR user_b_id=?)", (conv_id, uid, uid) ).fetchone() if not conv: raise HTTPException(404, "Konversation nicht gefunden.") partner_id = conv["user_b_id"] if conv["user_a_id"] == uid else conv["user_a_id"] cur = conn.execute(""" INSERT INTO direct_messages (conversation_id, sender_id, text, media_url, media_type) VALUES (?,?,?,?,?) """, (conv_id, uid, "", media_url, media_type)) msg_id = cur.lastrowid 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(partner_id, { "title": f"Foto von {user['name']}", "body": "hat dir ein Foto geschickt", "type": "chat_message", "tag": f"chat-{conv_id}", "data": {"page": "chat", "conversation_id": conv_id}, }) except Exception: pass return {"id": msg_id, "media_url": media_url, "media_type": media_type, "ok": True} @router.post("/heartbeat") async def heartbeat(user=Depends(get_current_user)): uid = user["id"] now_iso = datetime.utcnow().isoformat() with db() as conn: conn.execute("UPDATE users SET last_seen=? WHERE id=?", (now_iso, uid)) return {"ok": True} @router.post("/conversations/{conv_id}/read") async def mark_read(conv_id: int, user=Depends(get_current_user)): uid = user["id"] with db() as conn: conv = conn.execute( "SELECT * FROM conversations WHERE id=? AND (user_a_id=? OR user_b_id=?)", (conv_id, uid, uid) ).fetchone() if not conv: raise HTTPException(404) field = "a_read_at" if conv["user_a_id"] == uid else "b_read_at" conn.execute( f"UPDATE conversations SET {field}=datetime('now') WHERE id=?", (conv_id,) ) return {"ok": True} @router.delete("/messages/{msg_id}") async def delete_message(msg_id: int, user=Depends(get_current_user)): uid = user["id"] with db() as conn: msg = conn.execute( "SELECT id FROM direct_messages WHERE id=? AND sender_id=?", (msg_id, uid) ).fetchone() if not msg: raise HTTPException(404, "Nachricht nicht gefunden.") conn.execute( "UPDATE direct_messages SET is_deleted=1, text='[gelöscht]' WHERE id=?", (msg_id,) ) return {"ok": True}