mirror of
https://github.com/tanrax/org-social-relay
synced 2026-01-09 22:53:32 +01:00
212 lines
7.0 KiB
Python
212 lines
7.0 KiB
Python
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
|