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/timeline"
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 // Return feed
57 w.Header().Set("Content-Type", "application/json")
58 w.WriteHeader(http.StatusOK)
59 if err := json.NewEncoder(w).Encode(response); err != nil {
60 // Log encoding errors but don't return error response (headers already sent)
61 log.Printf("ERROR: Failed to encode timeline response: %v", err)
62 }
63}
64
65// parseRequest parses query parameters into GetTimelineRequest
66func (h *GetTimelineHandler) parseRequest(r *http.Request, userDID string) (timeline.GetTimelineRequest, error) {
67 req := timeline.GetTimelineRequest{
68 UserDID: userDID, // Set from authenticated context
69 }
70
71 // Optional: sort (default: hot)
72 req.Sort = r.URL.Query().Get("sort")
73 if req.Sort == "" {
74 req.Sort = "hot"
75 }
76
77 // Optional: timeframe (default: day for top sort)
78 req.Timeframe = r.URL.Query().Get("timeframe")
79 if req.Timeframe == "" && req.Sort == "top" {
80 req.Timeframe = "day"
81 }
82
83 // Optional: limit (default: 15, max: 50)
84 req.Limit = 15
85 if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
86 if limit, err := strconv.Atoi(limitStr); err == nil {
87 req.Limit = limit
88 }
89 }
90
91 // Optional: cursor
92 if cursor := r.URL.Query().Get("cursor"); cursor != "" {
93 req.Cursor = &cursor
94 }
95
96 return req, nil
97}