# backend/routers/file_chat.py — chat par fichier (#169) """Per-file chat endpoints: history read + message post with SSE fan-out. GET/POST ``/api/file/{vault_name}/chat`` — auth + vault access + path traversal check (``resolve_safe_path`` raises ``ServiceError`` mapped by the app-level handler). A posted message is broadcast on the existing SSE channel (``chat_message``) so every connected client updates live without a second WebSocket. """ from __future__ import annotations from pathlib import Path from typing import Any from fastapi import APIRouter, Body, Depends, HTTPException from backend import file_chat as _store from backend.auth.middleware import check_vault_access, require_auth from backend.indexer import get_vault_data from backend.schemas import ChatHistoryResponse, ChatMessageResponse from backend.services.paths import resolve_safe_path from backend.sse import sse_manager router = APIRouter() # tags dérivés de `tag_for_path` → « Files » def _check(vault_name: str, path: str, current_user: dict[str, Any]) -> None: """Authz + traversal guard shared by both verbs.""" if not check_vault_access(vault_name, current_user): raise HTTPException(403, f"Accès refusé à la vault '{vault_name}'") vault_data = get_vault_data(vault_name) if not vault_data: raise HTTPException(404, "Vault not found") resolve_safe_path(Path(vault_data["path"]), path) # ServiceError → 403/500 @router.get("/api/file/{vault_name}/chat", response_model=ChatHistoryResponse) async def api_file_chat_history( vault_name: str, path: str, current_user: dict[str, Any] = Depends(require_auth), ): """Return the chat history for a file (chronological).""" _check(vault_name, path, current_user) return {"messages": _store.get_messages(vault_name, path)} @router.post("/api/file/{vault_name}/chat", response_model=ChatMessageResponse) async def api_file_chat_post( vault_name: str, body: dict[str, Any] = Body(...), current_user: dict[str, Any] = Depends(require_auth), ): """Post a chat message and broadcast it on SSE (``chat_message``).""" path = str(body.get("path") or "") text = str(body.get("text") or "") if not path: raise HTTPException(400, "path is required") if not text.strip(): raise HTTPException(400, "text is required") _check(vault_name, path, current_user) msg = _store.add_message(vault_name, path, current_user.get("username", ""), text) await sse_manager.broadcast("chat_message", {"vault": vault_name, "path": path, "message": msg}) return {"message": msg, "status": "ok"}