Sprint 16: Chat-Fotos/Online/Read-Receipts, Gesundheit-Dokumente löschen, Bugfixes
- Chat: Foto-Versand (POST /api/chat/conversations/{id}/upload, media_url/media_type)
- Chat: Online-Indikator (last_seen Heartbeat, grüner Dot, 3min-Fenster)
- Chat: Read Receipts (read_at, Einzel-/Doppelhaken-Icons)
- Gesundheit: Dokument löschen (DELETE .../dokument, Datei + DB-Eintrag)
- Bug: events.user_id NOT NULL → nullable (Table-Recreation-Migration)
- Bug: scheduler INSERT user_id 0 → NULL
- Bug: Wikidata Rate-Limit: sleep 0.3s→1.0s, retries 2→4, exponentielles Backoff
- SW: by-v146, APP_VER 119
This commit is contained in:
parent
34f29f9d0a
commit
a7753c9cf5
15 changed files with 375 additions and 43 deletions
|
|
@ -1,6 +1,9 @@
|
|||
"""BAN YARO — Direktnachrichten"""
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||
from pydantic import BaseModel
|
||||
from database import db
|
||||
from auth import get_current_user
|
||||
|
|
@ -8,6 +11,12 @@ 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."""
|
||||
|
|
@ -17,11 +26,13 @@ def _conv_key(a: int, b: int):
|
|||
@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,
|
||||
|
|
@ -39,8 +50,14 @@ async def list_conversations(user=Depends(get_current_user)):
|
|||
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]
|
||||
""", (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):
|
||||
|
|
@ -79,6 +96,8 @@ async def start_conversation(data: StartConvModel, user=Depends(get_current_user
|
|||
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=?)",
|
||||
|
|
@ -88,10 +107,20 @@ async def get_messages(conv_id: int, offset: int = 0, limit: int = 50,
|
|||
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()
|
||||
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
|
||||
|
|
@ -100,10 +129,14 @@ async def get_messages(conv_id: int, offset: int = 0, limit: int = 50,
|
|||
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],
|
||||
}
|
||||
|
||||
|
|
@ -157,6 +190,71 @@ async def send_message(conv_id: int, data: SendMsgModel, user=Depends(get_curren
|
|||
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"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue