Download api/models/comments.py from mwask/upcove: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/spaces/mwask/upcove/resolve/main/api/models/comments.py
- Command line
-
hf download hf://spaces/mwask/upcove/api/models/comments.py
-
curl -L -o comments.py https://huggingface.co/spaces/mwask/upcove/resolve/main/api/models/comments.py
11.1 kB
| """ | |
| Comments & Episode Reactions model layer. | |
| Handles all MongoDB operations for the comment system. | |
| """ | |
| from datetime import datetime, timezone | |
| from bson import ObjectId | |
| from pymongo import ASCENDING, DESCENDING | |
| from ..core.db_connector import comments_collection, episode_reactions_collection | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Index setup (run once on import — idempotent) | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def _ensure_indexes(): | |
| try: | |
| comments_collection.create_index( | |
| [("anime_id", ASCENDING), ("episode_number", ASCENDING), ("created_at", DESCENDING)] | |
| ) | |
| comments_collection.create_index([("parent_id", ASCENDING)]) | |
| episode_reactions_collection.create_index( | |
| [("anime_id", ASCENDING), ("episode_number", ASCENDING)], | |
| unique=True | |
| ) | |
| except Exception: | |
| pass # Don't crash the app if index creation fails | |
| _ensure_indexes() | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Helpers | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def _serialize_comment(doc): | |
| """Convert a MongoDB comment document into a JSON-serialisable dict.""" | |
| if not doc: | |
| return None | |
| return { | |
| "_id": str(doc["_id"]), | |
| "anime_id": doc.get("anime_id", ""), | |
| "episode_number": doc.get("episode_number", 0), | |
| "parent_id": str(doc["parent_id"]) if doc.get("parent_id") else None, | |
| "author": doc.get("author", "Anonymous"), | |
| "author_id": str(doc["author_id"]) if doc.get("author_id") is not None else None, | |
| "avatar": doc.get("avatar"), | |
| "body": doc.get("body", ""), | |
| "gif_url": doc.get("gif_url"), | |
| "likes": [str(u) for u in doc.get("likes", [])], | |
| "dislikes": [str(u) for u in doc.get("dislikes", [])], | |
| "like_count": len(doc.get("likes", [])), | |
| "dislike_count": len(doc.get("dislikes", [])), | |
| "created_at": doc["created_at"].isoformat() if doc.get("created_at") else None, | |
| "edited_at": doc["edited_at"].isoformat() if doc.get("edited_at") else None, | |
| "deleted": doc.get("deleted", False), | |
| "replies": [], # populated by get_comments() | |
| } | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Comments | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def get_comments(anime_id: str, episode_number: int) -> list: | |
| """ | |
| Return all top-level comments for an episode, each with nested replies. | |
| Replies are sorted oldest-first; top-level comments are sorted newest-first. | |
| """ | |
| raw = list( | |
| comments_collection.find( | |
| {"anime_id": anime_id, "episode_number": episode_number, "deleted": False}, | |
| sort=[("created_at", ASCENDING)], | |
| ) | |
| ) | |
| top_level = [] | |
| reply_map: dict[str, list] = {} | |
| for doc in raw: | |
| serialized = _serialize_comment(doc) | |
| pid = serialized["parent_id"] | |
| if pid is None: | |
| top_level.append(serialized) | |
| else: | |
| reply_map.setdefault(pid, []).append(serialized) | |
| # Attach replies | |
| for comment in top_level: | |
| comment["replies"] = reply_map.get(comment["_id"], []) | |
| # Sort top-level newest first | |
| top_level.sort(key=lambda c: c["created_at"] or "", reverse=True) | |
| return top_level | |
| def create_comment( | |
| anime_id: str, | |
| episode_number: int, | |
| author: str, | |
| avatar, | |
| body: str, | |
| gif_url=None, | |
| parent_id=None, | |
| author_id=None, | |
| ) -> dict | None: | |
| """Insert a new comment and return the serialised document.""" | |
| if not body and not gif_url: | |
| return None # Must have content | |
| doc = { | |
| "anime_id": anime_id, | |
| "episode_number": int(episode_number), | |
| "parent_id": ObjectId(parent_id) if parent_id else None, | |
| "author": author, | |
| "author_id": str(author_id) if author_id is not None else None, | |
| "avatar": avatar, | |
| "body": body.strip() if body else "", | |
| "gif_url": gif_url, | |
| "likes": [], | |
| "dislikes": [], | |
| "created_at": datetime.now(timezone.utc), | |
| "edited_at": None, | |
| "deleted": False, | |
| } | |
| result = comments_collection.insert_one(doc) | |
| doc["_id"] = result.inserted_id | |
| return _serialize_comment(doc) | |
| def toggle_comment_reaction(comment_id: str, user_id: str, reaction_type: str) -> dict | None: | |
| """ | |
| Toggle like or dislike on a comment. | |
| - Adding a like removes any existing dislike, and vice-versa. | |
| - Reacting again to the same type removes it (toggle off). | |
| Returns updated like/dislike counts, or None if comment not found. | |
| """ | |
| try: | |
| oid = ObjectId(comment_id) | |
| except Exception: | |
| return None | |
| doc = comments_collection.find_one({"_id": oid, "deleted": False}) | |
| if not doc: | |
| return None | |
| likes = [str(u) for u in doc.get("likes", [])] | |
| dislikes = [str(u) for u in doc.get("dislikes", [])] | |
| if reaction_type == "like": | |
| if user_id in likes: | |
| likes.remove(user_id) # toggle off | |
| else: | |
| likes.append(user_id) | |
| if user_id in dislikes: | |
| dislikes.remove(user_id) # remove opposite | |
| elif reaction_type == "dislike": | |
| if user_id in dislikes: | |
| dislikes.remove(user_id) # toggle off | |
| else: | |
| dislikes.append(user_id) | |
| if user_id in likes: | |
| likes.remove(user_id) # remove opposite | |
| else: | |
| return None | |
| comments_collection.update_one( | |
| {"_id": oid}, | |
| {"$set": {"likes": likes, "dislikes": dislikes}}, | |
| ) | |
| return {"like_count": len(likes), "dislike_count": len(dislikes), | |
| "user_liked": user_id in likes, "user_disliked": user_id in dislikes} | |
| def edit_comment(comment_id: str, new_body: str, new_gif_url: str | None) -> dict | None: | |
| """Updates a comment's body/gif_url and sets edited_at timestamp.""" | |
| try: | |
| oid = ObjectId(comment_id) | |
| except Exception: | |
| return None | |
| update_fields = {"edited_at": datetime.now(timezone.utc)} | |
| if new_body is not None: update_fields["body"] = new_body.strip() | |
| if new_gif_url is not None: update_fields["gif_url"] = new_gif_url | |
| comments_collection.update_one({"_id": oid}, {"$set": update_fields}) | |
| doc = comments_collection.find_one({"_id": oid}) | |
| return _serialize_comment(doc) | |
| def delete_comment(comment_id: str) -> bool: | |
| """ | |
| Soft-deletes a comment (sets body/author to '[deleted]') if it has replies. | |
| Otherwise, hard-deletes it completely. | |
| """ | |
| try: | |
| oid = ObjectId(comment_id) | |
| except Exception: | |
| return False | |
| has_replies = comments_collection.find_one({"parent_id": oid}) is not None | |
| if has_replies: | |
| comments_collection.update_one( | |
| {"_id": oid}, | |
| {"$set": { | |
| "body": "[deleted]", | |
| "gif_url": None, | |
| "author": "[deleted]", | |
| "avatar": None, | |
| "deleted": True # mark as formally deleted in UI state while preserving replies | |
| }} | |
| ) | |
| else: | |
| comments_collection.delete_one({"_id": oid}) | |
| return True | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Episode reactions | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def get_episode_reaction(anime_id: str, episode_number: int, user_id: str | None = None) -> dict: | |
| """Return episode like/dislike counts (and optionally the user's vote).""" | |
| doc = episode_reactions_collection.find_one( | |
| {"anime_id": anime_id, "episode_number": int(episode_number)} | |
| ) | |
| likes = [str(u) for u in (doc.get("likes", []) if doc else [])] | |
| dislikes = [str(u) for u in (doc.get("dislikes", []) if doc else [])] | |
| result = { | |
| "like_count": len(likes), | |
| "dislike_count": len(dislikes), | |
| "user_liked": False, | |
| "user_disliked": False, | |
| } | |
| if user_id: | |
| result["user_liked"] = user_id in likes | |
| result["user_disliked"] = user_id in dislikes | |
| return result | |
| def toggle_episode_reaction(anime_id: str, episode_number: int, user_id: str, reaction_type: str) -> dict: | |
| """Toggle like or dislike on an episode. Returns updated counts.""" | |
| doc = episode_reactions_collection.find_one( | |
| {"anime_id": anime_id, "episode_number": int(episode_number)} | |
| ) | |
| if doc: | |
| likes = [str(u) for u in doc.get("likes", [])] | |
| dislikes = [str(u) for u in doc.get("dislikes", [])] | |
| else: | |
| likes, dislikes = [], [] | |
| if reaction_type == "like": | |
| if user_id in likes: | |
| likes.remove(user_id) | |
| else: | |
| likes.append(user_id) | |
| if user_id in dislikes: | |
| dislikes.remove(user_id) | |
| elif reaction_type == "dislike": | |
| if user_id in dislikes: | |
| dislikes.remove(user_id) | |
| else: | |
| dislikes.append(user_id) | |
| if user_id in likes: | |
| likes.remove(user_id) | |
| episode_reactions_collection.update_one( | |
| {"anime_id": anime_id, "episode_number": int(episode_number)}, | |
| {"$set": {"likes": likes, "dislikes": dislikes}}, | |
| upsert=True, | |
| ) | |
| return { | |
| "like_count": len(likes), | |
| "dislike_count": len(dislikes), | |
| "user_liked": user_id in likes, | |
| "user_disliked": user_id in dislikes, | |
| } | |