A community based topic aggregation platform built on atproto
1package communityFeed 2 3import ( 4 "Coves/internal/api/middleware" 5 "Coves/internal/core/communityFeeds" 6 "Coves/internal/core/posts" 7 "Coves/internal/core/votes" 8 "encoding/json" 9 "log" 10 "net/http" 11 "strconv" 12) 13 14// GetCommunityHandler handles community feed retrieval 15type GetCommunityHandler struct { 16 service communityFeeds.Service 17 voteService votes.Service 18} 19 20// NewGetCommunityHandler creates a new community feed handler 21func NewGetCommunityHandler(service communityFeeds.Service, voteService votes.Service) *GetCommunityHandler { 22 return &GetCommunityHandler{ 23 service: service, 24 voteService: voteService, 25 } 26} 27 28// HandleGetCommunity retrieves posts from a community with sorting 29// GET /xrpc/social.coves.communityFeed.getCommunity?community={did_or_handle}&sort=hot&limit=15&cursor=... 30func (h *GetCommunityHandler) HandleGetCommunity(w http.ResponseWriter, r *http.Request) { 31 if r.Method != http.MethodGet { 32 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 33 return 34 } 35 36 // Parse query parameters 37 req, err := h.parseRequest(r) 38 if err != nil { 39 writeError(w, http.StatusBadRequest, "InvalidRequest", err.Error()) 40 return 41 } 42 43 // Get community feed 44 response, err := h.service.GetCommunityFeed(r.Context(), req) 45 if err != nil { 46 handleServiceError(w, err) 47 return 48 } 49 50 // Populate viewer vote state if authenticated and vote service available 51 if h.voteService != nil { 52 session := middleware.GetOAuthSession(r) 53 if session != nil { 54 userDID := middleware.GetUserDID(r) 55 // Ensure vote cache is populated from PDS 56 if err := h.voteService.EnsureCachePopulated(r.Context(), session); err != nil { 57 // Log but don't fail - viewer state is optional 58 log.Printf("Warning: failed to populate vote cache: %v", err) 59 } else { 60 // Collect post URIs to batch lookup 61 postURIs := make([]string, 0, len(response.Feed)) 62 for _, feedPost := range response.Feed { 63 if feedPost.Post != nil { 64 postURIs = append(postURIs, feedPost.Post.URI) 65 } 66 } 67 68 // Get viewer votes for all posts 69 viewerVotes := h.voteService.GetViewerVotesForSubjects(userDID, postURIs) 70 71 // Populate viewer state on each post 72 for _, feedPost := range response.Feed { 73 if feedPost.Post != nil { 74 if vote, exists := viewerVotes[feedPost.Post.URI]; exists { 75 feedPost.Post.Viewer = &posts.ViewerState{ 76 Vote: &vote.Direction, 77 VoteURI: &vote.URI, 78 } 79 } 80 } 81 } 82 } 83 } 84 } 85 86 // Transform blob refs to URLs for all posts 87 for _, feedPost := range response.Feed { 88 if feedPost.Post != nil { 89 posts.TransformBlobRefsToURLs(feedPost.Post) 90 } 91 } 92 93 // Return feed 94 w.Header().Set("Content-Type", "application/json") 95 w.WriteHeader(http.StatusOK) 96 if err := json.NewEncoder(w).Encode(response); err != nil { 97 // Log encoding errors but don't return error response (headers already sent) 98 log.Printf("ERROR: Failed to encode feed response: %v", err) 99 } 100} 101 102// parseRequest parses query parameters into GetCommunityFeedRequest 103func (h *GetCommunityHandler) parseRequest(r *http.Request) (communityFeeds.GetCommunityFeedRequest, error) { 104 req := communityFeeds.GetCommunityFeedRequest{} 105 106 // Required: community 107 req.Community = r.URL.Query().Get("community") 108 109 // Optional: sort (default: hot) 110 req.Sort = r.URL.Query().Get("sort") 111 if req.Sort == "" { 112 req.Sort = "hot" 113 } 114 115 // Optional: timeframe (default: day for top sort) 116 req.Timeframe = r.URL.Query().Get("timeframe") 117 if req.Timeframe == "" && req.Sort == "top" { 118 req.Timeframe = "day" 119 } 120 121 // Optional: limit (default: 15, max: 50) 122 req.Limit = 15 123 if limitStr := r.URL.Query().Get("limit"); limitStr != "" { 124 if limit, err := strconv.Atoi(limitStr); err == nil { 125 req.Limit = limit 126 } 127 } 128 129 // Optional: cursor 130 if cursor := r.URL.Query().Get("cursor"); cursor != "" { 131 req.Cursor = &cursor 132 } 133 134 return req, nil 135}