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}