veza/veza-backend-api/internal/handlers/chat_reaction_handler.go
senke a1000ce7fb style(backend): gofmt -w on 85 files (whitespace only)
backend-ci.yml's `test -z "$(gofmt -l .)"` strict gate (added in
13c21ac11) failed on a backlog of unformatted files. None of the
85 files in this commit had been edited since the gate was added
because no push touched veza-backend-api/** in between, so the
gate never fired until today's CI fixes triggered it.

The diff is exclusively whitespace alignment in struct literals
and trailing-space comments. `go build ./...` and the full test
suite (with VEZA_SKIP_INTEGRATION=1 -short) pass identically.
2026-04-14 12:22:14 +02:00

157 lines
4.3 KiB
Go

package handlers
import (
"net/http"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/models"
"veza-backend-api/internal/repositories"
chatws "veza-backend-api/internal/websocket/chat"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// ChatReactionHandler handles REST API for chat message reactions (v0.9.6)
type ChatReactionHandler struct {
reactionRepo *repositories.ReactionRepository
msgRepo *repositories.ChatMessageRepository
permissions *chatws.PermissionService
logger *zap.Logger
}
// NewChatReactionHandler creates a new ChatReactionHandler
func NewChatReactionHandler(
reactionRepo *repositories.ReactionRepository,
msgRepo *repositories.ChatMessageRepository,
permissions *chatws.PermissionService,
logger *zap.Logger,
) *ChatReactionHandler {
if logger == nil {
logger = zap.NewNop()
}
return &ChatReactionHandler{
reactionRepo: reactionRepo,
msgRepo: msgRepo,
permissions: permissions,
logger: logger,
}
}
// AddReactionRequest body for POST
type AddReactionRequest struct {
Emoji string `json:"emoji" binding:"required" validate:"required,max=50"`
}
// AddReaction adds a reaction to a message
// POST /api/v1/chat/rooms/:roomId/messages/:messageId/reactions
func (h *ChatReactionHandler) AddReaction(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
roomID, err := uuid.Parse(c.Param("roomId"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("Invalid room ID"))
return
}
messageID, err := uuid.Parse(c.Param("messageId"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("Invalid message ID"))
return
}
if !h.permissions.CanRead(c.Request.Context(), userID, roomID) {
RespondWithAppError(c, apperrors.NewForbiddenError("Not allowed to access this conversation"))
return
}
var req AddReactionRequest
if err := c.ShouldBindJSON(&req); err != nil || req.Emoji == "" {
RespondWithAppError(c, apperrors.NewValidationError("emoji is required"))
return
}
msg, err := h.msgRepo.GetByID(c.Request.Context(), messageID)
if err != nil {
RespondWithAppError(c, apperrors.NewNotFoundError("Message"))
return
}
if msg.ConversationID != roomID {
RespondWithAppError(c, apperrors.NewValidationError("Message does not belong to this room"))
return
}
reaction := &models.MessageReaction{
ID: uuid.New(),
UserID: userID,
MessageID: messageID,
Emoji: req.Emoji,
}
if err := h.reactionRepo.Add(c.Request.Context(), reaction); err != nil {
h.logger.Error("Failed to add reaction", zap.Error(err))
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to add reaction", err))
return
}
RespondSuccess(c, http.StatusCreated, gin.H{
"reaction": gin.H{
"id": reaction.ID,
"user_id": reaction.UserID,
"message_id": reaction.MessageID,
"emoji": reaction.Emoji,
},
})
}
// RemoveReaction removes a reaction from a message
// DELETE /api/v1/chat/rooms/:roomId/messages/:messageId/reactions?emoji=👍
func (h *ChatReactionHandler) RemoveReaction(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
roomID, err := uuid.Parse(c.Param("roomId"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("Invalid room ID"))
return
}
messageID, err := uuid.Parse(c.Param("messageId"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("Invalid message ID"))
return
}
if !h.permissions.CanRead(c.Request.Context(), userID, roomID) {
RespondWithAppError(c, apperrors.NewForbiddenError("Not allowed to access this conversation"))
return
}
msg, err := h.msgRepo.GetByID(c.Request.Context(), messageID)
if err != nil {
RespondWithAppError(c, apperrors.NewNotFoundError("Message"))
return
}
if msg.ConversationID != roomID {
RespondWithAppError(c, apperrors.NewValidationError("Message does not belong to this room"))
return
}
emoji := c.Query("emoji")
if emoji != "" {
err = h.reactionRepo.RemoveByEmoji(c.Request.Context(), userID, messageID, emoji)
} else {
err = h.reactionRepo.Remove(c.Request.Context(), userID, messageID)
}
if err != nil {
h.logger.Error("Failed to remove reaction", zap.Error(err))
RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to remove reaction", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"success": true})
}