feat: chat — suppression de post, messages privés, boîte compacte, link preview #191
This commit is contained in:
@@ -22,8 +22,9 @@ from fastapi.responses import FileResponse
|
||||
|
||||
from backend import file_chat as _store
|
||||
from backend.auth.middleware import check_vault_access, require_auth
|
||||
from backend.auth.user_store import get_all_users, get_user
|
||||
from backend.indexer import get_vault_data
|
||||
from backend.schemas import ChatHistoryResponse, ChatMessageResponse
|
||||
from backend.schemas import ChatHistoryResponse, ChatMessageResponse, StatusResponse
|
||||
from backend.services.paths import resolve_safe_path
|
||||
from backend.sse import sse_manager
|
||||
|
||||
@@ -104,8 +105,10 @@ async def api_chat_post(
|
||||
text = str(body.get("text") or "")
|
||||
if not text.strip():
|
||||
raise HTTPException(400, "text is required")
|
||||
# #191 — best-effort link preview: a dead/slow URL never blocks the post.
|
||||
msg = _store.add_global_message(
|
||||
current_user.get("username", ""), text, _attachment(body)
|
||||
current_user.get("username", ""), text, _attachment(body),
|
||||
_store.build_preview(text),
|
||||
)
|
||||
await sse_manager.broadcast(
|
||||
"chat_message",
|
||||
@@ -136,3 +139,105 @@ async def api_chat_attachment(name: str, current_user: dict[str, Any] = Depends(
|
||||
raise HTTPException(404, "Attachment not found")
|
||||
info = _store._MIME_BY_EXT.get(path.suffix.lower(), "application/octet-stream")
|
||||
return FileResponse(str(path), media_type=info)
|
||||
|
||||
|
||||
# --- #191 : suppression + messages privés -----------------------------------
|
||||
|
||||
def _owner_or_admin(msg_user: str, current_user: dict[str, Any]) -> None:
|
||||
"""A post may be deleted by its author or by an admin."""
|
||||
if current_user.get("role") != "admin" and current_user.get("username") != msg_user:
|
||||
raise HTTPException(403, "Seul l'auteur ou un administrateur peut supprimer ce message")
|
||||
|
||||
|
||||
def _find_and_authorize(vault: str, path: str, message_id: str, current_user: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Locate *message_id* in the conversation and check the delete right."""
|
||||
for m in _store.get_messages(vault, path):
|
||||
if m.get("id") == message_id:
|
||||
_owner_or_admin(m.get("user", ""), current_user)
|
||||
return m
|
||||
raise HTTPException(404, "Message not found")
|
||||
|
||||
|
||||
@router.delete("/api/chat/{message_id}", response_model=StatusResponse)
|
||||
async def api_chat_delete(message_id: str, current_user: dict[str, Any] = Depends(require_auth)):
|
||||
"""Delete a message from the general chat (author or admin, #191)."""
|
||||
_find_and_authorize(_store.GLOBAL_VAULT, _store.GLOBAL_PATH, message_id, current_user)
|
||||
if not _store.delete_message(_store.GLOBAL_VAULT, _store.GLOBAL_PATH, message_id):
|
||||
raise HTTPException(404, "Message not found")
|
||||
await sse_manager.broadcast(
|
||||
"chat_deleted",
|
||||
{"vault": _store.GLOBAL_VAULT, "path": _store.GLOBAL_PATH, "id": message_id},
|
||||
)
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
@router.get("/api/chat/users", response_model=list[dict[str, Any]])
|
||||
async def api_chat_users(current_user: dict[str, Any] = Depends(require_auth)):
|
||||
"""Usernames available for a private conversation (chat DM picker, #191).
|
||||
|
||||
Every authenticated member may see who else is around — this is a
|
||||
self-hosted portal, not a directory that needs hiding.
|
||||
"""
|
||||
me = current_user.get("username", "")
|
||||
return [
|
||||
{"username": u.get("username", ""), "display_name": u.get("display_name") or u.get("username", "")}
|
||||
for u in get_all_users()
|
||||
if u.get("username") and u.get("username") != me
|
||||
]
|
||||
|
||||
|
||||
def _dm_peer(username: str, current_user: dict[str, Any]) -> str:
|
||||
"""Validate the DM peer exists and is not ourselves."""
|
||||
if not username or username == current_user.get("username"):
|
||||
raise HTTPException(400, "Destinataire invalide")
|
||||
if not get_user(username):
|
||||
raise HTTPException(404, "Utilisateur inconnu")
|
||||
return username
|
||||
|
||||
|
||||
@router.get("/api/chat/dm/{username}", response_model=ChatHistoryResponse)
|
||||
async def api_chat_dm_history(username: str, current_user: dict[str, Any] = Depends(require_auth)):
|
||||
"""Private history with *username* (#191)."""
|
||||
peer = _dm_peer(username, current_user)
|
||||
return {"messages": _store.get_dm_messages(current_user["username"], peer)}
|
||||
|
||||
|
||||
@router.post("/api/chat/dm/{username}", response_model=ChatMessageResponse)
|
||||
async def api_chat_dm_post(
|
||||
username: str,
|
||||
body: dict[str, Any] = Body(...),
|
||||
current_user: dict[str, Any] = Depends(require_auth),
|
||||
):
|
||||
"""Post a private message and broadcast it to both participants (#191)."""
|
||||
peer = _dm_peer(username, current_user)
|
||||
text = str(body.get("text") or "")
|
||||
if not text.strip():
|
||||
raise HTTPException(400, "text is required")
|
||||
msg = _store.add_dm_message(
|
||||
current_user["username"], peer, current_user.get("username", ""), text,
|
||||
_attachment(body), _store.build_preview(text),
|
||||
)
|
||||
# Same shape as the general chat so the client routes on vault/path.
|
||||
await sse_manager.broadcast(
|
||||
"chat_message",
|
||||
{"vault": _store.DM_VAULT, "path": _store.dm_path(current_user["username"], peer), "message": msg},
|
||||
)
|
||||
return {"message": msg, "status": "ok"}
|
||||
|
||||
|
||||
@router.delete("/api/chat/dm/{username}/{message_id}", response_model=StatusResponse)
|
||||
async def api_chat_dm_delete(
|
||||
username: str,
|
||||
message_id: str,
|
||||
current_user: dict[str, Any] = Depends(require_auth),
|
||||
):
|
||||
"""Delete one private message (author or admin, #191)."""
|
||||
peer = _dm_peer(username, current_user)
|
||||
vault, path = _store.DM_VAULT, _store.dm_path(current_user["username"], peer)
|
||||
_find_and_authorize(vault, path, message_id, current_user)
|
||||
if not _store.delete_message(vault, path, message_id):
|
||||
raise HTTPException(404, "Message not found")
|
||||
await sse_manager.broadcast(
|
||||
"chat_deleted", {"vault": vault, "path": path, "id": message_id}
|
||||
)
|
||||
return {"status": "deleted"}
|
||||
|
||||
Reference in New Issue
Block a user