feat(conversations): align chatwoot message serializers

This commit is contained in:
2026-06-05 01:52:07 +08:00
parent c964d6c8fd
commit 9f89cbfe7c
12 changed files with 908 additions and 208 deletions
+141 -42
View File
@@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/search"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/pagination"
@@ -52,7 +53,7 @@ func (h *ConversationHandler) List(c *gin.Context) {
// Support status filter via query param
status := c.Query("status")
var items []interface{}
var conversations []model.Conversation
var total int64
if status != "" {
@@ -61,7 +62,7 @@ func (h *ConversationHandler) List(c *gin.Context) {
handleServiceError(c, svcErr)
return
}
items = toInterfaceSlice(result)
conversations = result
total = count
} else {
result, count, svcErr := h.conversationSvc.ListByAccount(c.Request.Context(), accountID, p.Offset, p.PerPage)
@@ -69,11 +70,11 @@ func (h *ConversationHandler) List(c *gin.Context) {
handleServiceError(c, svcErr)
return
}
items = toInterfaceSlice(result)
conversations = result
total = count
}
response.OKWithMeta(c, items, p.Page, p.PerPage, total)
c.JSON(http.StatusOK, serializeConversationList(c.Request.Context(), h.conversationSvc.DB(), conversations, total))
}
// Create creates a new conversation.
@@ -97,7 +98,7 @@ func (h *ConversationHandler) Create(c *gin.Context) {
handleServiceError(c, svcErr)
return
}
response.Created(c, conversation)
c.JSON(http.StatusOK, serializeConversation(c.Request.Context(), h.conversationSvc.DB(), conversation))
}
// @Summary Get a single conversation
@@ -126,12 +127,12 @@ func (h *ConversationHandler) Get(c *gin.Context) {
return
}
conversation, svcErr := h.conversationSvc.GetByAccountAndID(c.Request.Context(), accountID, conversationID)
conversation, svcErr := h.conversationSvc.GetByAccountAndDisplayIDOrID(c.Request.Context(), accountID, conversationID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, conversation)
c.JSON(http.StatusOK, serializeConversation(c.Request.Context(), h.conversationSvc.DB(), conversation))
}
// Update updates a conversation (status, priority).
@@ -155,12 +156,17 @@ func (h *ConversationHandler) Update(c *gin.Context) {
return
}
conversation, svcErr := h.conversationSvc.Update(c.Request.Context(), accountID, conversationID, req)
conversation, svcErr := h.conversationSvc.GetByAccountAndDisplayIDOrID(c.Request.Context(), accountID, conversationID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, conversation)
conversation, svcErr = h.conversationSvc.Update(c.Request.Context(), accountID, conversation.ID, req)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, serializeConversation(c.Request.Context(), h.conversationSvc.DB(), conversation))
}
// Delete soft-deletes a conversation.
@@ -177,7 +183,12 @@ func (h *ConversationHandler) Delete(c *gin.Context) {
return
}
if svcErr := h.conversationSvc.Delete(c.Request.Context(), accountID, conversationID); svcErr != nil {
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
if svcErr := h.conversationSvc.Delete(c.Request.Context(), accountID, conversation.ID); svcErr != nil {
handleServiceError(c, svcErr)
return
}
@@ -217,12 +228,16 @@ func (h *ConversationHandler) AssignAgent(c *gin.Context) {
return
}
conversation, svcErr := h.conversationSvc.AssignAgent(c.Request.Context(), accountID, conversationID, req.AssigneeID)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
conversation, svcErr := h.conversationSvc.AssignAgent(c.Request.Context(), accountID, conversation.ID, req.AssigneeID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, conversation)
c.JSON(http.StatusOK, serializeUserFromDB(c.Request.Context(), h.conversationSvc.DB(), req.AssigneeID, accountID))
}
// @Summary Toggle conversation status
@@ -261,12 +276,21 @@ func (h *ConversationHandler) ToggleStatus(c *gin.Context) {
return
}
conversation, svcErr := h.conversationSvc.ToggleStatus(c.Request.Context(), accountID, conversationID, req)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
conversation, svcErr := h.conversationSvc.ToggleStatus(c.Request.Context(), accountID, conversation.ID, req)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, conversation)
c.JSON(http.StatusOK, gin.H{"meta": gin.H{}, "payload": gin.H{
"success": true,
"conversation_id": conversationDisplayID(conversation),
"current_status": conversation.Status,
"snoozed_until": conversation.SnoozedUntil,
}})
}
// @Summary Mute a conversation
@@ -297,12 +321,16 @@ func (h *ConversationHandler) Mute(c *gin.Context) {
return
}
conversation, svcErr := h.conversationSvc.Mute(c.Request.Context(), accountID, conversationID)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
_, svcErr := h.conversationSvc.Mute(c.Request.Context(), accountID, conversation.ID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, conversation)
c.Status(http.StatusOK)
}
// @Summary Unmute a conversation
@@ -333,12 +361,16 @@ func (h *ConversationHandler) Unmute(c *gin.Context) {
return
}
conversation, svcErr := h.conversationSvc.Unmute(c.Request.Context(), accountID, conversationID)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
_, svcErr := h.conversationSvc.Unmute(c.Request.Context(), accountID, conversation.ID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, conversation)
c.Status(http.StatusOK)
}
// UpdateLabels updates the labels on a conversation.
@@ -362,12 +394,16 @@ func (h *ConversationHandler) UpdateLabels(c *gin.Context) {
return
}
conversation, svcErr := h.conversationSvc.UpdateLabels(c.Request.Context(), accountID, conversationID, req.Labels)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
conversation, svcErr := h.conversationSvc.UpdateLabels(c.Request.Context(), accountID, conversation.ID, req.Labels)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, conversation)
c.JSON(http.StatusOK, serializeConversation(c.Request.Context(), h.conversationSvc.DB(), conversation))
}
// @Summary Search conversations
@@ -410,7 +446,7 @@ func (h *ConversationHandler) Search(c *gin.Context) {
return
}
response.OKWithMeta(c, toInterfaceSlice(conversations), p.Page, p.PerPage, total)
c.JSON(http.StatusOK, serializeConversationList(c.Request.Context(), h.conversationSvc.DB(), conversations, total))
}
// Filter retrieves conversations matching advanced filter criteria.
@@ -439,9 +475,14 @@ func (h *ConversationHandler) Filter(c *gin.Context) {
return
}
// 1:1 Chatwoot ConversationFinder response shape:
// {conversations: [...], count: {mine_count, assigned_count, unassigned_count, all_count}}
response.OKWithMeta(c, toInterfaceSlice(result.Conversations), p.Page, p.PerPage, result.Count.AllCount)
payload := serializeConversationList(c.Request.Context(), h.conversationSvc.DB(), result.Conversations, result.Count.AllCount)
payload.Data.Meta = chatwootConversationCounts{
MineCount: result.Count.MineCount,
AssignedCount: result.Count.AssignedCount,
UnassignedCount: result.Count.UnassignedCount,
AllCount: result.Count.AllCount,
}
c.JSON(http.StatusOK, payload.Data)
}
// UpdatePriority updates the priority of a conversation.
@@ -466,12 +507,16 @@ func (h *ConversationHandler) UpdatePriority(c *gin.Context) {
return
}
conversation, svcErr := h.conversationSvc.UpdatePriority(c.Request.Context(), accountID, conversationID, req.Priority)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
conversation, svcErr := h.conversationSvc.UpdatePriority(c.Request.Context(), accountID, conversation.ID, req.Priority)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, conversation)
c.JSON(http.StatusOK, serializeConversation(c.Request.Context(), h.conversationSvc.DB(), conversation))
}
// TogglePriority updates a conversation priority using Chatwoot's member action path.
@@ -501,7 +546,11 @@ func (h *ConversationHandler) TogglePriority(c *gin.Context) {
priority = *req.Priority
}
if _, svcErr := h.conversationSvc.UpdatePriority(c.Request.Context(), accountID, conversationID, priority); svcErr != nil {
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
if _, svcErr := h.conversationSvc.UpdatePriority(c.Request.Context(), accountID, conversation.ID, priority); svcErr != nil {
handleServiceError(c, svcErr)
return
}
@@ -511,21 +560,30 @@ func (h *ConversationHandler) TogglePriority(c *gin.Context) {
// ListMessages lists messages in a conversation.
// GET /api/v1/accounts/:account_id/conversations/:conversation_id/messages
func (h *ConversationHandler) ListMessages(c *gin.Context) {
accountID, err := parseUintParam(c, "account_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
conversationID, err := parseUintParam(c, "conversation_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation_id")
return
}
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
p := pagination.Parse(c)
messages, total, svcErr := h.conversationSvc.ListMessages(c.Request.Context(), conversationID, p.Offset, p.PerPage)
messages, _, svcErr := h.conversationSvc.ListMessages(c.Request.Context(), conversation.ID, p.Offset, p.PerPage)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OKWithMeta(c, toInterfaceSlice(messages), p.Page, p.PerPage, total)
c.JSON(http.StatusOK, serializeMessageIndex(c.Request.Context(), h.conversationSvc.DB(), conversation, messages))
}
// Meta retrieves aggregated conversation metadata (status counts, label counts) for an account.
@@ -563,13 +621,17 @@ func (h *ConversationHandler) Unread(c *gin.Context) {
return
}
conversation, svcErr := h.conversationSvc.MarkUnread(c.Request.Context(), accountID, conversationID)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
conversation, svcErr := h.conversationSvc.MarkUnread(c.Request.Context(), accountID, conversation.ID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, conversation)
c.JSON(http.StatusOK, serializeConversation(c.Request.Context(), h.conversationSvc.DB(), conversation))
}
// Transcript sends a conversation transcript via email.
@@ -596,7 +658,11 @@ func (h *ConversationHandler) Transcript(c *gin.Context) {
return
}
if svcErr := h.conversationSvc.SendTranscript(c.Request.Context(), accountID, conversationID, req.Email); svcErr != nil {
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
if svcErr := h.conversationSvc.SendTranscript(c.Request.Context(), accountID, conversation.ID, req.Email); svcErr != nil {
handleServiceError(c, svcErr)
return
}
@@ -628,13 +694,17 @@ func (h *ConversationHandler) UpdateCustomAttributes(c *gin.Context) {
return
}
conversation, svcErr := h.conversationSvc.UpdateCustomAttributes(c.Request.Context(), accountID, conversationID, req.CustomAttributes)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
conversation, svcErr := h.conversationSvc.UpdateCustomAttributes(c.Request.Context(), accountID, conversation.ID, req.CustomAttributes)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, conversation)
c.JSON(http.StatusOK, serializeConversation(c.Request.Context(), h.conversationSvc.DB(), conversation))
}
// ListAttachments returns paginated message attachments for a conversation.
@@ -654,13 +724,17 @@ func (h *ConversationHandler) ListAttachments(c *gin.Context) {
}
p := pagination.Parse(c)
attachments, total, svcErr := h.messageSvc.ListAttachments(c.Request.Context(), accountID, conversationID, p.Offset, p.PerPage)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
attachments, _, svcErr := h.messageSvc.ListAttachments(c.Request.Context(), accountID, conversation.ID, p.Offset, p.PerPage)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OKWithMeta(c, toInterfaceSlice(attachments), p.Page, p.PerPage, total)
c.JSON(http.StatusOK, gin.H{"payload": attachments})
}
// ToggleTyping toggles the typing status for an agent in a conversation.
@@ -687,13 +761,17 @@ func (h *ConversationHandler) ToggleTyping(c *gin.Context) {
return
}
svcErr := h.conversationSvc.ToggleTyping(c.Request.Context(), accountID, conversationID, req.TypingStatus)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
svcErr := h.conversationSvc.ToggleTyping(c.Request.Context(), accountID, conversation.ID, req.TypingStatus)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, gin.H{"typing_status": req.TypingStatus})
c.Status(http.StatusOK)
}
// UpdateLastSeen updates the agent's last seen timestamp for a conversation.
@@ -712,13 +790,17 @@ func (h *ConversationHandler) UpdateLastSeen(c *gin.Context) {
return
}
svcErr := h.conversationSvc.UpdateLastSeen(c.Request.Context(), accountID, conversationID)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
svcErr := h.conversationSvc.UpdateLastSeen(c.Request.Context(), accountID, conversation.ID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, nil)
c.Status(http.StatusOK)
}
// @Summary Assign a team to a conversation
@@ -761,13 +843,21 @@ func (h *ConversationHandler) AssignTeam(c *gin.Context) {
return
}
conversation, svcErr := h.conversationSvc.AssignTeam(c.Request.Context(), accountID, conversationID, req.AgentID, req.TeamID)
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
if !ok {
return
}
conversation, svcErr := h.conversationSvc.AssignTeam(c.Request.Context(), accountID, conversation.ID, req.AgentID, req.TeamID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, conversation)
if req.AgentID != nil {
c.JSON(http.StatusOK, serializeUserFromDB(c.Request.Context(), h.conversationSvc.DB(), *req.AgentID, accountID))
return
}
c.JSON(http.StatusOK, serializeConversation(c.Request.Context(), h.conversationSvc.DB(), conversation))
}
// handleServiceError maps service-layer errors to appropriate HTTP responses.
@@ -798,6 +888,15 @@ func toInterfaceSlice[T any](slice []T) []interface{} {
return result
}
func (h *ConversationHandler) resolveConversationRoute(c *gin.Context, accountID, routeID uint) (*model.Conversation, bool) {
conversation, svcErr := h.conversationSvc.GetByAccountAndDisplayIDOrID(c.Request.Context(), accountID, routeID)
if svcErr != nil {
handleServiceError(c, svcErr)
return nil, false
}
return conversation, true
}
// UnreadCounts returns unread conversation counts grouped by inbox, label, and team.
// GET /api/v1/accounts/:account_id/conversations/unread_counts
// Reference: Chatwoot app/controllers/api/v1/accounts/conversations/unread_counts_controller.rb