veza/veza-backend-api/internal/handlers/chat_reaction_handler.go
senke ef386e0ae3 fix(backend): commit swagger annotation pass + missing handler methods
routes_users.go (already on main) calls settingsHandler.GetPreferences /
UpdatePreferences and gdprExportHandler.ExportJSON, but the methods only
existed in the working tree — main wouldn't compile, so deploy.yml's
build-backend job was stuck on the same compile error every run.

Bundles the WIP swagger annotation sweep across chat / marketplace /
role / settings / gdpr / etc. handlers with the regenerated swagger.json,
swagger.yaml, docs.go and openapi.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:16:57 +02:00

183 lines
5.4 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
// @Summary Add reaction
// @Description Add an emoji reaction to a specific chat message.
// @Tags Chat
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param roomId path string true "Room ID"
// @Param messageId path string true "Message ID"
// @Param reaction body AddReactionRequest true "Reaction emoji"
// @Success 201 {object} object{reaction=object}
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Forbidden"
// @Router /api/v1/chat/rooms/{roomId}/messages/{messageId}/reactions [post]
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=👍
// @Summary Remove reaction
// @Description Remove an emoji reaction from a specific chat message.
// @Tags Chat
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param roomId path string true "Room ID"
// @Param messageId path string true "Message ID"
// @Param emoji query string false "Specific emoji to remove"
// @Success 200 {object} object{success=boolean}
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
// @Failure 403 {object} handlers.APIResponse "Forbidden"
// @Router /api/v1/chat/rooms/{roomId}/messages/{messageId}/reactions [delete]
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})
}