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.
157 lines
4.3 KiB
Go
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})
|
|
}
|