Sprint 11: Freunde & Chat + Phosphor-Icon-Vollmigration

- Freundschaften (pending/accepted), Nutzersuche, Anfragen per Push
- Direktnachrichten mit Polling, iMessage-Stil, Deep-Links aus Push
- Alle Seiten (map, places, diary, health, dog-profile, sitting, knigge,
  forum, wiki, walks) vollständig auf Phosphor-Icons migriert
- Wikidata-Rassen-Scraper (~833 neue Rassen, lokal gespiegelte Fotos)
- TheDogAPI lokal gespiegelt (169 Rassen + Fotos)
- Quiz-Result-Cards horizontal (korrekte Bildproportionen)
- SW by-v89
This commit is contained in:
rene 2026-04-15 21:33:53 +02:00
parent 96bd57f0ad
commit 097295c628
44 changed files with 9980 additions and 300 deletions

191
backend/routes/chat.py Normal file
View file

@ -0,0 +1,191 @@
"""BAN YARO — Direktnachrichten"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from database import db
from auth import get_current_user
router = APIRouter()
logger = logging.getLogger(__name__)
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"]
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,
(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)).fetchall()
return [dict(r) for r in rows]
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"]
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 FROM users WHERE id=?", (partner_id,)).fetchone()
msgs = conn.execute("""
SELECT m.id, m.sender_id, m.text, m.is_deleted, m.created_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()
return {
"conversation_id": conv_id,
"partner_id": partner_id,
"partner_name": partner["name"] if partner else "Unbekannt",
"messages": [dict(m) for m in msgs],
}
class SendMsgModel(BaseModel):
text: str
@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.")
if len(text) > 2000:
raise HTTPException(400, "Nachricht zu lang (max. 2000 Zeichen).")
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}/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}