A community based topic aggregation platform built on atproto
at main 107 lines 3.1 kB view raw
1package discover 2 3import ( 4 "encoding/json" 5 "log" 6 "net/http" 7 "strconv" 8 9 "Coves/internal/api/handlers/common" 10 "Coves/internal/api/middleware" 11 "Coves/internal/core/blueskypost" 12 "Coves/internal/core/discover" 13 "Coves/internal/core/posts" 14 "Coves/internal/core/votes" 15) 16 17// GetDiscoverHandler handles discover feed retrieval 18type GetDiscoverHandler struct { 19 service discover.Service 20 voteService votes.Service 21 blueskyService blueskypost.Service 22} 23 24// NewGetDiscoverHandler creates a new discover handler 25func NewGetDiscoverHandler(service discover.Service, voteService votes.Service, blueskyService blueskypost.Service) *GetDiscoverHandler { 26 if blueskyService == nil { 27 log.Printf("[DISCOVER-HANDLER] WARNING: blueskyService is nil - Bluesky post embeds will not be resolved") 28 } 29 return &GetDiscoverHandler{ 30 service: service, 31 voteService: voteService, 32 blueskyService: blueskyService, 33 } 34} 35 36// HandleGetDiscover retrieves posts from all communities (public feed) 37// GET /xrpc/social.coves.feed.getDiscover?sort=hot&limit=15&cursor=... 38// Public endpoint with optional auth - if authenticated, includes viewer vote state 39func (h *GetDiscoverHandler) HandleGetDiscover(w http.ResponseWriter, r *http.Request) { 40 if r.Method != http.MethodGet { 41 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 42 return 43 } 44 45 // Parse query parameters 46 req := h.parseRequest(r) 47 48 // Get discover feed 49 response, err := h.service.GetDiscover(r.Context(), req) 50 if err != nil { 51 handleServiceError(w, err) 52 return 53 } 54 55 // Populate viewer vote state if authenticated 56 common.PopulateViewerVoteState(r.Context(), r, h.voteService, response.Feed) 57 58 // Transform blob refs to URLs and resolve post embeds for all posts 59 for _, feedPost := range response.Feed { 60 if feedPost.Post != nil { 61 posts.TransformBlobRefsToURLs(feedPost.Post) 62 posts.TransformPostEmbeds(r.Context(), feedPost.Post, h.blueskyService) 63 } 64 } 65 66 // Return feed 67 w.Header().Set("Content-Type", "application/json") 68 w.WriteHeader(http.StatusOK) 69 if err := json.NewEncoder(w).Encode(response); err != nil { 70 log.Printf("ERROR: Failed to encode discover response: %v", err) 71 } 72} 73 74// parseRequest parses query parameters into GetDiscoverRequest 75func (h *GetDiscoverHandler) parseRequest(r *http.Request) discover.GetDiscoverRequest { 76 req := discover.GetDiscoverRequest{} 77 78 // Extract viewer DID from OptionalAuth context for block filtering 79 req.ViewerDID = middleware.GetUserDID(r) 80 81 // Optional: sort (default: hot) 82 req.Sort = r.URL.Query().Get("sort") 83 if req.Sort == "" { 84 req.Sort = "hot" 85 } 86 87 // Optional: timeframe (default: day for top sort) 88 req.Timeframe = r.URL.Query().Get("timeframe") 89 if req.Timeframe == "" && req.Sort == "top" { 90 req.Timeframe = "day" 91 } 92 93 // Optional: limit (default: 15, max: 50) 94 req.Limit = 15 95 if limitStr := r.URL.Query().Get("limit"); limitStr != "" { 96 if limit, err := strconv.Atoi(limitStr); err == nil { 97 req.Limit = limit 98 } 99 } 100 101 // Optional: cursor 102 if cursor := r.URL.Query().Get("cursor"); cursor != "" { 103 req.Cursor = &cursor 104 } 105 106 return req 107}