diff --git a/internal/api/handlers/communityFeed/errors.go b/internal/api/handlers/communityFeed/errors.go new file mode 100644 --- /dev/null +++ b/internal/api/handlers/communityFeed/errors.go @@ -0,0 +1,46 @@ +package communityFeed + +import ( + "Coves/internal/core/communityFeeds" + "encoding/json" + "errors" + "log" + "net/http" +) + +// ErrorResponse represents an XRPC error response +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` +} + +// writeError writes a JSON error response +func writeError(w http.ResponseWriter, statusCode int, errorType, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + if err := json.NewEncoder(w).Encode(ErrorResponse{ + Error: errorType, + Message: message, + }); err != nil { + // Log encoding errors but can't send error response (headers already sent) + log.Printf("ERROR: Failed to encode error response: %v", err) + } +} + +// handleServiceError maps service errors to HTTP responses +func handleServiceError(w http.ResponseWriter, err error) { + switch { + case errors.Is(err, communityFeeds.ErrCommunityNotFound): + writeError(w, http.StatusNotFound, "CommunityNotFound", "Community not found") + + case errors.Is(err, communityFeeds.ErrInvalidCursor): + writeError(w, http.StatusBadRequest, "InvalidCursor", "Invalid pagination cursor") + + case communityFeeds.IsValidationError(err): + writeError(w, http.StatusBadRequest, "InvalidRequest", err.Error()) + + default: + // Internal server error - don't leak details + writeError(w, http.StatusInternalServerError, "InternalServerError", "An internal error occurred") + } +} diff --git a/internal/api/handlers/communityFeed/get_community.go b/internal/api/handlers/communityFeed/get_community.go new file mode 100644 --- /dev/null +++ b/internal/api/handlers/communityFeed/get_community.go @@ -0,0 +1,91 @@ +package communityFeed + +import ( + "Coves/internal/core/communityFeeds" + "encoding/json" + "log" + "net/http" + "strconv" +) + +// GetCommunityHandler handles community feed retrieval +type GetCommunityHandler struct { + service communityFeeds.Service +} + +// NewGetCommunityHandler creates a new community feed handler +func NewGetCommunityHandler(service communityFeeds.Service) *GetCommunityHandler { + return &GetCommunityHandler{ + service: service, + } +} + +// HandleGetCommunity retrieves posts from a community with sorting +// GET /xrpc/social.coves.communityFeed.getCommunity?community={did_or_handle}&sort=hot&limit=15&cursor=... +func (h *GetCommunityHandler) HandleGetCommunity(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse query parameters + req, err := h.parseRequest(r) + if err != nil { + writeError(w, http.StatusBadRequest, "InvalidRequest", err.Error()) + return + } + + // Alpha: No viewer context needed for basic community sorting + // TODO(feed-generator): Extract viewer DID when implementing viewer-specific state + // (blocks, upvotes, saves) in feed generator skeleton + + // Get community feed + response, err := h.service.GetCommunityFeed(r.Context(), req) + if err != nil { + handleServiceError(w, err) + return + } + + // Return feed + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(response); err != nil { + // Log encoding errors but don't return error response (headers already sent) + log.Printf("ERROR: Failed to encode feed response: %v", err) + } +} + +// parseRequest parses query parameters into GetCommunityFeedRequest +func (h *GetCommunityHandler) parseRequest(r *http.Request) (communityFeeds.GetCommunityFeedRequest, error) { + req := communityFeeds.GetCommunityFeedRequest{} + + // Required: community + req.Community = r.URL.Query().Get("community") + + // Optional: sort (default: hot) + req.Sort = r.URL.Query().Get("sort") + if req.Sort == "" { + req.Sort = "hot" + } + + // Optional: timeframe (default: day for top sort) + req.Timeframe = r.URL.Query().Get("timeframe") + if req.Timeframe == "" && req.Sort == "top" { + req.Timeframe = "day" + } + + // Optional: limit (default: 15, max: 50) + req.Limit = 15 + if limitStr := r.URL.Query().Get("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + req.Limit = limit + } + } + + // Optional: cursor + if cursor := r.URL.Query().Get("cursor"); cursor != "" { + req.Cursor = &cursor + } + + return req, nil +} diff --git a/internal/api/routes/communityFeed.go b/internal/api/routes/communityFeed.go new file mode 100644 --- /dev/null +++ b/internal/api/routes/communityFeed.go @@ -0,0 +1,23 @@ +package routes + +import ( + "Coves/internal/api/handlers/communityFeed" + "Coves/internal/core/communityFeeds" + + "github.com/go-chi/chi/v5" +) + +// RegisterCommunityFeedRoutes registers feed-related XRPC endpoints +func RegisterCommunityFeedRoutes( + r chi.Router, + feedService communityFeeds.Service, +) { + // Create handlers + getCommunityHandler := communityFeed.NewGetCommunityHandler(feedService) + + // GET /xrpc/social.coves.communityFeed.getCommunity + // Public endpoint - basic community sorting only for Alpha + // TODO(feed-generator): Add OptionalAuth middleware when implementing viewer-specific state + // (blocks, upvotes, saves, etc.) in feed generator skeleton + r.Get("/xrpc/social.coves.communityFeed.getCommunity", getCommunityHandler.HandleGetCommunity) +}