A community based topic aggregation platform built on atproto
1package timeline 2 3import ( 4 "Coves/internal/api/middleware" 5 "Coves/internal/core/posts" 6 "Coves/internal/core/timeline" 7 "encoding/json" 8 "log" 9 "net/http" 10 "strconv" 11 "strings" 12) 13 14// GetTimelineHandler handles timeline feed retrieval 15type GetTimelineHandler struct { 16 service timeline.Service 17} 18 19// NewGetTimelineHandler creates a new timeline handler 20func NewGetTimelineHandler(service timeline.Service) *GetTimelineHandler { 21 return &GetTimelineHandler{ 22 service: service, 23 } 24} 25 26// HandleGetTimeline retrieves posts from all communities the user subscribes to 27// GET /xrpc/social.coves.feed.getTimeline?sort=hot&limit=15&cursor=... 28// Requires authentication (user must be logged in) 29func (h *GetTimelineHandler) HandleGetTimeline(w http.ResponseWriter, r *http.Request) { 30 if r.Method != http.MethodGet { 31 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 32 return 33 } 34 35 // Extract authenticated user DID from context (set by RequireAuth middleware) 36 userDID := middleware.GetUserDID(r) 37 if userDID == "" || !strings.HasPrefix(userDID, "did:") { 38 writeError(w, http.StatusUnauthorized, "AuthenticationRequired", "User must be authenticated to view timeline") 39 return 40 } 41 42 // Parse query parameters 43 req, err := h.parseRequest(r, userDID) 44 if err != nil { 45 writeError(w, http.StatusBadRequest, "InvalidRequest", err.Error()) 46 return 47 } 48 49 // Get timeline 50 response, err := h.service.GetTimeline(r.Context(), req) 51 if err != nil { 52 handleServiceError(w, err) 53 return 54 } 55 56 // Transform blob refs to URLs for all posts 57 for _, feedPost := range response.Feed { 58 if feedPost.Post != nil { 59 posts.TransformBlobRefsToURLs(feedPost.Post) 60 } 61 } 62 63 // Return feed 64 w.Header().Set("Content-Type", "application/json") 65 w.WriteHeader(http.StatusOK) 66 if err := json.NewEncoder(w).Encode(response); err != nil { 67 // Log encoding errors but don't return error response (headers already sent) 68 log.Printf("ERROR: Failed to encode timeline response: %v", err) 69 } 70} 71 72// parseRequest parses query parameters into GetTimelineRequest 73func (h *GetTimelineHandler) parseRequest(r *http.Request, userDID string) (timeline.GetTimelineRequest, error) { 74 req := timeline.GetTimelineRequest{ 75 UserDID: userDID, // Set from authenticated context 76 } 77 78 // Optional: sort (default: hot) 79 req.Sort = r.URL.Query().Get("sort") 80 if req.Sort == "" { 81 req.Sort = "hot" 82 } 83 84 // Optional: timeframe (default: day for top sort) 85 req.Timeframe = r.URL.Query().Get("timeframe") 86 if req.Timeframe == "" && req.Sort == "top" { 87 req.Timeframe = "day" 88 } 89 90 // Optional: limit (default: 15, max: 50) 91 req.Limit = 15 92 if limitStr := r.URL.Query().Get("limit"); limitStr != "" { 93 if limit, err := strconv.Atoi(limitStr); err == nil { 94 req.Limit = limit 95 } 96 } 97 98 // Optional: cursor 99 if cursor := r.URL.Query().Get("cursor"); cursor != "" { 100 req.Cursor = &cursor 101 } 102 103 return req, nil 104}