from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from motor.motor_asyncio import AsyncIOMotorDatabase from sqlalchemy.orm import Session from app.database import get_db from app.dependencies import get_current_user, get_mongo_db_dep from app.models.user import User from app.schemas.document import DocumentCreate, DocumentResponse, DocumentUpdate, DocumentVersionResponse, ShareRequest from app.schemas.collaboration import ( ActivityResponse, CommentCreate, CommentResolveRequest, CommentResponse, NotificationResponse, ) from app.services.collaboration_service import ( create_comment, delete_comment, list_activity, list_comments, list_notifications, mark_notifications_read, mention_names, resolve_comment, ) from app.services.document_service import ( add_member, create_document, delete_document, get_document, list_documents, record_visit, remove_member, search_documents, update_document, ) from app.services.user_service import get_user_by_email_or_username from app.services.history_service import list_versions, restore_version from html import escape router = APIRouter(prefix="/documents", tags=["documents"]) @router.get("/{doc_id}/comments", response_model=list[CommentResponse]) async def get_comments( doc_id: str, current_user: User = Depends(get_current_user), db: AsyncIOMotorDatabase = Depends(get_mongo_db_dep), ) -> list[CommentResponse]: comments = await list_comments(db, doc_id, current_user.id) if comments is None: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Document not found or access denied") return [CommentResponse(**comment) for comment in comments] @router.post("/{doc_id}/comments", response_model=CommentResponse, status_code=status.HTTP_201_CREATED) async def post_comment( doc_id: str, data: CommentCreate, current_user: User = Depends(get_current_user), db: AsyncIOMotorDatabase = Depends(get_mongo_db_dep), sql: Session = Depends(get_db), ) -> CommentResponse: mentioned_users = [] for name in mention_names(data.body): mentioned = get_user_by_email_or_username(sql, name.lower()) if mentioned is not None and mentioned.id != current_user.id: mentioned_users.append({"id": mentioned.id}) comment = await create_comment( db, doc_id, current_user.id, current_user.full_name or current_user.username, data.body, data.parent_id, mentioned_users, ) if comment is None: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Document not found or parent comment is invalid") return CommentResponse(**comment) @router.patch("/{doc_id}/comments/{comment_id}/resolve", response_model=CommentResponse) async def update_comment_resolution( doc_id: str, comment_id: str, data: CommentResolveRequest, current_user: User = Depends(get_current_user), db: AsyncIOMotorDatabase = Depends(get_mongo_db_dep), ) -> CommentResponse: comment = await resolve_comment( db, doc_id, comment_id, current_user.id, data.resolved, current_user.full_name or current_user.username, ) if comment is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Comment not found or access denied") return CommentResponse(**comment) @router.get("/{doc_id}/activity", response_model=list[ActivityResponse]) async def get_activity( doc_id: str, current_user: User = Depends(get_current_user), db: AsyncIOMotorDatabase = Depends(get_mongo_db_dep), ) -> list[ActivityResponse]: entries = await list_activity(db, doc_id, current_user.id) if entries is None: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Document not found or access denied") return [ActivityResponse(**entry) for entry in entries] @router.get("/notifications", response_model=list[NotificationResponse]) async def get_notifications( current_user: User = Depends(get_current_user), db: AsyncIOMotorDatabase = Depends(get_mongo_db_dep), ) -> list[NotificationResponse]: return [NotificationResponse(**item) for item in await list_notifications(db, current_user.id)] @router.post("/notifications/read", status_code=status.HTTP_204_NO_CONTENT) async def read_notifications( current_user: User = Depends(get_current_user), db: AsyncIOMotorDatabase = Depends(get_mongo_db_dep), ) -> None: await mark_notifications_read(db, current_user.id) @router.get("/{doc_id}/history", response_model=list[DocumentVersionResponse]) async def get_history( doc_id: str, current_user: User = Depends(get_current_user), db: AsyncIOMotorDatabase = Depends(get_mongo_db_dep), ) -> list[DocumentVersionResponse]: versions = await list_versions(db, doc_id, current_user.id) if versions is None: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Document not found or access denied") return [DocumentVersionResponse(**version) for version in versions] @router.post("/{doc_id}/history/{version_id}/restore", response_model=DocumentResponse) async def restore_history( doc_id: str, version_id: str, current_user: User = Depends(get_current_user), db: AsyncIOMotorDatabase = Depends(get_mongo_db_dep), ) -> DocumentResponse: document = await restore_version( db, doc_id, version_id, current_user.id, current_user.full_name or current_user.username, ) if document is None: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Version not found or restore access denied") from app.services.document_service import _serialize return DocumentResponse(**_serialize(document)) @router.get("/{doc_id}/export") async def export_document( doc_id: str, format: str = Query("html", pattern="^(html|text)$"), current_user: User = Depends(get_current_user), db: AsyncIOMotorDatabase = Depends(get_mongo_db_dep), ) -> Response: document = await get_document(db, doc_id, current_user.id) if document is None: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Document not found or access denied") body_html = str(document["content"].get("bodyHtml", document["content"].get("body", ""))) if format == "text": import re return Response(re.sub(r"<[^>]+>", " ", body_html).strip(), media_type="text/plain") html = ( "
" f"