Files
spsays-bff/internal/handler/comment.go
T
2026-06-13 01:29:55 +05:30

157 lines
5.0 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/prabhupadasays/spsays-bff/internal/middleware"
"github.com/prabhupadasays/spsays-bff/internal/service"
"github.com/prabhupadasays/spsays-bff/pkg/response"
)
// CommentHandler serves the post-comment endpoints: listing top-level comments
// + a thread's replies (public), and create / like / pin / delete / report
// (auth-required).
type CommentHandler struct {
svc *service.CommentService
}
func NewCommentHandler(svc *service.CommentService) *CommentHandler {
return &CommentHandler{svc: svc}
}
type createCommentReq struct {
Body string `json:"body"`
// ParentID nil → top-level comment; otherwise the comment being replied
// to (the service flattens replies onto the thread root).
ParentID *string `json:"parent_id"`
}
type commentLikeResp struct {
LikesCount int `json:"likes_count"`
}
// List — GET /posts/{id}/comments?limit=&offset=. Top-level comments, pinned
// first then newest. Public (OptionalAuth populates viewer_has_liked).
func (h *CommentHandler) List(w http.ResponseWriter, r *http.Request) {
postID := chi.URLParam(r, "id")
viewerID := middleware.UserID(r.Context())
limit, offset := parseLimitOffset(r)
items, total, err := h.svc.List(r.Context(), postID, viewerID, limit, offset)
if err != nil {
mapSocialErr(w, err)
return
}
response.JSONWithMeta(w, http.StatusOK, items, paginationMeta(total, limit, offset))
}
// Replies — GET /comments/{id}/replies?limit=&offset=. A thread's replies,
// oldest first. Public (OptionalAuth).
func (h *CommentHandler) Replies(w http.ResponseWriter, r *http.Request) {
commentID := chi.URLParam(r, "id")
viewerID := middleware.UserID(r.Context())
limit, offset := parseLimitOffset(r)
items, total, err := h.svc.Replies(r.Context(), commentID, viewerID, limit, offset)
if err != nil {
mapSocialErr(w, err)
return
}
response.JSONWithMeta(w, http.StatusOK, items, paginationMeta(total, limit, offset))
}
// Create — POST /posts/{id}/comments. Body: {body, parent_id?}. RequireAuth.
func (h *CommentHandler) Create(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
postID := chi.URLParam(r, "id")
var req createCommentReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.Error(w, http.StatusBadRequest, "invalid request body")
return
}
// Treat an empty parent_id as a top-level comment, not a (missing) reply.
if req.ParentID != nil && *req.ParentID == "" {
req.ParentID = nil
}
c, err := h.svc.Add(r.Context(), uid, postID, req.ParentID, req.Body)
if err != nil {
mapSocialErr(w, err)
return
}
response.JSON(w, http.StatusCreated, c)
}
// Like — POST /comments/{id}/like. Idempotent. RequireAuth.
func (h *CommentHandler) Like(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
id := chi.URLParam(r, "id")
count, err := h.svc.Like(r.Context(), uid, id)
if err != nil {
mapSocialErr(w, err)
return
}
response.JSON(w, http.StatusOK, commentLikeResp{LikesCount: count})
}
// Unlike — DELETE /comments/{id}/like. Idempotent. RequireAuth.
func (h *CommentHandler) Unlike(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
id := chi.URLParam(r, "id")
count, err := h.svc.Unlike(r.Context(), uid, id)
if err != nil {
mapSocialErr(w, err)
return
}
response.JSON(w, http.StatusOK, commentLikeResp{LikesCount: count})
}
// Pin — POST /comments/{id}/pin. Post author only; top-level only. RequireAuth.
func (h *CommentHandler) Pin(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
id := chi.URLParam(r, "id")
if err := h.svc.Pin(r.Context(), uid, id); err != nil {
mapSocialErr(w, err)
return
}
response.NoContent(w)
}
// Unpin — DELETE /comments/{id}/pin. Post author only. RequireAuth.
func (h *CommentHandler) Unpin(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
id := chi.URLParam(r, "id")
if err := h.svc.Unpin(r.Context(), uid, id); err != nil {
mapSocialErr(w, err)
return
}
response.NoContent(w)
}
// Delete — DELETE /comments/{id}. Comment author or post author. RequireAuth.
// For a thread root this removes the whole thread.
func (h *CommentHandler) Delete(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
id := chi.URLParam(r, "id")
if err := h.svc.Delete(r.Context(), uid, id); err != nil {
mapSocialErr(w, err)
return
}
response.NoContent(w)
}
// Report — POST /comments/{id}/report. Body: {category, reason?}. RequireAuth.
func (h *CommentHandler) Report(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
id := chi.URLParam(r, "id")
var req reportPostReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.Error(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.svc.Report(r.Context(), uid, id, req.Category, req.Reason); err != nil {
mapSocialErr(w, err)
return
}
response.NoContent(w)
}