package handlers import ( "net/http" "strconv" apperrors "veza-backend-api/internal/errors" "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" ) // ChatSearchHandler handles REST API for chat message search (v0.9.6) type ChatSearchHandler struct { msgRepo *repositories.ChatMessageRepository permissions *chatws.PermissionService logger *zap.Logger } // NewChatSearchHandler creates a new ChatSearchHandler func NewChatSearchHandler( msgRepo *repositories.ChatMessageRepository, permissions *chatws.PermissionService, logger *zap.Logger, ) *ChatSearchHandler { if logger == nil { logger = zap.NewNop() } return &ChatSearchHandler{ msgRepo: msgRepo, permissions: permissions, logger: logger, } } const ( defaultSearchLimit = 20 maxSearchLimit = 100 ) // SearchMessages searches messages in a room // GET /api/v1/chat/rooms/:roomId/messages/search?q=...&limit=20&offset=0 func (h *ChatSearchHandler) SearchMessages(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 } if !h.permissions.CanRead(c.Request.Context(), userID, roomID) { RespondWithAppError(c, apperrors.NewForbiddenError("Not allowed to access this conversation")) return } query := c.Query("q") if query == "" { RespondWithAppError(c, apperrors.NewValidationError("query parameter 'q' is required")) return } limit := defaultSearchLimit if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > maxSearchLimit { limit = maxSearchLimit } } } offset := 0 if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed > 0 { offset = parsed } } messages, total, err := h.msgRepo.Search(c.Request.Context(), roomID, query, limit, offset) if err != nil { h.logger.Error("Failed to search messages", zap.Error(err)) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to search messages", err)) return } // Convert to response format out := make([]gin.H, 0, len(messages)) for _, m := range messages { out = append(out, gin.H{ "id": m.ID, "conversation_id": m.ConversationID, "sender_id": m.SenderID, "content": m.Content, "message_type": m.MessageType, "parent_message_id": m.ParentMessageID, "reply_to_id": m.ReplyToID, "is_pinned": m.IsPinned, "is_edited": m.IsEdited, "is_deleted": m.IsDeleted, "edited_at": m.EditedAt, "status": m.Status, "created_at": m.CreatedAt, "updated_at": m.UpdatedAt, }) } RespondSuccess(c, http.StatusOK, gin.H{ "messages": out, "total": total, }) }