from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from django.core.cache import cache import logging from app.feeds.models import Post, Profile from app.feeds.utils import get_parent_chain logger = logging.getLogger(__name__) class RepliesView(APIView): """Get replies for a specific post in tree structure""" def get(self, request): post_url = request.query_params.get("post") if not post_url: return Response( { "type": "Error", "errors": ["'post' parameter is required"], "data": None, }, status=status.HTTP_400_BAD_REQUEST, ) # Parse the post URL format: https://feed.com/social.org#post_id try: if "#" not in post_url: raise ValueError("Invalid post URL format") feed_url, post_id = post_url.split("#", 1) except ValueError: return Response( { "type": "Error", "errors": ["Invalid post URL format. Expected: feed_url#post_id"], "data": None, }, status=status.HTTP_400_BAD_REQUEST, ) cache_key = f"replies_{feed_url}_{post_id}" cached_response = cache.get(cache_key) if cached_response is not None: return Response(cached_response, status=status.HTTP_200_OK) # Find the original post try: profile = Profile.objects.get(feed=feed_url) original_post = Post.objects.get(profile=profile, post_id=post_id) except (Profile.DoesNotExist, Post.DoesNotExist): return Response( { "type": "Error", "errors": ["Post not found"], "data": None, }, status=status.HTTP_404_NOT_FOUND, ) # Get all replies to this post and any nested replies original_post_url = f"{feed_url}#{post_id}" # First get direct replies direct_replies = list( Post.objects.filter(reply_to=original_post_url) .select_related("profile") .order_by("created_at") ) # Then get all nested replies recursively all_replies = list(direct_replies) # Get replies to replies (iteratively to capture all levels) current_level = direct_replies while current_level: next_level = [] for reply in current_level: reply_url = f"{reply.profile.feed}#{reply.post_id}" nested_replies = list( Post.objects.filter(reply_to=reply_url) .select_related("profile") .order_by("created_at") ) all_replies.extend(nested_replies) next_level.extend(nested_replies) current_level = next_level replies = all_replies # Build tree structure replies_tree = self._build_replies_tree(replies, original_post_url) # Find moods for the original post moods = self._find_moods(original_post_url, replies) # Calculate parent chain for the original post parent_chain = get_parent_chain(original_post) # URL encode the post_url for the self link from urllib.parse import quote encoded_post_url = quote(post_url, safe="") response_data = { "type": "Success", "errors": [], "data": replies_tree, "meta": { "parent": original_post_url, "moods": moods, "parentChain": parent_chain, }, "_links": { "self": {"href": f"/replies/?post={encoded_post_url}", "method": "GET"} }, } # Cache permanently (will be cleared by scan_feeds task) cache.set(cache_key, response_data, None) return Response(response_data, status=status.HTTP_200_OK) def _build_replies_tree(self, all_replies, parent_url): """Build a tree structure of replies with moods""" # Create a map of all posts by their URL posts_map = {} for reply in all_replies: reply_url = f"{reply.profile.feed}#{reply.post_id}" posts_map[reply_url] = reply # Find direct replies to the parent (exclude mood reactions) direct_replies = [ reply for reply in all_replies if reply.reply_to == parent_url and not (reply.mood and not reply.content.strip()) ] result = [] for reply in direct_replies: reply_url = f"{reply.profile.feed}#{reply.post_id}" # Recursively find children of this reply children = self._find_children(reply_url, all_replies) # Find moods for this reply moods = self._find_moods(reply_url, all_replies) reply_node = { "post": reply_url, "children": children, "moods": moods, } result.append(reply_node) return result def _find_children(self, parent_url, all_replies): """Recursively find children of a specific post""" children = [] for reply in all_replies: # Exclude mood reactions from children (they're not regular replies) if reply.reply_to == parent_url and not ( reply.mood and not reply.content.strip() ): reply_url = f"{reply.profile.feed}#{reply.post_id}" # Recursively find children of this reply grandchildren = self._find_children(reply_url, all_replies) # Find moods for this child moods = self._find_moods(reply_url, all_replies) child_node = { "post": reply_url, "children": grandchildren, "moods": moods, } children.append(child_node) return children def _find_moods(self, target_url, all_replies): """Find mood reactions for a specific post""" moods_dict = {} for reply in all_replies: # A mood reaction is a reply with mood and empty/whitespace content if ( reply.reply_to == target_url and reply.mood and not reply.content.strip() ): emoji = reply.mood reply_url = f"{reply.profile.feed}#{reply.post_id}" if emoji not in moods_dict: moods_dict[emoji] = [] moods_dict[emoji].append(reply_url) # Convert to the desired format: list of {emoji, posts} moods_list = [] for emoji, posts in moods_dict.items(): moods_list.append({"emoji": emoji, "posts": posts}) return moods_list