feat(conversations): align participant payloads
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
)
|
||||
@@ -40,7 +41,7 @@ func (h *ConversationParticipantHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, toInterfaceSlice(participants))
|
||||
c.JSON(http.StatusOK, serializeConversationParticipantAgents(accountID, participants))
|
||||
}
|
||||
|
||||
// Add adds a participant to a conversation.
|
||||
@@ -59,21 +60,27 @@ func (h *ConversationParticipantHandler) Add(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id" binding:"required"`
|
||||
Role string `json:"role"`
|
||||
UserID uint `json:"user_id"`
|
||||
UserIDs []uint `json:"user_ids"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
userIDs := participantRequestUserIDs(req.UserID, req.UserIDs)
|
||||
if len(userIDs) == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "user_ids is required")
|
||||
return
|
||||
}
|
||||
|
||||
participant, svcErr := h.participantSvc.Add(c.Request.Context(), accountID, conversationID, req.UserID, req.Role)
|
||||
participants, svcErr := h.participantSvc.AddMany(c.Request.Context(), accountID, conversationID, userIDs, req.Role)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, participant)
|
||||
c.JSON(http.StatusOK, serializeConversationParticipantAgents(accountID, participants))
|
||||
}
|
||||
|
||||
// Update updates a participant's role in a conversation.
|
||||
@@ -111,7 +118,7 @@ func (h *ConversationParticipantHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, participant)
|
||||
c.JSON(http.StatusOK, serializeConversationParticipantAgents(accountID, []model.ConversationParticipant{*participant}))
|
||||
}
|
||||
|
||||
// Remove removes a participant from a conversation.
|
||||
@@ -140,12 +147,45 @@ func (h *ConversationParticipantHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, nil)
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// Destroy removes multiple participants from a conversation.
|
||||
// DELETE /api/v1/accounts/:account_id/conversations/:conversation_id/participants
|
||||
func (h *ConversationParticipantHandler) Destroy(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
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserIDs []uint `json:"user_ids"`
|
||||
}
|
||||
if c.Request.ContentLength != 0 {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if svcErr := h.participantSvc.RemoveMany(c.Request.Context(), accountID, conversationID, req.UserIDs); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// BatchUpdate adds and/or removes multiple participants in a single call.
|
||||
// PATCH /api/v1/accounts/:account_id/conversations/:conversation_id/participants
|
||||
// Accepts: { "user_ids": [1,2,3], "remove_user_ids": [4,5], "role": "participant" }
|
||||
// Accepts: { "user_ids": [1,2,3] } as the final participant set.
|
||||
// Reference: Chatwoot app/controllers/api/v1/conversations/participants_controller.rb#update
|
||||
func (h *ConversationParticipantHandler) BatchUpdate(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "account_id")
|
||||
@@ -161,20 +201,37 @@ func (h *ConversationParticipantHandler) BatchUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserIDs []uint `json:"user_ids"`
|
||||
RemoveUserIDs []uint `json:"remove_user_ids"`
|
||||
Role string `json:"role"`
|
||||
UserIDs []uint `json:"user_ids"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
participants, svcErr := h.participantSvc.BatchUpdate(c.Request.Context(), accountID, conversationID, req.UserIDs, req.RemoveUserIDs, req.Role)
|
||||
participants, svcErr := h.participantSvc.Replace(c.Request.Context(), accountID, conversationID, req.UserIDs, req.Role)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, toInterfaceSlice(participants))
|
||||
}
|
||||
c.JSON(http.StatusOK, serializeConversationParticipantAgents(accountID, participants))
|
||||
}
|
||||
|
||||
func participantRequestUserIDs(userID uint, userIDs []uint) []uint {
|
||||
if len(userIDs) > 0 {
|
||||
return userIDs
|
||||
}
|
||||
if userID != 0 {
|
||||
return []uint{userID}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func serializeConversationParticipantAgents(accountID uint, participants []model.ConversationParticipant) []map[string]any {
|
||||
payload := make([]map[string]any, 0, len(participants))
|
||||
for i := range participants {
|
||||
payload = append(payload, serializeAgentUser(participants[i].User, accountID, "", "", false, 0))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
@@ -91,8 +91,13 @@ func (s *ConversationParticipantHandlerTestSuite) SetupSuite() {
|
||||
participants := convGroup.Group("/participants")
|
||||
{
|
||||
participants.GET("", handler.List)
|
||||
participants.GET("/", handler.List)
|
||||
participants.POST("", handler.Add)
|
||||
participants.POST("/", handler.Add)
|
||||
participants.PATCH("", handler.BatchUpdate)
|
||||
participants.PATCH("/", handler.BatchUpdate)
|
||||
participants.PUT("", handler.BatchUpdate)
|
||||
participants.DELETE("", handler.Destroy)
|
||||
participants.PATCH("/:user_id", handler.Update)
|
||||
participants.DELETE("/:user_id", handler.Remove)
|
||||
}
|
||||
@@ -102,8 +107,7 @@ func (s *ConversationParticipantHandlerTestSuite) SetupSuite() {
|
||||
|
||||
func (s *ConversationParticipantHandlerTestSuite) Test_AddParticipant() {
|
||||
body := map[string]interface{}{
|
||||
"user_id": s.testUser.ID,
|
||||
"role": "assignee",
|
||||
"user_ids": []uint{s.testUser.ID},
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
@@ -114,13 +118,13 @@ func (s *ConversationParticipantHandlerTestSuite) Test_AddParticipant() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
data, ok := resp["data"].([]interface{})
|
||||
if ok && len(data) > 0 {
|
||||
item := data[0].(map[string]interface{})
|
||||
assert.Equal(s.T(), "assignee", item["role"])
|
||||
}
|
||||
var resp []map[string]interface{}
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Len(s.T(), resp, 1)
|
||||
assert.Equal(s.T(), float64(s.testUser.ID), resp[0]["id"])
|
||||
assert.Equal(s.T(), s.testUser.Email, resp[0]["email"])
|
||||
assert.NotContains(s.T(), resp[0], "conversation_id")
|
||||
assert.NotContains(s.T(), resp[0], "user_id")
|
||||
}
|
||||
|
||||
func (s *ConversationParticipantHandlerTestSuite) Test_ListParticipants() {
|
||||
@@ -142,6 +146,10 @@ func (s *ConversationParticipantHandlerTestSuite) Test_ListParticipants() {
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp []map[string]interface{}
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.NotEmpty(s.T(), resp)
|
||||
assert.NotContains(s.T(), resp[0], "data")
|
||||
}
|
||||
|
||||
func (s *ConversationParticipantHandlerTestSuite) Test_RemoveParticipant() {
|
||||
@@ -210,36 +218,62 @@ func (s *ConversationParticipantHandlerTestSuite) Test_AddParticipant_InvalidAcc
|
||||
// ========== BatchUpdate Handler Tests ==========
|
||||
|
||||
func (s *ConversationParticipantHandlerTestSuite) Test_BatchUpdate_AddAndRemove() {
|
||||
// Create a second user for testing batch add/remove
|
||||
user1 := &model.User{Name: "BatchUser1", Email: "batch1@test.com", Password: "hashed", Role: "agent", Active: true}
|
||||
s.Require().NoError(s.db.Create(user1).Error)
|
||||
user2 := &model.User{Name: "BatchUser2", Email: "batch2@test.com", Password: "hashed", Role: "agent", Active: true}
|
||||
s.Require().NoError(s.db.Create(user2).Error)
|
||||
conv := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testConv.InboxID, ContactID: s.testConv.ContactID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"}
|
||||
s.Require().NoError(s.db.Create(conv).Error)
|
||||
|
||||
// First add s.testUser as participant
|
||||
// First add user1 as participant.
|
||||
addBody := map[string]interface{}{
|
||||
"user_id": s.testUser.ID,
|
||||
"role": "assignee",
|
||||
"user_ids": []uint{user1.ID},
|
||||
}
|
||||
addBytes, _ := json.Marshal(addBody)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/participants", bytes.NewReader(addBytes))
|
||||
req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(conv.ID), 10)+"/participants", bytes.NewReader(addBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
// Now batch update: add user2, remove s.testUser
|
||||
// Chatwoot update treats user_ids as the final participant set.
|
||||
batchBody := map[string]interface{}{
|
||||
"user_ids": []uint{user2.ID},
|
||||
"removed_user_ids": []uint{s.testUser.ID},
|
||||
"user_ids": []uint{user2.ID},
|
||||
}
|
||||
batchBytes, _ := json.Marshal(batchBody)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("PATCH", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/participants", bytes.NewReader(batchBytes))
|
||||
req, _ = http.NewRequest("PATCH", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(conv.ID), 10)+"/participants", bytes.NewReader(batchBytes))
|
||||
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{}
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Len(s.T(), resp, 1)
|
||||
assert.Equal(s.T(), float64(user2.ID), resp[0]["id"])
|
||||
|
||||
var count int64
|
||||
s.Require().NoError(s.db.Model(&model.ConversationParticipant{}).Where("conversation_id = ? AND user_id = ?", conv.ID, user1.ID).Count(&count).Error)
|
||||
assert.Equal(s.T(), int64(0), count)
|
||||
}
|
||||
|
||||
func (s *ConversationParticipantHandlerTestSuite) Test_DestroyParticipantsRawPayload() {
|
||||
user := &model.User{Name: "DestroyUser", Email: "destroy-participant@test.com", Password: "hashed", Role: "agent", Active: true}
|
||||
s.Require().NoError(s.db.Create(user).Error)
|
||||
conv := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testConv.InboxID, ContactID: s.testConv.ContactID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"}
|
||||
s.Require().NoError(s.db.Create(conv).Error)
|
||||
s.Require().NoError(s.db.Create(&model.ConversationParticipant{AccountID: s.testAccount.ID, ConversationID: conv.ID, UserID: user.ID}).Error)
|
||||
|
||||
body, _ := json.Marshal(map[string]interface{}{"user_ids": []uint{user.ID}})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(conv.ID), 10)+"/participants", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
assert.Empty(s.T(), w.Body.String())
|
||||
}
|
||||
|
||||
func (s *ConversationParticipantHandlerTestSuite) Test_BatchUpdate_InvalidAccountID() {
|
||||
|
||||
Reference in New Issue
Block a user