Files
gochat/internal/handler/api/v1/bulk_action_handler.go
T

172 lines
6.0 KiB
Go

package v1
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/internal/worker"
"github.com/gochat/gochat/pkg/response"
)
// BulkActionHandler handles generic bulk actions for Conversations and Contacts.
// Reference: Chatwoot app/controllers/api/v1/accounts/bulk_actions_controller.rb
type BulkActionHandler struct {
conversationSvc *service.ConversationService
contactSvc *service.ContactService
worker *worker.WorkerPool
}
// NewBulkActionHandler creates a new BulkActionHandler.
func NewBulkActionHandler(conversationSvc *service.ConversationService, contactSvc *service.ContactService) *BulkActionHandler {
return &BulkActionHandler{conversationSvc: conversationSvc, contactSvc: contactSvc}
}
func (h *BulkActionHandler) WithWorkerPool(wp *worker.WorkerPool) *BulkActionHandler {
h.worker = wp
return h
}
// BulkActionRequest is the DTO for generic bulk actions.
// Reference: Chatwoot bulk_actions_controller#create — params[:type], params[:action_name], params[:ids]
type BulkActionRequest struct {
Type string `json:"type" binding:"required"` // "Conversation" or "Contact"
ActionName string `json:"action_name,omitempty"` // legacy local action names
IDs []uint `json:"ids" binding:"required,min=1"` // Chatwoot sends conversation display IDs
Fields service.ConversationBulkActionFields `json:"fields,omitempty"`
Labels service.ConversationBulkActionLabels `json:"labels,omitempty"`
AssigneeID *uint `json:"assignee_id,omitempty"` // legacy local assign field
TeamID *uint `json:"team_id,omitempty"` // legacy local team field
SnoozedUntil string `json:"snoozed_until,omitempty"` // for conversation snooze
}
// Create processes a bulk action request.
// POST /api/v1/accounts/:account_id/bulk_actions
// Reference: Chatwoot bulk_actions_controller#create — returns 200 OK on success
func (h *BulkActionHandler) Create(c *gin.Context) {
accountID, err := parseUintParam(c, "account_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
var req BulkActionRequest
if bindErr := c.ShouldBindJSON(&req); bindErr != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, bindErr.Error())
return
}
switch req.Type {
case "Conversation":
h.handleConversationBulk(c, accountID, req)
case "Contact":
h.handleContactBulk(c, accountID, req)
default:
c.JSON(http.StatusUnprocessableEntity, gin.H{"success": false})
}
}
// handleConversationBulk processes bulk actions on conversations.
func (h *BulkActionHandler) handleConversationBulk(c *gin.Context, accountID uint, req BulkActionRequest) {
if h.worker != nil {
params := service.ConversationBulkActionParams{
Type: req.Type,
ActionName: req.ActionName,
IDs: req.IDs,
Fields: req.Fields,
Labels: req.Labels,
SnoozedUntil: req.SnoozedUntil,
}
if req.AssigneeID != nil && params.Fields.AssigneeID == nil {
params.Fields.AssigneeID = req.AssigneeID
}
if req.TeamID != nil && params.Fields.TeamID == nil {
params.Fields.TeamID = req.TeamID
}
if _, err := service.EnqueueConversationBulkAction(c.Request.Context(), h.worker, accountID, getUserID(c), params); err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to enqueue bulk action")
return
}
c.Status(http.StatusOK)
return
}
successCount := 0
failCount := 0
for _, convID := range req.IDs {
var svcErr error
switch req.ActionName {
case "resolve":
_, svcErr = h.conversationSvc.ToggleStatus(c.Request.Context(), accountID, convID, service.ToggleStatusRequest{Status: string(model.ConversationStatusResolved)})
case "open":
_, svcErr = h.conversationSvc.ToggleStatus(c.Request.Context(), accountID, convID, service.ToggleStatusRequest{Status: string(model.ConversationStatusOpen)})
case "snooze":
_, svcErr = h.conversationSvc.ToggleStatus(c.Request.Context(), accountID, convID, service.ToggleStatusRequest{Status: string(model.ConversationStatusSnoozed)})
case "assign":
if req.AssigneeID != nil {
_, svcErr = h.conversationSvc.AssignAgent(c.Request.Context(), accountID, convID, *req.AssigneeID)
}
case "unassign":
_, svcErr = h.conversationSvc.UnassignAgent(c.Request.Context(), accountID, convID)
case "assign_team":
_, svcErr = h.conversationSvc.AssignTeam(c.Request.Context(), accountID, convID, req.AssigneeID, req.TeamID)
case "delete":
svcErr = h.conversationSvc.Delete(c.Request.Context(), accountID, convID)
case "label_add":
if len(req.Labels.Add) > 0 {
_, svcErr = h.conversationSvc.UpdateLabels(c.Request.Context(), accountID, convID, req.Labels.Add)
}
default:
failCount++
continue
}
if svcErr != nil {
failCount++
} else {
successCount++
}
}
response.OK(c, gin.H{
"success_count": successCount,
"fail_count": failCount,
})
}
// handleContactBulk processes bulk actions on contacts.
// Reference: Chatwoot only supports "delete" and label operations for contacts in bulk_actions.
func (h *BulkActionHandler) handleContactBulk(c *gin.Context, accountID uint, req BulkActionRequest) {
successCount := 0
failCount := 0
for _, contactID := range req.IDs {
var svcErr error
switch req.ActionName {
case "delete":
svcErr = h.contactSvc.Delete(c.Request.Context(), accountID, contactID)
default:
// Chatwoot: other contact actions (labels) require separate async jobs
// For now, return error for unsupported actions
failCount++
continue
}
if svcErr != nil {
failCount++
} else {
successCount++
}
}
response.OK(c, gin.H{
"success_count": successCount,
"fail_count": failCount,
})
}