package handlers import ( "net/http" "veza-backend-api/internal/models" "veza-backend-api/internal/repositories" apperrors "veza-backend-api/internal/errors" 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}) }