284 lines
13 KiB
Go
284 lines
13 KiB
Go
package v1
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/gochat/gochat/internal/search"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
// SearchHandler handles global search API endpoints.
|
|
// Reference: Chatwoot GlobalSearchService — cross-entity search with advanced filtering.
|
|
type SearchHandler struct {
|
|
svc *search.SearchService
|
|
}
|
|
|
|
// NewSearchHandler creates a new SearchHandler.
|
|
func NewSearchHandler(svc *search.SearchService) *SearchHandler {
|
|
return &SearchHandler{svc: svc}
|
|
}
|
|
|
|
// GlobalSearch performs a unified search across conversations, messages, and contacts.
|
|
// GET /api/v1/accounts/:account_id/search?q=xxx&types=conversation,message&status=open&assignee_id=1
|
|
// Reference: Chatwoot GlobalSearchService — searches across conversations, messages, contacts.
|
|
|
|
// @Summary Global search across all entity types
|
|
// @Description Searches across conversations, messages, contacts, and articles with advanced filtering and pagination
|
|
// @Tags Search
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param account_id path uint true "Account ID"
|
|
// @Param q query string false "Search query string"
|
|
// @Param types query string false "Entity types to search (conversation,message,contact,article)" default(conversation,message,contact,article)
|
|
// @Param search_mode query string false "Search mode: ilike (substring) or trigram (fuzzy)" default(ilike)
|
|
// @Param status query string false "Conversation status filter (open,resolved,pending,snoozed)"
|
|
// @Param priority query string false "Conversation priority filter (none,low,medium,high,urgent)"
|
|
// @Param assignee_id query int false "Assignee agent ID filter"
|
|
// @Param team_id query int false "Team ID filter"
|
|
// @Param inbox_id query int false "Inbox ID filter"
|
|
// @Param labels query string false "Label filter (comma-separated)"
|
|
// @Param contact_source query string false "Contact source filter (email,phone,website,api)"
|
|
// @Param message_type query string false "Message type filter (incoming,outgoing,activity)"
|
|
// @Param sender_type query string false "Sender type filter"
|
|
// @Param content_type query string false "Content type filter (text,input_email,card)"
|
|
// @Param private query bool false "Private message filter"
|
|
// @Param date_from query string false "Date range start (ISO 8601)"
|
|
// @Param date_to query string false "Date range end (ISO 8601)"
|
|
// @Param portal_id query int false "Portal ID filter (for article search)"
|
|
// @Param article_status query string false "Article status filter (draft,published,archived)"
|
|
// @Param article_locale query string false "Article locale filter"
|
|
// @Param sort_by query string false "Sort field (created_at,last_activity_at,updated_at)" default(created_at)
|
|
// @Param sort_order query string false "Sort order (asc,desc)" default(desc)
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param per_page query int false "Items per page" default(25)
|
|
// @Success 200 {object} search.SearchResponse
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{account_id}/search [get]
|
|
func (h *SearchHandler) GlobalSearch(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
query := c.Query("q")
|
|
filter := search.ParseSearchFilter(c)
|
|
|
|
result, svcErr := h.svc.GlobalSearch(c.Request.Context(), accountID, query, &filter)
|
|
if svcErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "search failed")
|
|
return
|
|
}
|
|
|
|
response.OKWithMeta(c, result, result.Page, result.PerPage, result.TotalCount)
|
|
}
|
|
|
|
// SearchConversations performs a conversation-only search with advanced filters.
|
|
// GET /api/v1/accounts/:account_id/search/conversations?q=xxx&status=open&assignee_id=1
|
|
// Reference: Chatwoot conversations#index with filter params.
|
|
|
|
// @Summary Search conversations
|
|
// @Description Searches conversations by query string with advanced filtering (status, assignee, labels, date range)
|
|
// @Tags Search
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param account_id path uint true "Account ID"
|
|
// @Param q query string true "Search query string"
|
|
// @Param search_mode query string false "Search mode: ilike (substring) or trigram (fuzzy)" default(ilike)
|
|
// @Param status query string false "Conversation status filter (open,resolved,pending,snoozed)"
|
|
// @Param priority query string false "Conversation priority filter (none,low,medium,high,urgent)"
|
|
// @Param assignee_id query int false "Assignee agent ID filter"
|
|
// @Param team_id query int false "Team ID filter"
|
|
// @Param inbox_id query int false "Inbox ID filter"
|
|
// @Param labels query string false "Label filter (comma-separated)"
|
|
// @Param date_from query string false "Date range start (ISO 8601)"
|
|
// @Param date_to query string false "Date range end (ISO 8601)"
|
|
// @Param sort_by query string false "Sort field (created_at,last_activity_at,updated_at)" default(created_at)
|
|
// @Param sort_order query string false "Sort order (asc,desc)" default(desc)
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param per_page query int false "Items per page" default(25)
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{account_id}/search/conversations [get]
|
|
func (h *SearchHandler) SearchConversations(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
query := c.Query("q")
|
|
filter := search.ParseSearchFilter(c)
|
|
// Force type to conversations only
|
|
filter.Types = []search.SearchResultType{search.ResultTypeConversation}
|
|
|
|
results, total, svcErr := h.svc.SearchConversations(c.Request.Context(), accountID, query, &filter)
|
|
if svcErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "conversation search failed")
|
|
return
|
|
}
|
|
|
|
response.OKWithMeta(c, gin.H{
|
|
"results": results,
|
|
"by_type": gin.H{"conversation": total},
|
|
}, filter.Page, filter.PerPage, total)
|
|
}
|
|
|
|
// SearchMessages performs a message-only search with advanced filters.
|
|
// GET /api/v1/accounts/:account_id/search/messages?q=xxx&message_type=incoming&private=false
|
|
// Reference: Chatwoot messages search — full text search on message content.
|
|
|
|
// @Summary Search messages
|
|
// @Description Searches messages by query string with advanced filtering (message type, sender type, content type, private)
|
|
// @Tags Search
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param account_id path uint true "Account ID"
|
|
// @Param q query string true "Search query string"
|
|
// @Param search_mode query string false "Search mode: ilike (substring) or trigram (fuzzy)" default(ilike)
|
|
// @Param message_type query string false "Message type filter (incoming,outgoing,activity)"
|
|
// @Param sender_type query string false "Sender type filter"
|
|
// @Param content_type query string false "Content type filter (text,input_email,card)"
|
|
// @Param private query bool false "Private message filter"
|
|
// @Param inbox_id query int false "Inbox ID filter"
|
|
// @Param date_from query string false "Date range start (ISO 8601)"
|
|
// @Param date_to query string false "Date range end (ISO 8601)"
|
|
// @Param sort_by query string false "Sort field (created_at,updated_at)" default(created_at)
|
|
// @Param sort_order query string false "Sort order (asc,desc)" default(desc)
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param per_page query int false "Items per page" default(25)
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{account_id}/search/messages [get]
|
|
func (h *SearchHandler) SearchMessages(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
query := c.Query("q")
|
|
filter := search.ParseSearchFilter(c)
|
|
// Force type to messages only
|
|
filter.Types = []search.SearchResultType{search.ResultTypeMessage}
|
|
|
|
results, total, svcErr := h.svc.SearchMessages(c.Request.Context(), accountID, query, &filter)
|
|
if svcErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "message search failed")
|
|
return
|
|
}
|
|
|
|
response.OKWithMeta(c, gin.H{
|
|
"results": results,
|
|
"by_type": gin.H{"message": total},
|
|
}, filter.Page, filter.PerPage, total)
|
|
}
|
|
|
|
// SearchContacts performs a contact-only search with advanced filters.
|
|
// GET /api/v1/accounts/:account_id/search/contacts?q=xxx&contact_source=email
|
|
// Reference: Chatwoot contacts#search — name, email, phone, identifier.
|
|
|
|
// @Summary Search contacts
|
|
// @Description Searches contacts by query string (name, email, phone, identifier) with advanced filtering
|
|
// @Tags Search
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param account_id path uint true "Account ID"
|
|
// @Param q query string true "Search query string"
|
|
// @Param search_mode query string false "Search mode: ilike (substring) or trigram (fuzzy)" default(ilike)
|
|
// @Param contact_source query string false "Contact source filter (email,phone,website,api)"
|
|
// @Param sort_by query string false "Sort field (created_at,updated_at,name)" default(created_at)
|
|
// @Param sort_order query string false "Sort order (asc,desc)" default(desc)
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param per_page query int false "Items per page" default(25)
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{account_id}/search/contacts [get]
|
|
func (h *SearchHandler) SearchContacts(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
query := c.Query("q")
|
|
filter := search.ParseSearchFilter(c)
|
|
// Force type to contacts only
|
|
filter.Types = []search.SearchResultType{search.ResultTypeContact}
|
|
|
|
results, total, svcErr := h.svc.SearchContacts(c.Request.Context(), accountID, query, &filter)
|
|
if svcErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "contact search failed")
|
|
return
|
|
}
|
|
|
|
response.OKWithMeta(c, gin.H{
|
|
"results": results,
|
|
"by_type": gin.H{"contact": total},
|
|
}, filter.Page, filter.PerPage, total)
|
|
}
|
|
|
|
// SearchArticles performs a knowledge base article-only search with advanced filters.
|
|
// GET /api/v1/accounts/:account_id/search/articles?q=xxx&portal_id=1&article_status=published&locale=en
|
|
// Reference: Chatwoot ArticlesController#search — full text search on article title, description, content.
|
|
|
|
// @Summary Search knowledge base articles
|
|
// @Description Searches knowledge base articles by query string (title, description, content) with portal, status, and locale filters
|
|
// @Tags Search
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param account_id path uint true "Account ID"
|
|
// @Param q query string true "Search query string"
|
|
// @Param search_mode query string false "Search mode: ilike (substring) or trigram (fuzzy)" default(ilike)
|
|
// @Param portal_id query int false "Portal ID filter"
|
|
// @Param article_status query string false "Article status filter (draft,published,archived)"
|
|
// @Param article_locale query string false "Article locale filter (en,es,fr,de,pt,etc)"
|
|
// @Param author_id query int false "Author ID filter"
|
|
// @Param sort_by query string false "Sort field (created_at,updated_at)" default(created_at)
|
|
// @Param sort_order query string false "Sort order (asc,desc)" default(desc)
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param per_page query int false "Items per page" default(25)
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{account_id}/search/articles [get]
|
|
func (h *SearchHandler) SearchArticles(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
query := c.Query("q")
|
|
filter := search.ParseSearchFilter(c)
|
|
// Force type to articles only
|
|
filter.Types = []search.SearchResultType{search.ResultTypeArticle}
|
|
|
|
results, total, svcErr := h.svc.SearchArticles(c.Request.Context(), accountID, query, &filter)
|
|
if svcErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "article search failed")
|
|
return
|
|
}
|
|
|
|
response.OKWithMeta(c, gin.H{
|
|
"results": results,
|
|
"by_type": gin.H{"article": total},
|
|
}, filter.Page, filter.PerPage, total)
|
|
}
|