import uuid import re from datetime import UTC, datetime from typing import Any from motor.motor_asyncio import AsyncIOMotorDatabase from pymongo import ReturnDocument from app.services.document_service import access_query COLLECTION = "comments" NOTIFICATIONS = "notifications" ACTIVITY = "activity" async def ensure_collaboration_indexes(db: AsyncIOMotorDatabase) -> None: await db[COLLECTION].create_index([("document_id", 1), ("created_at", 1)]) await db[NOTIFICATIONS].create_index([("user_id", 1), ("created_at", -1)]) await db[ACTIVITY].create_index([("document_id", 1), ("created_at", -1)]) def _serialize(comment: dict[str, Any]) -> dict[str, Any]: return { "id": comment["_id"], "document_id": comment["document_id"], "body": comment["body"], "parent_id": comment.get("parent_id"), "author_id": comment["author_id"], "author_name": comment["author_name"], "created_at": comment["created_at"], "updated_at": comment["updated_at"], "resolved": comment.get("resolved", False), } async def list_comments(db: AsyncIOMotorDatabase, document_id: str, user_id: int) -> list[dict[str, Any]] | None: document = await db["documents"].find_one({"_id": document_id, **access_query(user_id)}) if document is None: return None cursor = db[COLLECTION].find({"document_id": document_id}).sort("created_at", 1) return [_serialize(comment) async for comment in cursor] async def create_comment( db: AsyncIOMotorDatabase, document_id: str, user_id: int, author_name: str, body: str, parent_id: str | None, mentioned_users: list[dict[str, Any]] | None = None, ) -> dict[str, Any] | None: document = await db["documents"].find_one({"_id": document_id, **access_query(user_id)}) if document is None: return None if parent_id: parent = await db[COLLECTION].find_one({"_id": parent_id, "document_id": document_id}) if parent is None: return None now = datetime.now(UTC) comment = { "_id": str(uuid.uuid4()), "document_id": document_id, "body": body.strip(), "parent_id": parent_id, "author_id": user_id, "author_name": author_name, "created_at": now, "updated_at": now, "resolved": False, } await db[COLLECTION].insert_one(comment) await db[ACTIVITY].insert_one({ "_id": str(uuid.uuid4()), "document_id": document_id, "kind": "comment_created", "message": f"{author_name} commented on the page", "actor_name": author_name, "created_at": now, }) for mentioned in mentioned_users or []: await db[NOTIFICATIONS].insert_one({ "_id": str(uuid.uuid4()), "user_id": mentioned["id"], "kind": "mention", "message": f"{author_name} mentioned you in a comment", "document_id": document_id, "comment_id": comment["_id"], "actor_name": author_name, "read": False, "created_at": now, }) return _serialize(comment) async def delete_comment(db: AsyncIOMotorDatabase, comment_id: str, user_id: int) -> bool: comment = await db[COLLECTION].find_one({"_id": comment_id, "author_id": user_id}) if comment is None: return False result = await db[COLLECTION].delete_one({"_id": comment_id, "author_id": user_id}) return result.deleted_count == 1 async def resolve_comment( db: AsyncIOMotorDatabase, document_id: str, comment_id: str, user_id: int, resolved: bool, actor_name: str, ) -> dict[str, Any] | None: document = await db["documents"].find_one({"_id": document_id, **access_query(user_id)}) if document is None: return None now = datetime.now(UTC) comment = await db[COLLECTION].find_one_and_update( {"_id": comment_id, "document_id": document_id}, {"$set": {"resolved": resolved, "updated_at": now}}, return_document=ReturnDocument.AFTER, ) if comment is None: return None await db[ACTIVITY].insert_one({ "_id": str(uuid.uuid4()), "document_id": document_id, "kind": "comment_resolved" if resolved else "comment_reopened", "message": f"{actor_name} {'resolved' if resolved else 'reopened'} a comment", "actor_name": actor_name, "created_at": now, }) return _serialize(comment) async def list_notifications(db: AsyncIOMotorDatabase, user_id: int) -> list[dict[str, Any]]: cursor = db[NOTIFICATIONS].find({"user_id": user_id}).sort("created_at", -1).limit(50) return [{ "id": item["_id"], "kind": item["kind"], "message": item["message"], "document_id": item.get("document_id"), "comment_id": item.get("comment_id"), "actor_name": item["actor_name"], "read": item.get("read", False), "created_at": item["created_at"], } async for item in cursor] async def mark_notifications_read(db: AsyncIOMotorDatabase, user_id: int) -> None: await db[NOTIFICATIONS].update_many({"user_id": user_id, "read": False}, {"$set": {"read": True}}) async def list_activity(db: AsyncIOMotorDatabase, document_id: str, user_id: int) -> list[dict[str, Any]] | None: document = await db["documents"].find_one({"_id": document_id, **access_query(user_id)}) if document is None: return None cursor = db[ACTIVITY].find({"document_id": document_id}).sort("created_at", -1).limit(100) return [{ "id": item["_id"], "document_id": item["document_id"], "kind": item["kind"], "message": item["message"], "actor_name": item["actor_name"], "created_at": item["created_at"], } async for item in cursor] def mention_names(body: str) -> list[str]: return list(dict.fromkeys(re.findall(r"(?