feat(conversations): align chatwoot message serializers
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -194,18 +194,18 @@ func (s *ConversationCrudTestSuite) TestList_Success() {
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data"`
|
||||
Meta struct {
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
} `json:"meta"`
|
||||
Data struct {
|
||||
Meta struct {
|
||||
AllCount int64 `json:"all_count"`
|
||||
} `json:"meta"`
|
||||
Payload []map[string]interface{} `json:"payload"`
|
||||
} `json:"data"`
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Equal(s.T(), int64(1), resp.Meta.TotalCount)
|
||||
assert.Equal(s.T(), int64(1), resp.Data.Meta.AllCount)
|
||||
assert.Len(s.T(), resp.Data.Payload, 1)
|
||||
assert.NotNil(s.T(), resp.Data.Payload[0]["meta"])
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestList_WithStatusFilter() {
|
||||
@@ -216,15 +216,15 @@ func (s *ConversationCrudTestSuite) TestList_WithStatusFilter() {
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data"`
|
||||
Meta struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
} `json:"meta"`
|
||||
Data struct {
|
||||
Meta struct {
|
||||
AllCount int64 `json:"all_count"`
|
||||
} `json:"meta"`
|
||||
} `json:"data"`
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Equal(s.T(), int64(1), resp.Data.Meta.AllCount)
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestList_InvalidAccountID() {
|
||||
@@ -247,16 +247,13 @@ func (s *ConversationCrudTestSuite) TestCreate_Success() {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusCreated, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.NotNil(s.T(), resp.Data["id"])
|
||||
assert.NotNil(s.T(), resp["id"])
|
||||
assert.NotNil(s.T(), resp["meta"])
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestCreate_InvalidAccountID() {
|
||||
@@ -304,14 +301,11 @@ func (s *ConversationCrudTestSuite) TestGet_Success() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Equal(s.T(), float64(s.testConv.ID), resp.Data["id"])
|
||||
assert.Equal(s.T(), float64(s.testConv.ID), resp["id"])
|
||||
assert.NotNil(s.T(), resp["messages"])
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestGet_InvalidAccountID() {
|
||||
@@ -352,14 +346,10 @@ func (s *ConversationCrudTestSuite) TestUpdate_Success() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Equal(s.T(), "resolved", resp.Data["status"])
|
||||
assert.Equal(s.T(), "resolved", resp["status"])
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestUpdate_InvalidAccountID() {
|
||||
@@ -466,13 +456,10 @@ func (s *ConversationCrudTestSuite) TestAssignAgent_Success() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Equal(s.T(), float64(user.ID), resp["id"])
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestAssignAgent_InvalidAccountID() {
|
||||
@@ -522,13 +509,13 @@ func (s *ConversationCrudTestSuite) TestToggleStatus_Success() {
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
Payload struct {
|
||||
CurrentStatus string `json:"current_status"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Equal(s.T(), "resolved", resp.Data["status"])
|
||||
assert.Equal(s.T(), "resolved", resp.Payload.CurrentStatus)
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestToggleStatus_InvalidAccountID() {
|
||||
@@ -598,13 +585,7 @@ func (s *ConversationCrudTestSuite) TestMute_Success() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Empty(s.T(), w.Body.String())
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestMute_InvalidAccountID() {
|
||||
@@ -643,13 +624,7 @@ func (s *ConversationCrudTestSuite) TestUnmute_Success() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Empty(s.T(), w.Body.String())
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestUnmute_InvalidAccountID() {
|
||||
@@ -689,13 +664,10 @@ func (s *ConversationCrudTestSuite) TestUpdateLabels_Success() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.ElementsMatch(s.T(), []interface{}{"support", "bug"}, resp["labels"])
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestUpdateLabels_InvalidAccountID() {
|
||||
@@ -754,15 +726,15 @@ func (s *ConversationCrudTestSuite) TestSearch_Success() {
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data"`
|
||||
Meta struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
Meta struct {
|
||||
AllCount int64 `json:"all_count"`
|
||||
} `json:"meta"`
|
||||
Payload []map[string]interface{} `json:"payload"`
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
|
||||
assert.Len(s.T(), resp.Payload, 1)
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestSearch_InvalidAccountID() {
|
||||
@@ -785,7 +757,7 @@ func (s *ConversationCrudTestSuite) TestSearch_MissingQuery() {
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestFilter_Success() {
|
||||
body, _ := json.Marshal(map[string]interface{}{
|
||||
"status": "open",
|
||||
"status": "open",
|
||||
"assignee_type": "all",
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
@@ -796,15 +768,15 @@ func (s *ConversationCrudTestSuite) TestFilter_Success() {
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data"`
|
||||
Meta struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
Meta struct {
|
||||
AllCount int64 `json:"all_count"`
|
||||
} `json:"meta"`
|
||||
Payload []map[string]interface{} `json:"payload"`
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
|
||||
assert.Len(s.T(), resp.Payload, 1)
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestFilter_InvalidAccountID() {
|
||||
@@ -841,14 +813,10 @@ func (s *ConversationCrudTestSuite) TestUpdatePriority_Success() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Equal(s.T(), "urgent", resp.Data["priority"])
|
||||
assert.Equal(s.T(), "urgent", resp["priority"])
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestUpdatePriority_InvalidAccountID() {
|
||||
@@ -925,13 +893,10 @@ func (s *ConversationCrudTestSuite) TestAssignTeam_Success() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Equal(s.T(), float64(s.testConv.ID), resp["id"])
|
||||
}
|
||||
|
||||
func (s *ConversationCrudTestSuite) TestAssignTeam_WithAgentID() {
|
||||
@@ -1009,4 +974,4 @@ func (s *ConversationCrudTestSuite) TestAssignTeam_NotFound() {
|
||||
|
||||
func TestConversationCrudTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ConversationCrudTestSuite))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
@@ -209,12 +208,12 @@ func (s *ConversationHandlerTestSuite) TestUnread_Success() {
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data"`
|
||||
ID uint `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Equal(s.T(), s.testConv.ID, resp.ID)
|
||||
}
|
||||
|
||||
func (s *ConversationHandlerTestSuite) TestUnread_InvalidAccountID() {
|
||||
@@ -330,16 +329,13 @@ func (s *ConversationHandlerTestSuite) TestUpdateCustomAttributes_Success() {
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data struct {
|
||||
ID uint `json:"id"`
|
||||
CustomAttributes datatypes.JSON `json:"custom_attributes"`
|
||||
} `json:"data"`
|
||||
ID uint `json:"id"`
|
||||
CustomAttributes map[string]interface{} `json:"custom_attributes"`
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.NotNil(s.T(), resp.Data.CustomAttributes)
|
||||
assert.Equal(s.T(), s.testConv.ID, resp.ID)
|
||||
assert.Equal(s.T(), "vip_customer", resp.CustomAttributes["priority_reason"])
|
||||
}
|
||||
|
||||
func (s *ConversationHandlerTestSuite) TestUpdateCustomAttributes_InvalidAccountID() {
|
||||
@@ -465,4 +461,4 @@ func (s *ConversationHandlerTestSuite) TestUpdateLastSeen_InvalidConversationID(
|
||||
// Run the test suite
|
||||
func TestConversationHandlerTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ConversationHandlerTestSuite))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type chatwootConversationListResponse struct {
|
||||
Data chatwootConversationListData `json:"data"`
|
||||
}
|
||||
|
||||
type chatwootConversationListData struct {
|
||||
Meta chatwootConversationCounts `json:"meta"`
|
||||
Payload []chatwootConversationPayload `json:"payload"`
|
||||
}
|
||||
|
||||
type chatwootConversationCounts struct {
|
||||
MineCount int64 `json:"mine_count"`
|
||||
AssignedCount int64 `json:"assigned_count"`
|
||||
UnassignedCount int64 `json:"unassigned_count"`
|
||||
AllCount int64 `json:"all_count"`
|
||||
}
|
||||
|
||||
type chatwootConversationPayload struct {
|
||||
Meta chatwootConversationMeta `json:"meta"`
|
||||
ID uint `json:"id"`
|
||||
Messages []chatwootMessagePayload `json:"messages"`
|
||||
AccountID uint `json:"account_id"`
|
||||
UUID string `json:"uuid"`
|
||||
AdditionalAttributes map[string]any `json:"additional_attributes"`
|
||||
AgentLastSeenAt int64 `json:"agent_last_seen_at"`
|
||||
AssigneeLastSeenAt int64 `json:"assignee_last_seen_at"`
|
||||
CanReply bool `json:"can_reply"`
|
||||
ContactLastSeenAt int64 `json:"contact_last_seen_at"`
|
||||
CustomAttributes map[string]any `json:"custom_attributes"`
|
||||
InboxID uint `json:"inbox_id"`
|
||||
Labels []string `json:"labels"`
|
||||
Muted bool `json:"muted"`
|
||||
SnoozedUntil *int64 `json:"snoozed_until"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt float64 `json:"updated_at"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
FirstReplyCreatedAt int64 `json:"first_reply_created_at"`
|
||||
UnreadCount int64 `json:"unread_count"`
|
||||
LastNonActivityMessage *chatwootMessagePayload `json:"last_non_activity_message"`
|
||||
LastActivityAt int64 `json:"last_activity_at"`
|
||||
Priority string `json:"priority"`
|
||||
WaitingSince int64 `json:"waiting_since"`
|
||||
SlaPolicyID *uint `json:"sla_policy_id"`
|
||||
}
|
||||
|
||||
type chatwootConversationMeta struct {
|
||||
Sender map[string]any `json:"sender"`
|
||||
Channel string `json:"channel"`
|
||||
Assignee map[string]any `json:"assignee,omitempty"`
|
||||
AssigneeType string `json:"assignee_type,omitempty"`
|
||||
Team map[string]any `json:"team,omitempty"`
|
||||
HMACVerified *bool `json:"hmac_verified,omitempty"`
|
||||
}
|
||||
|
||||
type chatwootMessageIndexResponse struct {
|
||||
Meta chatwootMessageIndexMeta `json:"meta"`
|
||||
Payload []chatwootMessagePayload `json:"payload"`
|
||||
}
|
||||
|
||||
type chatwootMessageIndexMeta struct {
|
||||
Labels []string `json:"labels"`
|
||||
AdditionalAttrs map[string]any `json:"additional_attributes"`
|
||||
Contact map[string]any `json:"contact"`
|
||||
Assignee map[string]any `json:"assignee,omitempty"`
|
||||
AgentLastSeenAt int64 `json:"agent_last_seen_at"`
|
||||
AssigneeLastSeenAt int64 `json:"assignee_last_seen_at"`
|
||||
}
|
||||
|
||||
type chatwootMessagePayload struct {
|
||||
ID uint `json:"id"`
|
||||
Content string `json:"content"`
|
||||
InboxID uint `json:"inbox_id"`
|
||||
EchoID string `json:"echo_id,omitempty"`
|
||||
ConversationID uint `json:"conversation_id"`
|
||||
MessageType int `json:"message_type"`
|
||||
ContentType string `json:"content_type"`
|
||||
Status string `json:"status"`
|
||||
ContentAttributes map[string]any `json:"content_attributes"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Private bool `json:"private"`
|
||||
SourceID string `json:"source_id"`
|
||||
Sender map[string]any `json:"sender,omitempty"`
|
||||
Attachments []any `json:"attachments,omitempty"`
|
||||
}
|
||||
|
||||
func serializeConversationList(ctx context.Context, db *gorm.DB, conversations []model.Conversation, total int64) chatwootConversationListResponse {
|
||||
payload := make([]chatwootConversationPayload, 0, len(conversations))
|
||||
for i := range conversations {
|
||||
payload = append(payload, serializeConversation(ctx, db, &conversations[i]))
|
||||
}
|
||||
return chatwootConversationListResponse{Data: chatwootConversationListData{
|
||||
Meta: chatwootConversationCounts{AllCount: total},
|
||||
Payload: payload,
|
||||
}}
|
||||
}
|
||||
|
||||
func serializeConversation(ctx context.Context, db *gorm.DB, conversation *model.Conversation) chatwootConversationPayload {
|
||||
var lastMessage *model.Message
|
||||
if db != nil {
|
||||
var msg model.Message
|
||||
if err := db.WithContext(ctx).Where("account_id = ? AND conversation_id = ?", conversation.AccountID, conversation.ID).Order("id DESC").First(&msg).Error; err == nil {
|
||||
lastMessage = &msg
|
||||
}
|
||||
}
|
||||
|
||||
messages := []chatwootMessagePayload{}
|
||||
var lastNonActivity *chatwootMessagePayload
|
||||
if lastMessage != nil {
|
||||
serialized := serializeMessage(ctx, db, lastMessage, conversation)
|
||||
messages = append(messages, serialized)
|
||||
if lastMessage.MessageType != "activity" {
|
||||
lastNonActivity = &serialized
|
||||
}
|
||||
}
|
||||
|
||||
return chatwootConversationPayload{
|
||||
Meta: serializeConversationMeta(ctx, db, conversation),
|
||||
ID: conversationDisplayID(conversation),
|
||||
Messages: messages,
|
||||
AccountID: conversation.AccountID,
|
||||
UUID: conversation.UUID,
|
||||
AdditionalAttributes: jsonObject(conversation.AdditionalAttributes),
|
||||
AgentLastSeenAt: int64Value(conversation.AgentLastSeenAt),
|
||||
AssigneeLastSeenAt: int64Value(conversation.AssigneeLastSeenAt),
|
||||
CanReply: true,
|
||||
ContactLastSeenAt: int64Value(conversation.ContactLastSeenAt),
|
||||
CustomAttributes: jsonObject(conversation.CustomAttributes),
|
||||
InboxID: conversation.InboxID,
|
||||
Labels: labelList(conversation.Labels),
|
||||
Muted: conversation.Muted,
|
||||
SnoozedUntil: conversation.SnoozedUntil,
|
||||
Status: conversation.Status,
|
||||
CreatedAt: conversation.CreatedAt.Unix(),
|
||||
UpdatedAt: float64(conversation.UpdatedAt.UnixNano()) / float64(time.Second),
|
||||
Timestamp: int64Value(conversation.LastActivityAt),
|
||||
FirstReplyCreatedAt: int64Value(conversation.FirstReplyCreatedAt),
|
||||
UnreadCount: unreadCount(ctx, db, conversation),
|
||||
LastNonActivityMessage: lastNonActivity,
|
||||
LastActivityAt: int64Value(conversation.LastActivityAt),
|
||||
Priority: conversation.Priority,
|
||||
WaitingSince: int64Value(conversation.WaitingSince),
|
||||
SlaPolicyID: conversation.SlaPolicyID,
|
||||
}
|
||||
}
|
||||
|
||||
func serializeConversationMeta(ctx context.Context, db *gorm.DB, conversation *model.Conversation) chatwootConversationMeta {
|
||||
meta := chatwootConversationMeta{Channel: conversation.ChannelType}
|
||||
if db == nil {
|
||||
meta.Sender = map[string]any{"id": conversation.ContactID}
|
||||
return meta
|
||||
}
|
||||
|
||||
var contact model.Contact
|
||||
if err := db.WithContext(ctx).First(&contact, conversation.ContactID).Error; err == nil {
|
||||
meta.Sender = serializeContact(&contact)
|
||||
} else {
|
||||
meta.Sender = map[string]any{"id": conversation.ContactID}
|
||||
}
|
||||
|
||||
if conversation.AssigneeID != nil && *conversation.AssigneeID != 0 {
|
||||
var user model.User
|
||||
if err := db.WithContext(ctx).First(&user, *conversation.AssigneeID).Error; err == nil {
|
||||
meta.Assignee = serializeUser(&user, conversation.AccountID)
|
||||
meta.AssigneeType = "User"
|
||||
}
|
||||
}
|
||||
|
||||
if conversation.ContactInboxID != nil && *conversation.ContactInboxID != 0 {
|
||||
var contactInbox model.ContactInbox
|
||||
if err := db.WithContext(ctx).First(&contactInbox, *conversation.ContactInboxID).Error; err == nil {
|
||||
meta.HMACVerified = &contactInbox.HMACVerified
|
||||
}
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
func serializeMessageIndex(ctx context.Context, db *gorm.DB, conversation *model.Conversation, messages []model.Message) chatwootMessageIndexResponse {
|
||||
payload := make([]chatwootMessagePayload, 0, len(messages))
|
||||
for i := range messages {
|
||||
payload = append(payload, serializeMessage(ctx, db, &messages[i], conversation))
|
||||
}
|
||||
|
||||
meta := chatwootMessageIndexMeta{
|
||||
Labels: labelList(conversation.Labels),
|
||||
AdditionalAttrs: jsonObject(conversation.AdditionalAttributes),
|
||||
Contact: map[string]any{"id": conversation.ContactID},
|
||||
AgentLastSeenAt: int64Value(conversation.AgentLastSeenAt),
|
||||
AssigneeLastSeenAt: int64Value(conversation.AssigneeLastSeenAt),
|
||||
}
|
||||
if db != nil {
|
||||
var contact model.Contact
|
||||
if err := db.WithContext(ctx).First(&contact, conversation.ContactID).Error; err == nil {
|
||||
meta.Contact = serializeContact(&contact)
|
||||
}
|
||||
if conversation.AssigneeID != nil && *conversation.AssigneeID != 0 {
|
||||
var user model.User
|
||||
if err := db.WithContext(ctx).First(&user, *conversation.AssigneeID).Error; err == nil {
|
||||
meta.Assignee = serializeUser(&user, conversation.AccountID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chatwootMessageIndexResponse{Meta: meta, Payload: payload}
|
||||
}
|
||||
|
||||
func serializeMessage(ctx context.Context, db *gorm.DB, message *model.Message, conversation *model.Conversation) chatwootMessagePayload {
|
||||
conversationID := message.ConversationID
|
||||
if conversation != nil {
|
||||
conversationID = conversationDisplayID(conversation)
|
||||
}
|
||||
payload := chatwootMessagePayload{
|
||||
ID: message.ID,
|
||||
Content: message.Content,
|
||||
InboxID: message.InboxID,
|
||||
EchoID: message.EchoID,
|
||||
ConversationID: conversationID,
|
||||
MessageType: messageTypeValue(message.MessageType),
|
||||
ContentType: nonEmpty(message.ContentType, "text"),
|
||||
Status: nonEmpty(message.Status, "sent"),
|
||||
ContentAttributes: jsonObject(message.ContentAttributes),
|
||||
CreatedAt: message.CreatedAt.Unix(),
|
||||
Private: message.Private,
|
||||
SourceID: message.SourceID,
|
||||
}
|
||||
if db != nil && message.SenderID != nil && *message.SenderID != 0 {
|
||||
senderType := strings.ToLower(message.SenderType)
|
||||
if senderType == "contact" {
|
||||
var contact model.Contact
|
||||
if err := db.WithContext(ctx).First(&contact, *message.SenderID).Error; err == nil {
|
||||
payload.Sender = serializeContact(&contact)
|
||||
}
|
||||
} else {
|
||||
var user model.User
|
||||
if err := db.WithContext(ctx).First(&user, *message.SenderID).Error; err == nil {
|
||||
payload.Sender = serializeUser(&user, message.AccountID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func serializeContact(contact *model.Contact) map[string]any {
|
||||
return map[string]any{
|
||||
"additional_attributes": jsonObject(contact.AdditionalAttributes),
|
||||
"availability_status": "offline",
|
||||
"email": contact.Email,
|
||||
"id": contact.ID,
|
||||
"name": contact.Name,
|
||||
"phone_number": contact.PhoneNumber,
|
||||
"blocked": contact.Blocked,
|
||||
"identifier": contact.Identifier,
|
||||
"thumbnail": contact.AvatarURL,
|
||||
"custom_attributes": jsonObject(contact.CustomAttributes),
|
||||
"last_activity_at": int64Value(contact.LastActivityAt),
|
||||
"created_at": contact.CreatedAt.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func serializeUser(user *model.User, accountID uint) map[string]any {
|
||||
return map[string]any{
|
||||
"id": user.ID,
|
||||
"account_id": accountID,
|
||||
"availability_status": availabilityStatus(user.Available),
|
||||
"auto_offline": false,
|
||||
"confirmed": user.ConfirmedAt != nil,
|
||||
"email": user.Email,
|
||||
"provider": nonEmpty(user.Provider, "email"),
|
||||
"available_name": nonEmpty(user.DisplayName, user.Name),
|
||||
"name": user.Name,
|
||||
"role": nonEmpty(user.Role, "agent"),
|
||||
"thumbnail": user.AvatarURL,
|
||||
}
|
||||
}
|
||||
|
||||
func serializeUserFromDB(ctx context.Context, db *gorm.DB, userID uint, accountID uint) any {
|
||||
if userID == 0 || db == nil {
|
||||
return nil
|
||||
}
|
||||
var user model.User
|
||||
if err := db.WithContext(ctx).First(&user, userID).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
return serializeUser(&user, accountID)
|
||||
}
|
||||
|
||||
func conversationDisplayID(conversation *model.Conversation) uint {
|
||||
if conversation.DisplayID != nil && *conversation.DisplayID != 0 {
|
||||
return *conversation.DisplayID
|
||||
}
|
||||
return conversation.ID
|
||||
}
|
||||
|
||||
func messageTypeValue(value string) int {
|
||||
switch strings.ToLower(value) {
|
||||
case "incoming":
|
||||
return 0
|
||||
case "outgoing", "private_note":
|
||||
return 1
|
||||
case "activity":
|
||||
return 2
|
||||
case "template":
|
||||
return 3
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func labelList(labels string) []string {
|
||||
labels = strings.TrimSpace(labels)
|
||||
if labels == "" {
|
||||
return []string{}
|
||||
}
|
||||
if strings.HasPrefix(labels, "[") {
|
||||
var list []string
|
||||
if err := json.Unmarshal([]byte(labels), &list); err == nil {
|
||||
return list
|
||||
}
|
||||
}
|
||||
parts := strings.Split(labels, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
label := strings.TrimSpace(part)
|
||||
if label != "" {
|
||||
result = append(result, label)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func jsonObject(raw datatypes.JSON) map[string]any {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return map[string]any{}
|
||||
}
|
||||
var value map[string]any
|
||||
if err := json.Unmarshal(raw, &value); err != nil || value == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func int64Value(value *int64) int64 {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func nonEmpty(value, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func availabilityStatus(available bool) string {
|
||||
if available {
|
||||
return "online"
|
||||
}
|
||||
return "offline"
|
||||
}
|
||||
|
||||
func unreadCount(ctx context.Context, db *gorm.DB, conversation *model.Conversation) int64 {
|
||||
if db == nil {
|
||||
return 0
|
||||
}
|
||||
query := db.WithContext(ctx).Model(&model.Message{}).
|
||||
Where("account_id = ? AND conversation_id = ? AND message_type = ?", conversation.AccountID, conversation.ID, "incoming")
|
||||
if conversation.AgentLastSeenAt != nil {
|
||||
query = query.Where("created_at > ?", time.Unix(*conversation.AgentLastSeenAt, 0))
|
||||
}
|
||||
var count int64
|
||||
_ = query.Count(&count).Error
|
||||
return count
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
"github.com/gochat/gochat/pkg/pagination"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// MessageHandler handles message-related API endpoints.
|
||||
@@ -43,21 +45,31 @@ func NewMessageHandler(svc *service.MessageService) *MessageHandler {
|
||||
// GET /api/v1/accounts/:account_id/conversations/:conversation_id/messages
|
||||
// Reference: Chatwoot conversations#messages (index)
|
||||
func (h *MessageHandler) List(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
|
||||
}
|
||||
|
||||
p := pagination.Parse(c)
|
||||
|
||||
messages, total, svcErr := h.svc.ListByConversation(c.Request.Context(), conversationID, p.Offset, p.PerPage)
|
||||
conversation, svcErr := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, conversationID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OKWithMeta(c, toInterfaceSlice(messages), p.Page, p.PerPage, total)
|
||||
p := pagination.Parse(c)
|
||||
|
||||
messages, _, svcErr := h.svc.ListByConversation(c.Request.Context(), conversation.ID, p.Offset, p.PerPage)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, serializeMessageIndex(c.Request.Context(), h.svc.DB(), conversation, messages))
|
||||
}
|
||||
|
||||
// @Summary Create a message in a conversation
|
||||
@@ -92,7 +104,7 @@ func (h *MessageHandler) Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req service.CreateMessageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if err := bindCreateMessageRequest(c, &req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -104,7 +116,12 @@ func (h *MessageHandler) Create(c *gin.Context) {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "conversation_id is required")
|
||||
return
|
||||
}
|
||||
req.ConversationID = conversationID
|
||||
conversation, resolveErr := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, conversationID)
|
||||
if resolveErr != nil {
|
||||
handleServiceError(c, resolveErr)
|
||||
return
|
||||
}
|
||||
req.ConversationID = conversation.ID
|
||||
}
|
||||
|
||||
message, svcErr := h.svc.Create(c.Request.Context(), accountID, userID, req)
|
||||
@@ -112,7 +129,8 @@ func (h *MessageHandler) Create(c *gin.Context) {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
response.Created(c, message)
|
||||
conversation, _ := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, req.ConversationID)
|
||||
c.JSON(http.StatusOK, serializeMessage(c.Request.Context(), h.svc.DB(), message, conversation))
|
||||
}
|
||||
|
||||
// @Summary Get a single message
|
||||
@@ -178,7 +196,8 @@ func (h *MessageHandler) Update(c *gin.Context) {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
response.OK(c, message)
|
||||
conversation, _ := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, message.ConversationID)
|
||||
c.JSON(http.StatusOK, serializeMessage(c.Request.Context(), h.svc.DB(), message, conversation))
|
||||
}
|
||||
|
||||
// @Summary Delete a message
|
||||
@@ -264,7 +283,8 @@ func (h *MessageHandler) Retry(c *gin.Context) {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
response.OK(c, message)
|
||||
conversation, _ := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, message.ConversationID)
|
||||
c.JSON(http.StatusOK, serializeMessage(c.Request.Context(), h.svc.DB(), message, conversation))
|
||||
}
|
||||
|
||||
// Translate translates a message's content to a target language using LLM.
|
||||
@@ -304,3 +324,34 @@ func (h *MessageHandler) Translate(c *gin.Context) {
|
||||
}
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func bindCreateMessageRequest(c *gin.Context, req *service.CreateMessageRequest) error {
|
||||
if strings.HasPrefix(c.ContentType(), "multipart/form-data") {
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
|
||||
return err
|
||||
}
|
||||
req.Content = c.PostForm("content")
|
||||
req.MessageType = c.PostForm("message_type")
|
||||
req.ContentType = c.PostForm("content_type")
|
||||
req.SourceID = c.PostForm("source_id")
|
||||
req.EchoID = c.PostForm("echo_id")
|
||||
req.Private = strings.EqualFold(c.PostForm("private"), "true") || c.PostForm("private") == "1"
|
||||
if raw := c.PostForm("content_attributes"); raw != "" {
|
||||
req.ContentAttributes = []byte(raw)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var raw map[string]json.RawMessage
|
||||
if err := c.ShouldBindJSON(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
bytes, _ := json.Marshal(raw)
|
||||
if err := json.Unmarshal(bytes, req); err != nil {
|
||||
return err
|
||||
}
|
||||
if value, ok := raw["content_attributes"]; ok && string(value) != "null" {
|
||||
req.ContentAttributes = datatypes.JSON(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ func (s *MessageHandlerTestSuite) SetupSuite() {
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Wire up repos, services, handlers
|
||||
// Wire up repos, services, handlers
|
||||
msgRepo := repository.NewMessageRepo(db)
|
||||
s.dispatcher = channel.NewDispatcher()
|
||||
s.mockLLM = &mockMsgHandlerLLMProvider{
|
||||
@@ -173,7 +173,7 @@ func (s *MessageHandlerTestSuite) SetupTest() {
|
||||
s.Require().NoError(s.db.Create(msg).Error)
|
||||
s.testMessage = msg
|
||||
|
||||
// Reset mock LLM to default success response
|
||||
// Reset mock LLM to default success response
|
||||
s.mockLLM.chatResponse = &llm.ChatResponse{
|
||||
Choices: []llm.ChatChoice{
|
||||
{
|
||||
@@ -199,7 +199,7 @@ func TestMessageHandlerTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MessageHandlerTestSuite))
|
||||
}
|
||||
|
||||
// --- Helper for building message list URL ---
|
||||
// --- Helper for building message list URL ---
|
||||
|
||||
func msgListURL(accountID, convID uint) string {
|
||||
return fmt.Sprintf("/api/v1/accounts/%d/conversations/%d/messages/", accountID, convID)
|
||||
@@ -217,7 +217,7 @@ func msgTranslateURL(accountID, convID, msgID uint) string {
|
||||
return fmt.Sprintf("/api/v1/accounts/%d/conversations/%d/messages/%d/translate", accountID, convID, msgID)
|
||||
}
|
||||
|
||||
// --- List Tests ---
|
||||
// --- List Tests ---
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestList_Success() {
|
||||
w := httptest.NewRecorder()
|
||||
@@ -228,10 +228,12 @@ func (s *MessageHandlerTestSuite) TestList_Success() {
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.True(s.T(), resp["success"].(bool))
|
||||
data, ok := resp["data"].([]interface{})
|
||||
data, ok := resp["payload"].([]interface{})
|
||||
assert.True(s.T(), ok)
|
||||
assert.GreaterOrEqual(s.T(), len(data), 1)
|
||||
meta, ok := resp["meta"].(map[string]interface{})
|
||||
assert.True(s.T(), ok)
|
||||
assert.NotNil(s.T(), meta["contact"])
|
||||
}
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestList_Empty() {
|
||||
@@ -246,7 +248,7 @@ func (s *MessageHandlerTestSuite) TestList_Empty() {
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
data, ok := resp["data"].([]interface{})
|
||||
data, ok := resp["payload"].([]interface{})
|
||||
assert.True(s.T(), ok)
|
||||
assert.Equal(s.T(), 0, len(data))
|
||||
}
|
||||
@@ -259,7 +261,7 @@ func (s *MessageHandlerTestSuite) TestList_InvalidConversationID() {
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
// --- Create Tests ---
|
||||
// --- Create Tests ---
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestCreate_Success() {
|
||||
payload := map[string]interface{}{
|
||||
@@ -276,13 +278,39 @@ func (s *MessageHandlerTestSuite) TestCreate_Success() {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusCreated, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.True(s.T(), resp["success"].(bool))
|
||||
data := resp["data"].(map[string]interface{})
|
||||
assert.NotNil(s.T(), data["id"])
|
||||
assert.Equal(s.T(), "New message", data["content"])
|
||||
assert.NotNil(s.T(), resp["id"])
|
||||
assert.Equal(s.T(), "New message", resp["content"])
|
||||
assert.Equal(s.T(), float64(1), resp["message_type"])
|
||||
assert.Equal(s.T(), float64(s.testConv.ID), resp["conversation_id"])
|
||||
}
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestCreate_ChatwootFrontendPayloadDefaultsOutgoing() {
|
||||
payload := map[string]interface{}{
|
||||
"content": "Frontend payload",
|
||||
"private": true,
|
||||
"echo_id": "tmp-123",
|
||||
"content_attributes": map[string]interface{}{"submitted_values": []interface{}{}},
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
url := msgListURL(s.testAccount.ID, s.testConv.ID)
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Equal(s.T(), "Frontend payload", resp["content"])
|
||||
assert.Equal(s.T(), true, resp["private"])
|
||||
assert.Equal(s.T(), "tmp-123", resp["echo_id"])
|
||||
assert.Equal(s.T(), float64(1), resp["message_type"])
|
||||
assert.Equal(s.T(), "text", resp["content_type"])
|
||||
assert.NotNil(s.T(), resp["content_attributes"])
|
||||
}
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestCreate_MissingContent() {
|
||||
@@ -316,7 +344,7 @@ func (s *MessageHandlerTestSuite) TestCreate_InvalidConversationID() {
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
// --- Get Tests ---
|
||||
// --- Get Tests ---
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestGet_Success() {
|
||||
w := httptest.NewRecorder()
|
||||
@@ -350,7 +378,7 @@ func (s *MessageHandlerTestSuite) TestGet_InvalidID() {
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
// --- Update Tests ---
|
||||
// --- Update Tests ---
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestUpdate_Success() {
|
||||
payload := map[string]interface{}{
|
||||
@@ -367,9 +395,7 @@ func (s *MessageHandlerTestSuite) TestUpdate_Success() {
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.True(s.T(), resp["success"].(bool))
|
||||
data := resp["data"].(map[string]interface{})
|
||||
assert.Equal(s.T(), "Updated content", data["content"])
|
||||
assert.Equal(s.T(), "Updated content", resp["content"])
|
||||
}
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestUpdate_NotFound() {
|
||||
@@ -387,7 +413,7 @@ func (s *MessageHandlerTestSuite) TestUpdate_NotFound() {
|
||||
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
// --- Delete Tests ---
|
||||
// --- Delete Tests ---
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestDelete_Success() {
|
||||
w := httptest.NewRecorder()
|
||||
@@ -407,7 +433,7 @@ func (s *MessageHandlerTestSuite) TestDelete_NotFound() {
|
||||
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
// --- Retry Tests ---
|
||||
// --- Retry Tests ---
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestRetry_Success() {
|
||||
w := httptest.NewRecorder()
|
||||
@@ -418,9 +444,8 @@ func (s *MessageHandlerTestSuite) TestRetry_Success() {
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.True(s.T(), resp["success"].(bool))
|
||||
data := resp["data"].(map[string]interface{})
|
||||
assert.Equal(s.T(), float64(s.testMessage.ID), data["id"])
|
||||
assert.Equal(s.T(), float64(s.testMessage.ID), resp["id"])
|
||||
assert.Equal(s.T(), float64(1), resp["message_type"])
|
||||
}
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestRetry_InvalidAccountID() {
|
||||
@@ -464,7 +489,7 @@ func (s *MessageHandlerTestSuite) TestRetry_AccountMismatch() {
|
||||
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
// --- Translate Tests ---
|
||||
// --- Translate Tests ---
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestTranslate_Success() {
|
||||
payload := map[string]interface{}{
|
||||
@@ -579,7 +604,7 @@ func (s *MessageHandlerTestSuite) TestTranslate_LLMError() {
|
||||
}
|
||||
|
||||
func (s *MessageHandlerTestSuite) TestTranslate_EmptyChoices() {
|
||||
// Set LLM to return response with no choices
|
||||
// Set LLM to return response with no choices
|
||||
s.mockLLM.chatResponse = &llm.ChatResponse{
|
||||
Choices: []llm.ChatChoice{},
|
||||
}
|
||||
@@ -637,4 +662,4 @@ func TestDirectUploadHandler_InvalidContentType(t *testing.T) {
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user