feat(search): align message result payloads
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/search"
|
||||
@@ -17,11 +19,16 @@ const chatwootSearchPerPage = 15
|
||||
// Reference: Chatwoot GlobalSearchService — cross-entity search with advanced filtering.
|
||||
type SearchHandler struct {
|
||||
svc *search.SearchService
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewSearchHandler creates a new SearchHandler.
|
||||
func NewSearchHandler(svc *search.SearchService) *SearchHandler {
|
||||
return &SearchHandler{svc: svc}
|
||||
func NewSearchHandler(svc *search.SearchService, dbs ...*gorm.DB) *SearchHandler {
|
||||
h := &SearchHandler{svc: svc}
|
||||
if len(dbs) > 0 {
|
||||
h.db = dbs[0]
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// GlobalSearch performs a unified search across conversations, messages, and contacts.
|
||||
@@ -79,7 +86,7 @@ func (h *SearchHandler) GlobalSearch(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"payload": serializeSearchPayload(result.Results)})
|
||||
c.JSON(http.StatusOK, gin.H{"payload": serializeSearchPayload(c.Request.Context(), h.db, result.Results)})
|
||||
}
|
||||
|
||||
// SearchConversations performs a conversation-only search with advanced filters.
|
||||
@@ -131,7 +138,7 @@ func (h *SearchHandler) SearchConversations(c *gin.Context) {
|
||||
}
|
||||
|
||||
_ = total
|
||||
c.JSON(http.StatusOK, gin.H{"payload": gin.H{"conversations": serializeSearchConversations(results)}})
|
||||
c.JSON(http.StatusOK, gin.H{"payload": gin.H{"conversations": serializeSearchConversations(c.Request.Context(), h.db, results)}})
|
||||
}
|
||||
|
||||
// SearchMessages performs a message-only search with advanced filters.
|
||||
@@ -182,7 +189,7 @@ func (h *SearchHandler) SearchMessages(c *gin.Context) {
|
||||
}
|
||||
|
||||
_ = total
|
||||
c.JSON(http.StatusOK, gin.H{"payload": gin.H{"messages": serializeSearchMessages(results)}})
|
||||
c.JSON(http.StatusOK, gin.H{"payload": gin.H{"messages": serializeSearchMessages(c.Request.Context(), h.db, results)}})
|
||||
}
|
||||
|
||||
// SearchContacts performs a contact-only search with advanced filters.
|
||||
@@ -278,11 +285,11 @@ func (h *SearchHandler) SearchArticles(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"payload": gin.H{"articles": serializeSearchArticles(results)}})
|
||||
}
|
||||
|
||||
func serializeSearchPayload(results []search.SearchResult) gin.H {
|
||||
func serializeSearchPayload(ctx context.Context, db *gorm.DB, results []search.SearchResult) gin.H {
|
||||
return gin.H{
|
||||
"conversations": serializeSearchConversations(filterSearchResults(results, search.ResultTypeConversation)),
|
||||
"conversations": serializeSearchConversations(ctx, db, filterSearchResults(results, search.ResultTypeConversation)),
|
||||
"contacts": serializeSearchContacts(filterSearchResults(results, search.ResultTypeContact)),
|
||||
"messages": serializeSearchMessages(filterSearchResults(results, search.ResultTypeMessage)),
|
||||
"messages": serializeSearchMessages(ctx, db, filterSearchResults(results, search.ResultTypeMessage)),
|
||||
"articles": serializeSearchArticles(filterSearchResults(results, search.ResultTypeArticle)),
|
||||
}
|
||||
}
|
||||
@@ -308,20 +315,20 @@ func filterSearchResults(results []search.SearchResult, resultType search.Search
|
||||
return filtered
|
||||
}
|
||||
|
||||
func serializeSearchConversations(results []search.SearchResult) []map[string]any {
|
||||
func serializeSearchConversations(ctx context.Context, db *gorm.DB, results []search.SearchResult) []map[string]any {
|
||||
payload := make([]map[string]any, 0, len(results))
|
||||
for _, result := range results {
|
||||
payload = append(payload, serializeSearchConversation(result))
|
||||
payload = append(payload, serializeSearchConversation(ctx, db, result))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func serializeSearchConversation(result search.SearchResult) map[string]any {
|
||||
func serializeSearchConversation(ctx context.Context, db *gorm.DB, result search.SearchResult) map[string]any {
|
||||
if conv, ok := result.Data.(model.Conversation); ok {
|
||||
return serializeSearchConversationModel(&conv)
|
||||
return serializeSearchConversationModel(ctx, db, &conv)
|
||||
}
|
||||
if conv, ok := result.Data.(*model.Conversation); ok && conv != nil {
|
||||
return serializeSearchConversationModel(conv)
|
||||
return serializeSearchConversationModel(ctx, db, conv)
|
||||
}
|
||||
root := searchDataRoot(result)
|
||||
data := nestedSearchDataFromRoot(root, "conversation")
|
||||
@@ -330,31 +337,31 @@ func serializeSearchConversation(result search.SearchResult) map[string]any {
|
||||
"account_id": firstMapValue(data, "account_id"),
|
||||
"created_at": unixFromMapValue(firstMapValue(data, "created_at", "created_at_ts")),
|
||||
"additional_attributes": firstMapValue(data, "additional_attributes"),
|
||||
"message": serializeSearchConversationMessageMap(firstNestedSearchData(root, data, "message")),
|
||||
"message": serializeSearchConversationMessageMap(ctx, db, firstNestedSearchData(root, data, "message")),
|
||||
"contact": serializeSearchConversationContactMap(firstNestedSearchData(root, data, "contact")),
|
||||
"inbox": serializeSearchConversationInboxMap(firstNestedSearchData(root, data, "inbox")),
|
||||
"agent": serializeSearchConversationAgentMap(firstNestedSearchData(root, data, "agent", "assignee")),
|
||||
}
|
||||
}
|
||||
|
||||
func serializeSearchConversationModel(conv *model.Conversation) map[string]any {
|
||||
func serializeSearchConversationModel(ctx context.Context, db *gorm.DB, conv *model.Conversation) map[string]any {
|
||||
return map[string]any{
|
||||
"id": conversationDisplayID(conv),
|
||||
"account_id": conv.AccountID,
|
||||
"created_at": conv.CreatedAt.Unix(),
|
||||
"additional_attributes": jsonObject(conv.AdditionalAttributes),
|
||||
"message": serializeSearchConversationMessageModel(conv),
|
||||
"message": serializeSearchConversationMessageModel(ctx, db, conv),
|
||||
"contact": serializeSearchConversationContactModel(conv),
|
||||
"inbox": serializeSearchConversationInboxModel(conv),
|
||||
"agent": serializeSearchConversationAgentModel(conv),
|
||||
}
|
||||
}
|
||||
|
||||
func serializeSearchConversationMessageModel(conv *model.Conversation) map[string]any {
|
||||
func serializeSearchConversationMessageModel(ctx context.Context, db *gorm.DB, conv *model.Conversation) map[string]any {
|
||||
if len(conv.Messages) == 0 {
|
||||
return map[string]any{}
|
||||
}
|
||||
return serializeSearchMessageModel(&conv.Messages[0])
|
||||
return serializeSearchMessageModel(ctx, db, &conv.Messages[0])
|
||||
}
|
||||
|
||||
func serializeSearchConversationContactModel(conv *model.Conversation) map[string]any {
|
||||
@@ -395,11 +402,11 @@ func serializeSearchConversationAgentModel(conv *model.Conversation) map[string]
|
||||
return map[string]any{}
|
||||
}
|
||||
|
||||
func serializeSearchConversationMessageMap(data map[string]any) map[string]any {
|
||||
func serializeSearchConversationMessageMap(ctx context.Context, db *gorm.DB, data map[string]any) map[string]any {
|
||||
if len(data) == 0 {
|
||||
return map[string]any{}
|
||||
}
|
||||
return serializeSearchMessage(search.SearchResult{Data: map[string]any{"message": data}})
|
||||
return serializeSearchMessage(ctx, db, search.SearchResult{Data: map[string]any{"message": data}})
|
||||
}
|
||||
|
||||
func serializeSearchConversationContactMap(data map[string]any) map[string]any {
|
||||
@@ -481,37 +488,44 @@ func serializeSearchContactModel(contact *model.Contact) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func serializeSearchMessages(results []search.SearchResult) []map[string]any {
|
||||
func serializeSearchMessages(ctx context.Context, db *gorm.DB, results []search.SearchResult) []map[string]any {
|
||||
payload := make([]map[string]any, 0, len(results))
|
||||
for _, result := range results {
|
||||
payload = append(payload, serializeSearchMessage(result))
|
||||
payload = append(payload, serializeSearchMessage(ctx, db, result))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func serializeSearchMessage(result search.SearchResult) map[string]any {
|
||||
func serializeSearchMessage(ctx context.Context, db *gorm.DB, result search.SearchResult) map[string]any {
|
||||
if message, ok := result.Data.(model.Message); ok {
|
||||
return serializeSearchMessageModel(&message)
|
||||
return serializeSearchMessageModel(ctx, db, &message)
|
||||
}
|
||||
if message, ok := result.Data.(*model.Message); ok && message != nil {
|
||||
return serializeSearchMessageModel(message)
|
||||
return serializeSearchMessageModel(ctx, db, message)
|
||||
}
|
||||
data := nestedSearchData(result, "message")
|
||||
return map[string]any{
|
||||
"id": firstMapValue(data, "id"),
|
||||
"content": firstMapValue(data, "content"),
|
||||
"account_id": firstMapValue(data, "account_id"),
|
||||
"inbox_id": firstMapValue(data, "inbox_id"),
|
||||
"conversation_id": firstMapValue(data, "conversation_id"),
|
||||
"message_type": normalizeSearchMessageType(firstMapValue(data, "message_type")),
|
||||
"content_type": firstMapValue(data, "content_type"),
|
||||
"status": firstMapValue(data, "status"),
|
||||
"content_attributes": firstMapValue(data, "content_attributes"),
|
||||
"additional_attributes": firstMapValue(data, "additional_attributes"),
|
||||
"created_at": unixFromMapValue(firstMapValue(data, "created_at", "created_at_ts")),
|
||||
"private": firstMapValue(data, "private"),
|
||||
"source_id": firstMapValue(data, "source_id"),
|
||||
if db != nil {
|
||||
if payload, ok := loadSearchMessagePayload(ctx, db, uintFromAny(firstMapValue(data, "id")), result.AccountID); ok {
|
||||
return payload
|
||||
}
|
||||
}
|
||||
return omitNilSearchMessageFields(map[string]any{
|
||||
"id": firstMapValue(data, "id"),
|
||||
"content": firstMapValue(data, "content"),
|
||||
"account_id": firstMapValue(data, "account_id"),
|
||||
"inbox_id": firstMapValue(data, "inbox_id"),
|
||||
"conversation_id": firstMapValue(data, "conversation_id"),
|
||||
"message_type": normalizeSearchMessageType(firstMapValue(data, "message_type")),
|
||||
"content_type": firstMapValue(data, "content_type"),
|
||||
"status": firstMapValue(data, "status"),
|
||||
"content_attributes": firstMapValue(data, "content_attributes"),
|
||||
"created_at": unixFromMapValue(firstMapValue(data, "created_at", "created_at_ts")),
|
||||
"private": firstMapValue(data, "private"),
|
||||
"source_id": firstMapValue(data, "source_id"),
|
||||
"echo_id": firstMapValue(data, "echo_id"),
|
||||
"sender": firstMapValue(data, "sender"),
|
||||
"attachments": firstMapValue(data, "attachments"),
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeSearchMessageType(value any) any {
|
||||
@@ -525,22 +539,83 @@ func normalizeSearchMessageType(value any) any {
|
||||
return messageTypeValue(s)
|
||||
}
|
||||
|
||||
func serializeSearchMessageModel(message *model.Message) map[string]any {
|
||||
return map[string]any{
|
||||
"id": message.ID,
|
||||
"content": message.Content,
|
||||
"account_id": message.AccountID,
|
||||
"inbox_id": message.InboxID,
|
||||
"conversation_id": message.ConversationID,
|
||||
"message_type": messageTypeValue(message.MessageType),
|
||||
"content_type": nonEmpty(message.ContentType, "text"),
|
||||
"status": nonEmpty(message.Status, "sent"),
|
||||
"content_attributes": jsonObject(message.ContentAttributes),
|
||||
"additional_attributes": jsonObject(message.AdditionalAttributes),
|
||||
"created_at": message.CreatedAt.Unix(),
|
||||
"private": message.Private,
|
||||
"source_id": message.SourceID,
|
||||
func serializeSearchMessageModel(ctx context.Context, db *gorm.DB, message *model.Message) map[string]any {
|
||||
if db != nil {
|
||||
if payload, ok := loadSearchMessagePayload(ctx, db, message.ID, message.AccountID); ok {
|
||||
return payload
|
||||
}
|
||||
}
|
||||
return omitNilSearchMessageFields(map[string]any{
|
||||
"id": message.ID,
|
||||
"content": message.Content,
|
||||
"account_id": message.AccountID,
|
||||
"inbox_id": message.InboxID,
|
||||
"conversation_id": message.ConversationID,
|
||||
"message_type": messageTypeValue(message.MessageType),
|
||||
"content_type": nonEmpty(message.ContentType, "text"),
|
||||
"status": nonEmpty(message.Status, "sent"),
|
||||
"content_attributes": jsonObject(message.ContentAttributes),
|
||||
"created_at": message.CreatedAt.Unix(),
|
||||
"private": message.Private,
|
||||
"source_id": message.SourceID,
|
||||
"echo_id": message.EchoID,
|
||||
})
|
||||
}
|
||||
|
||||
func loadSearchMessagePayload(ctx context.Context, db *gorm.DB, messageID uint, accountID uint) (map[string]any, bool) {
|
||||
if db == nil || messageID == 0 {
|
||||
return nil, false
|
||||
}
|
||||
var message model.Message
|
||||
q := db.WithContext(ctx).Where("id = ?", messageID)
|
||||
if accountID != 0 {
|
||||
q = q.Where("account_id = ?", accountID)
|
||||
}
|
||||
if err := q.First(&message).Error; err != nil {
|
||||
return nil, false
|
||||
}
|
||||
var conversation model.Conversation
|
||||
if err := db.WithContext(ctx).
|
||||
Where("id = ? AND account_id = ?", message.ConversationID, message.AccountID).
|
||||
First(&conversation).Error; err != nil {
|
||||
return chatwootMessagePayloadMap(serializeMessage(ctx, db, &message, nil)), true
|
||||
}
|
||||
return chatwootMessagePayloadMap(serializeMessage(ctx, db, &message, &conversation)), true
|
||||
}
|
||||
|
||||
func chatwootMessagePayloadMap(payload chatwootMessagePayload) map[string]any {
|
||||
data := map[string]any{
|
||||
"id": payload.ID,
|
||||
"content": payload.Content,
|
||||
"inbox_id": payload.InboxID,
|
||||
"conversation_id": payload.ConversationID,
|
||||
"message_type": payload.MessageType,
|
||||
"content_type": payload.ContentType,
|
||||
"status": payload.Status,
|
||||
"content_attributes": payload.ContentAttributes,
|
||||
"created_at": payload.CreatedAt,
|
||||
"private": payload.Private,
|
||||
"source_id": payload.SourceID,
|
||||
}
|
||||
if payload.EchoID != "" {
|
||||
data["echo_id"] = payload.EchoID
|
||||
}
|
||||
if len(payload.Sender) > 0 {
|
||||
data["sender"] = payload.Sender
|
||||
}
|
||||
if len(payload.Attachments) > 0 {
|
||||
data["attachments"] = payload.Attachments
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func omitNilSearchMessageFields(data map[string]any) map[string]any {
|
||||
for _, key := range []string{"echo_id", "sender", "attachments"} {
|
||||
if data[key] == nil || data[key] == "" {
|
||||
delete(data, key)
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func serializeSearchArticles(results []search.SearchResult) []map[string]any {
|
||||
@@ -670,3 +745,23 @@ func unixFromMapValue(value any) any {
|
||||
return typed
|
||||
}
|
||||
}
|
||||
|
||||
func uintFromAny(value any) uint {
|
||||
switch typed := value.(type) {
|
||||
case uint:
|
||||
return typed
|
||||
case int:
|
||||
if typed > 0 {
|
||||
return uint(typed)
|
||||
}
|
||||
case int64:
|
||||
if typed > 0 {
|
||||
return uint(typed)
|
||||
}
|
||||
case float64:
|
||||
if typed > 0 {
|
||||
return uint(typed)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user