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