veza/veza-backend-api/internal/handlers/chat_search_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

117 lines
2.9 KiB
Go

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,
})
}