A community based topic aggregation platform built on atproto
at main 4.8 kB view raw
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}