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