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}