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:
parent
96bd57f0ad
commit
097295c628
44 changed files with 9980 additions and 300 deletions
191
backend/routes/chat.py
Normal file
191
backend/routes/chat.py
Normal 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}
|
||||
Loading…
Add table
Add a link
Reference in a new issue