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