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}