A community based topic aggregation platform built on atproto
1// Package comments provides HTTP handlers for the comment query API.
2// These handlers follow XRPC conventions and integrate with the comments service layer.
3package comments
4
5import (
6 "encoding/json"
7 "log"
8 "net/http"
9 "strconv"
10
11 "Coves/internal/api/middleware"
12 "Coves/internal/core/comments"
13)
14
15// GetCommentsHandler handles comment retrieval for posts
16type GetCommentsHandler struct {
17 service Service
18}
19
20// Service defines the interface for comment business logic
21// This will be implemented by the comments service layer in Phase 2
22type Service interface {
23 GetComments(r *http.Request, req *GetCommentsRequest) (*comments.GetCommentsResponse, error)
24}
25
26// GetCommentsRequest represents the query parameters for fetching comments
27// Matches social.coves.feed.getComments lexicon input
28type GetCommentsRequest struct {
29 Cursor *string `json:"cursor,omitempty"`
30 ViewerDID *string `json:"-"`
31 PostURI string `json:"post"`
32 Sort string `json:"sort,omitempty"`
33 Timeframe string `json:"timeframe,omitempty"`
34 Depth int `json:"depth,omitempty"`
35 Limit int `json:"limit,omitempty"`
36}
37
38// NewGetCommentsHandler creates a new handler for fetching comments
39func NewGetCommentsHandler(service Service) *GetCommentsHandler {
40 return &GetCommentsHandler{
41 service: service,
42 }
43}
44
45// HandleGetComments handles GET /xrpc/social.coves.feed.getComments
46// Retrieves comments on a post with threading support
47func (h *GetCommentsHandler) HandleGetComments(w http.ResponseWriter, r *http.Request) {
48 // 1. Only allow GET method
49 if r.Method != http.MethodGet {
50 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
51 return
52 }
53
54 // 2. Parse query parameters
55 query := r.URL.Query()
56 post := query.Get("post")
57 sort := query.Get("sort")
58 timeframe := query.Get("timeframe")
59 depthStr := query.Get("depth")
60 limitStr := query.Get("limit")
61 cursor := query.Get("cursor")
62
63 // 3. Validate required parameters
64 if post == "" {
65 writeError(w, http.StatusBadRequest, "InvalidRequest", "post parameter is required")
66 return
67 }
68
69 // 4. Parse and validate depth with default
70 depth := 10 // Default depth
71 if depthStr != "" {
72 parsed, err := strconv.Atoi(depthStr)
73 if err != nil {
74 writeError(w, http.StatusBadRequest, "InvalidRequest", "depth must be a valid integer")
75 return
76 }
77 if parsed < 0 {
78 writeError(w, http.StatusBadRequest, "InvalidRequest", "depth must be non-negative")
79 return
80 }
81 depth = parsed
82 }
83
84 // 5. Parse and validate limit with default and max
85 limit := 50 // Default limit
86 if limitStr != "" {
87 parsed, err := strconv.Atoi(limitStr)
88 if err != nil {
89 writeError(w, http.StatusBadRequest, "InvalidRequest", "limit must be a valid integer")
90 return
91 }
92 if parsed < 1 {
93 writeError(w, http.StatusBadRequest, "InvalidRequest", "limit must be positive")
94 return
95 }
96 if parsed > 100 {
97 writeError(w, http.StatusBadRequest, "InvalidRequest", "limit cannot exceed 100")
98 return
99 }
100 limit = parsed
101 }
102
103 // 6. Validate sort parameter (if provided)
104 if sort != "" && sort != "hot" && sort != "top" && sort != "new" {
105 writeError(w, http.StatusBadRequest, "InvalidRequest",
106 "sort must be one of: hot, top, new")
107 return
108 }
109
110 // 7. Validate timeframe parameter (only valid with "top" sort)
111 if timeframe != "" {
112 if sort != "top" {
113 writeError(w, http.StatusBadRequest, "InvalidRequest",
114 "timeframe can only be used with sort=top")
115 return
116 }
117 validTimeframes := map[string]bool{
118 "hour": true, "day": true, "week": true,
119 "month": true, "year": true, "all": true,
120 }
121 if !validTimeframes[timeframe] {
122 writeError(w, http.StatusBadRequest, "InvalidRequest",
123 "timeframe must be one of: hour, day, week, month, year, all")
124 return
125 }
126 }
127
128 // 8. Extract viewer DID from context (set by OptionalAuth middleware)
129 viewerDID := middleware.GetUserDID(r)
130 var viewerPtr *string
131 if viewerDID != "" {
132 viewerPtr = &viewerDID
133 }
134
135 // 9. Build service request
136 req := &GetCommentsRequest{
137 PostURI: post,
138 Sort: sort,
139 Timeframe: timeframe,
140 Depth: depth,
141 Limit: limit,
142 Cursor: ptrOrNil(cursor),
143 ViewerDID: viewerPtr,
144 }
145
146 // 10. Call service layer
147 resp, err := h.service.GetComments(r, req)
148 if err != nil {
149 handleServiceError(w, err)
150 return
151 }
152
153 // 11. Return JSON response
154 w.Header().Set("Content-Type", "application/json")
155 w.WriteHeader(http.StatusOK)
156 if err := json.NewEncoder(w).Encode(resp); err != nil {
157 // Log encoding errors but don't return error response (headers already sent)
158 log.Printf("Failed to encode comments response: %v", err)
159 }
160}
161
162// ptrOrNil converts an empty string to nil pointer, otherwise returns pointer to string
163func ptrOrNil(s string) *string {
164 if s == "" {
165 return nil
166 }
167 return &s
168}