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}