A community based topic aggregation platform built on atproto
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}