423 lines
12 KiB
Go
423 lines
12 KiB
Go
package v1
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gochat/gochat/internal/automation"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/service"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
// MacroHandler handles macro API endpoints.
|
|
// Reference: Chatwoot MacrosController — CRUD + execute
|
|
type MacroHandler struct {
|
|
svc *automation.MacroService
|
|
auditSvc *service.AuditService
|
|
}
|
|
|
|
// NewMacroHandler creates a new MacroHandler.
|
|
func NewMacroHandler(svc *automation.MacroService) *MacroHandler {
|
|
return &MacroHandler{svc: svc}
|
|
}
|
|
|
|
func (h *MacroHandler) WithAuditService(auditSvc *service.AuditService) *MacroHandler {
|
|
h.auditSvc = auditSvc
|
|
return h
|
|
}
|
|
|
|
// List retrieves all macros for an account, respecting visibility.
|
|
// GET /api/v1/accounts/:account_id/macros
|
|
func (h *MacroHandler) List(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
userID := getUserID(c)
|
|
|
|
macros, svcErr := h.svc.ListByAccount(c.Request.Context(), accountID, userID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"payload": serializeMacros(macros),
|
|
})
|
|
}
|
|
|
|
// Get retrieves a single macro by ID.
|
|
// GET /api/v1/accounts/:account_id/macros/:id
|
|
func (h *MacroHandler) Get(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
macroID, err := parseUintParam(c, "macro_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
macro, svcErr := h.svc.GetByIDForAccount(c.Request.Context(), accountID, macroID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
if !h.svc.CanAccess(macro, getUserID(c), getRole(c), "show") {
|
|
abortMacroUnauthorized(c)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": serializeMacro(macro)})
|
|
}
|
|
|
|
// Create creates a new macro.
|
|
// POST /api/v1/accounts/:account_id/macros
|
|
func (h *MacroHandler) Create(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
macro, err := bindMacroRequest(c)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
macro.AccountID = accountID
|
|
macro.CreatedByID = getUserID(c)
|
|
macro.UpdatedByID = getUserID(c)
|
|
|
|
// set_visibility: Chatwoot forces visibility=personal if user role is agent
|
|
// Reference: Chatwoot macro.rb set_visibility — self.visibility = :personal if user.agent?
|
|
role := getRole(c)
|
|
if role == "agent" {
|
|
macro.Visibility = automation.MacroVisibilityPersonal
|
|
}
|
|
|
|
if svcErr := h.svc.Create(c.Request.Context(), macro); svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
recordAuditMutation(c, h.auditSvc, auditMutation{
|
|
AccountID: accountID,
|
|
AuditableType: "Macro",
|
|
AuditableID: macro.ID,
|
|
Action: "create",
|
|
AuditedChanges: serializeMacro(macro),
|
|
})
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": serializeMacro(macro)})
|
|
}
|
|
|
|
// Update updates an existing macro.
|
|
// PUT /api/v1/accounts/:account_id/macros/:id
|
|
func (h *MacroHandler) Update(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
macroID, err := parseUintParam(c, "macro_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
existing, svcErr := h.svc.GetByIDForAccount(c.Request.Context(), accountID, macroID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
if !h.svc.CanAccess(existing, getUserID(c), getRole(c), "update") {
|
|
abortMacroUnauthorized(c)
|
|
return
|
|
}
|
|
|
|
macro, err := bindMacroRequest(c)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
macro.ID = macroID
|
|
macro.UpdatedByID = getUserID(c)
|
|
|
|
updated, svcErr := h.svc.UpdateForAccount(c.Request.Context(), accountID, macro)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
recordAuditMutation(c, h.auditSvc, auditMutation{
|
|
AccountID: accountID,
|
|
AuditableType: "Macro",
|
|
AuditableID: updated.ID,
|
|
Action: "update",
|
|
AuditedChanges: serializeMacro(updated),
|
|
})
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": serializeMacro(updated)})
|
|
}
|
|
|
|
// Delete soft-deletes a macro.
|
|
// DELETE /api/v1/accounts/:account_id/macros/:id
|
|
func (h *MacroHandler) Delete(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
macroID, err := parseUintParam(c, "macro_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
macro, svcErr := h.svc.GetByIDForAccount(c.Request.Context(), accountID, macroID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
if !h.svc.CanAccess(macro, getUserID(c), getRole(c), "destroy") {
|
|
abortMacroUnauthorized(c)
|
|
return
|
|
}
|
|
|
|
if svcErr := h.svc.DeleteForAccount(c.Request.Context(), accountID, macroID); svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
recordAuditMutation(c, h.auditSvc, auditMutation{
|
|
AccountID: accountID,
|
|
AuditableType: "Macro",
|
|
AuditableID: macroID,
|
|
Action: "destroy",
|
|
AuditedChanges: gin.H{"id": macroID},
|
|
})
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// Execute runs a macro on a conversation.
|
|
// POST /api/v1/accounts/:account_id/macros/:id/execute
|
|
// Reference: Chatwoot MacrosController#execute — MacroService.Execute returns error only
|
|
func (h *MacroHandler) Execute(c *gin.Context) {
|
|
macroID, err := parseUintParam(c, "macro_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid macro id")
|
|
return
|
|
}
|
|
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
var body macroExecuteRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "conversation_ids is required")
|
|
return
|
|
}
|
|
displayIDs := body.DisplayIDs()
|
|
|
|
userID := getUserID(c)
|
|
macro, svcErr := h.svc.GetByIDForAccount(c.Request.Context(), accountID, macroID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
if !h.svc.CanAccess(macro, userID, getRole(c), "execute") {
|
|
abortMacroUnauthorized(c)
|
|
return
|
|
}
|
|
|
|
if svcErr := h.svc.ExecuteForDisplayIDs(c.Request.Context(), accountID, macroID, displayIDs, userID); svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// Clone duplicates a macro.
|
|
// POST /api/v1/accounts/:account_id/macros/:id/clone
|
|
// Reference: Chatwoot MacrosController does not have clone, but AutomationRulesController does.
|
|
// gochat adds clone for macros per M6 requirements.
|
|
func (h *MacroHandler) Clone(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
macroID, err := parseUintParam(c, "macro_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
_ = accountID // validated but not used — authorization enforced by middleware
|
|
|
|
cloned, svcErr := h.svc.Clone(c.Request.Context(), macroID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
response.Created(c, cloned)
|
|
}
|
|
|
|
// ToggleActive toggles the active state of a macro.
|
|
// POST /api/v1/accounts/:account_id/macros/:id/toggle_active
|
|
// Reference: Chatwoot does not have toggle_active for macros; gochat adds this per M6 requirements.
|
|
func (h *MacroHandler) ToggleActive(c *gin.Context) {
|
|
macroID, err := parseUintParam(c, "macro_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Active bool `json:"active"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "active field is required")
|
|
return
|
|
}
|
|
|
|
if svcErr := h.svc.ToggleActive(c.Request.Context(), macroID, req.Active); svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"id": macroID,
|
|
"active": req.Active,
|
|
})
|
|
}
|
|
|
|
type macroActionRequest struct {
|
|
ActionName string `json:"action_name"`
|
|
ActionParams json.RawMessage `json:"action_params"`
|
|
}
|
|
|
|
type macroRequest struct {
|
|
Name string `json:"name"`
|
|
Visibility interface{} `json:"visibility"`
|
|
Actions []macroActionRequest `json:"actions"`
|
|
}
|
|
|
|
func bindMacroRequest(c *gin.Context) (*automation.Macro, error) {
|
|
var req macroRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
return nil, err
|
|
}
|
|
actions := make(automation.Actions, 0, len(req.Actions))
|
|
for _, actionReq := range req.Actions {
|
|
params, err := normalizeAutomationActionParams(actionReq.ActionName, actionReq.ActionParams)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
actions = append(actions, automation.Action{ActionName: actionReq.ActionName, ActionParams: params})
|
|
}
|
|
return &automation.Macro{
|
|
Name: strings.TrimSpace(req.Name),
|
|
Visibility: parseMacroVisibility(req.Visibility),
|
|
Actions: actions,
|
|
}, nil
|
|
}
|
|
|
|
func parseMacroVisibility(value interface{}) automation.MacroVisibility {
|
|
switch v := value.(type) {
|
|
case string:
|
|
if strings.EqualFold(v, "global") {
|
|
return automation.MacroVisibilityGlobal
|
|
}
|
|
case float64:
|
|
if int(v) == int(automation.MacroVisibilityGlobal) {
|
|
return automation.MacroVisibilityGlobal
|
|
}
|
|
case int:
|
|
if v == int(automation.MacroVisibilityGlobal) {
|
|
return automation.MacroVisibilityGlobal
|
|
}
|
|
}
|
|
return automation.MacroVisibilityPersonal
|
|
}
|
|
|
|
type macroExecuteRequest struct {
|
|
ConversationIDs []uint `json:"conversation_ids"`
|
|
ConversationID uint `json:"conversation_id"`
|
|
}
|
|
|
|
func (r macroExecuteRequest) DisplayIDs() []uint {
|
|
if len(r.ConversationIDs) > 0 {
|
|
return r.ConversationIDs
|
|
}
|
|
if r.ConversationID > 0 {
|
|
return []uint{r.ConversationID}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func serializeMacros(macros []automation.Macro) []gin.H {
|
|
result := make([]gin.H, 0, len(macros))
|
|
for i := range macros {
|
|
result = append(result, serializeMacro(¯os[i]))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func serializeMacro(macro *automation.Macro) gin.H {
|
|
item := gin.H{
|
|
"id": macro.ID,
|
|
"name": macro.Name,
|
|
"visibility": macroVisibilityString(macro.Visibility),
|
|
"account_id": macro.AccountID,
|
|
"actions": serializeAutomationActions(macro.Actions),
|
|
}
|
|
if macro.CreatedBy != nil && macro.CreatedBy.ID != 0 {
|
|
item["created_by"] = serializeMacroAgent(macro.CreatedBy)
|
|
}
|
|
if macro.UpdatedBy != nil && macro.UpdatedBy.ID != 0 {
|
|
item["updated_by"] = serializeMacroAgent(macro.UpdatedBy)
|
|
}
|
|
return item
|
|
}
|
|
|
|
func macroVisibilityString(visibility automation.MacroVisibility) string {
|
|
if visibility == automation.MacroVisibilityGlobal {
|
|
return "global"
|
|
}
|
|
return "personal"
|
|
}
|
|
|
|
func serializeMacroAgent(user *model.User) gin.H {
|
|
availability := "offline"
|
|
if user.Available {
|
|
availability = "online"
|
|
}
|
|
return gin.H{
|
|
"id": user.ID,
|
|
"name": user.Name,
|
|
"available_name": user.Name,
|
|
"email": user.Email,
|
|
"availability_status": availability,
|
|
"thumbnail": user.AvatarURL,
|
|
}
|
|
}
|
|
|
|
func abortMacroUnauthorized(c *gin.Context) {
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "You are not authorized to do this action"})
|
|
}
|