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