504 lines
15 KiB
Go
504 lines
15 KiB
Go
package v1
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gochat/gochat/internal/automation"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
// AutomationRuleHandler handles automation rule API endpoints.
|
|
// Reference: Chatwoot AutomationRulesController — CRUD + clone
|
|
type AutomationRuleHandler struct {
|
|
svc *automation.AutomationRuleService
|
|
}
|
|
|
|
// NewAutomationRuleHandler creates a new AutomationRuleHandler.
|
|
func NewAutomationRuleHandler(svc *automation.AutomationRuleService) *AutomationRuleHandler {
|
|
return &AutomationRuleHandler{svc: svc}
|
|
}
|
|
|
|
type automationRuleRequest struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
EventName string `json:"event_name"`
|
|
Active *bool `json:"active"`
|
|
Conditions []automationRuleConditionRequest `json:"conditions"`
|
|
Actions []automationRuleActionRequest `json:"actions"`
|
|
}
|
|
|
|
type automationRuleConditionRequest struct {
|
|
AttributeKey string `json:"attribute_key"`
|
|
Attribute string `json:"attribute"`
|
|
FilterOperator string `json:"filter_operator"`
|
|
Values []interface{} `json:"values"`
|
|
QueryOperator string `json:"query_operator"`
|
|
CustomAttributeType string `json:"custom_attribute_type"`
|
|
}
|
|
|
|
type automationRuleActionRequest struct {
|
|
ActionName string `json:"action_name"`
|
|
ActionParams json.RawMessage `json:"action_params"`
|
|
}
|
|
|
|
// List retrieves all automation rules for an account.
|
|
// GET /api/v1/accounts/:account_id/automation_rules
|
|
func (h *AutomationRuleHandler) List(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
rules, svcErr := h.svc.ListByAccount(c.Request.Context(), accountID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"payload": serializeAutomationRules(rules),
|
|
})
|
|
}
|
|
|
|
// Get retrieves a single automation rule by ID.
|
|
// GET /api/v1/accounts/:account_id/automation_rules/:id
|
|
func (h *AutomationRuleHandler) Get(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
automationID, err := parseUintParam(c, "automation_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
rule, svcErr := h.svc.GetByIDForAccount(c.Request.Context(), accountID, automationID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": serializeAutomationRule(rule)})
|
|
}
|
|
|
|
// Create creates a new automation rule.
|
|
// POST /api/v1/accounts/:account_id/automation_rules
|
|
func (h *AutomationRuleHandler) 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 automationRuleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
if req.Name == "" || req.EventName == "" {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "name and event_name are required")
|
|
return
|
|
}
|
|
|
|
rule, bindErr := buildAutomationRuleFromRequest(accountID, req)
|
|
if bindErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, bindErr.Error())
|
|
return
|
|
}
|
|
|
|
if svcErr := h.svc.Create(c.Request.Context(), rule); svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, serializeAutomationRule(rule))
|
|
}
|
|
|
|
// Update updates an existing automation rule.
|
|
// PUT /api/v1/accounts/:account_id/automation_rules/:id
|
|
func (h *AutomationRuleHandler) Update(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
automationID, err := parseUintParam(c, "automation_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
var req automationRuleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
if req.Name == "" || req.EventName == "" {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "name and event_name are required")
|
|
return
|
|
}
|
|
|
|
rule, bindErr := buildAutomationRuleFromRequest(accountID, req)
|
|
if bindErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, bindErr.Error())
|
|
return
|
|
}
|
|
rule.ID = automationID
|
|
|
|
if svcErr := h.svc.UpdateForAccount(c.Request.Context(), accountID, rule); svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
updated, svcErr := h.svc.GetByIDForAccount(c.Request.Context(), accountID, automationID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"payload": serializeAutomationRule(updated)})
|
|
}
|
|
|
|
// Delete soft-deletes an automation rule.
|
|
// DELETE /api/v1/accounts/:account_id/automation_rules/:id
|
|
func (h *AutomationRuleHandler) Delete(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
automationID, err := parseUintParam(c, "automation_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
if svcErr := h.svc.DeleteForAccount(c.Request.Context(), accountID, automationID); svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// Clone duplicates an automation rule.
|
|
// POST /api/v1/accounts/:account_id/automation_rules/:id/clone
|
|
func (h *AutomationRuleHandler) Clone(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
automationID, err := parseUintParam(c, "automation_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
cloned, svcErr := h.svc.CloneForAccount(c.Request.Context(), accountID, automationID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": serializeAutomationRule(cloned)})
|
|
}
|
|
|
|
// ToggleActive toggles the active state of an automation rule.
|
|
// POST /api/v1/accounts/:account_id/automation_rules/:id/toggle_active
|
|
// Reference: Chatwoot does not have a toggle_active endpoint; gochat adds this per M6 requirements.
|
|
func (h *AutomationRuleHandler) ToggleActive(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
automationID, err := parseUintParam(c, "automation_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 req.Active == nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "active field is required")
|
|
return
|
|
}
|
|
|
|
if svcErr := h.svc.ToggleActiveForAccount(c.Request.Context(), accountID, automationID, *req.Active); svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
rule, svcErr := h.svc.GetByIDForAccount(c.Request.Context(), accountID, automationID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"payload": serializeAutomationRule(rule)})
|
|
}
|
|
|
|
func buildAutomationRuleFromRequest(accountID uint, req automationRuleRequest) (*automation.AutomationRule, error) {
|
|
active := true
|
|
if req.Active != nil {
|
|
active = *req.Active
|
|
}
|
|
|
|
conditions := make(automation.Conditions, 0, len(req.Conditions))
|
|
for _, condition := range req.Conditions {
|
|
attribute := condition.Attribute
|
|
if attribute == "" {
|
|
attribute = condition.AttributeKey
|
|
}
|
|
values := make([]string, 0, len(condition.Values))
|
|
for _, value := range condition.Values {
|
|
if value == nil {
|
|
continue
|
|
}
|
|
values = append(values, valueToString(value))
|
|
}
|
|
conditions = append(conditions, automation.Condition{
|
|
Attribute: attribute,
|
|
AttributeKey: attribute,
|
|
FilterOperator: automation.NormalizeFilterOperator(condition.FilterOperator),
|
|
Values: values,
|
|
QueryOperator: condition.QueryOperator,
|
|
CustomAttributeType: condition.CustomAttributeType,
|
|
})
|
|
}
|
|
|
|
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.AutomationRule{
|
|
AccountID: accountID,
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
EventName: req.EventName,
|
|
Conditions: automation.NormalizeConditions(conditions),
|
|
Actions: actions,
|
|
Active: active,
|
|
}, nil
|
|
}
|
|
|
|
func serializeAutomationRules(rules []automation.AutomationRule) []gin.H {
|
|
result := make([]gin.H, 0, len(rules))
|
|
for i := range rules {
|
|
result = append(result, serializeAutomationRule(&rules[i]))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func serializeAutomationRule(rule *automation.AutomationRule) gin.H {
|
|
return gin.H{
|
|
"id": rule.ID,
|
|
"account_id": rule.AccountID,
|
|
"name": rule.Name,
|
|
"description": rule.Description,
|
|
"event_name": rule.EventName,
|
|
"conditions": serializeAutomationConditions(rule.Conditions),
|
|
"actions": serializeAutomationActions(rule.Actions),
|
|
"created_on": rule.CreatedAt.Unix(),
|
|
"active": rule.Active,
|
|
}
|
|
}
|
|
|
|
func serializeAutomationConditions(conditions automation.Conditions) []gin.H {
|
|
conditions = automation.NormalizeConditions(conditions)
|
|
result := make([]gin.H, 0, len(conditions))
|
|
for _, condition := range conditions {
|
|
item := gin.H{
|
|
"attribute_key": condition.Attribute,
|
|
"filter_operator": automation.ChatwootFilterOperator(condition.FilterOperator),
|
|
"values": condition.Values,
|
|
}
|
|
if condition.QueryOperator != "" {
|
|
item["query_operator"] = condition.QueryOperator
|
|
}
|
|
if condition.CustomAttributeType != "" {
|
|
item["custom_attribute_type"] = condition.CustomAttributeType
|
|
}
|
|
result = append(result, item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func serializeAutomationActions(actions automation.Actions) []gin.H {
|
|
result := make([]gin.H, 0, len(actions))
|
|
for _, action := range actions {
|
|
result = append(result, gin.H{
|
|
"action_name": action.ActionName,
|
|
"action_params": chatwootActionParams(action),
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func normalizeAutomationActionParams(actionName string, raw json.RawMessage) (map[string]interface{}, error) {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
var object map[string]interface{}
|
|
if err := json.Unmarshal(raw, &object); err == nil {
|
|
if object == nil {
|
|
object = map[string]interface{}{}
|
|
}
|
|
return object, nil
|
|
}
|
|
var values []interface{}
|
|
if err := json.Unmarshal(raw, &values); err != nil {
|
|
var single interface{}
|
|
if singleErr := json.Unmarshal(raw, &single); singleErr != nil {
|
|
return nil, err
|
|
}
|
|
values = []interface{}{single}
|
|
}
|
|
return actionArrayToMap(actionName, values), nil
|
|
}
|
|
|
|
func actionArrayToMap(actionName string, values []interface{}) map[string]interface{} {
|
|
params := map[string]interface{}{}
|
|
first := firstValue(values)
|
|
switch actionName {
|
|
case "assign_agent":
|
|
params["assignee_id"] = first
|
|
case "assign_team", "send_email_to_team":
|
|
params["team_id"] = first
|
|
case "add_label", "remove_label":
|
|
params["labels"] = valuesToStrings(values)
|
|
case "change_status":
|
|
params["status"] = first
|
|
case "change_priority":
|
|
params["priority"] = first
|
|
case "send_message", "add_private_note":
|
|
params["content"] = first
|
|
case "send_email_to_contact", "send_email_transcript":
|
|
params["email"] = first
|
|
case "send_attachment":
|
|
params["blob_id"] = first
|
|
case "add_sla":
|
|
params["sla_policy_id"] = first
|
|
default:
|
|
params["values"] = values
|
|
}
|
|
return params
|
|
}
|
|
|
|
func chatwootActionParams(action automation.Action) []interface{} {
|
|
params := action.ActionParams
|
|
if params == nil {
|
|
return []interface{}{}
|
|
}
|
|
switch action.ActionName {
|
|
case "assign_agent":
|
|
return compactValues(params["assignee_id"], params["agent_id"])
|
|
case "assign_team", "send_email_to_team":
|
|
return compactValues(params["team_id"])
|
|
case "add_label", "remove_label":
|
|
if labels, ok := params["labels"]; ok {
|
|
return valuesToInterfaces(labels)
|
|
}
|
|
return compactValues(params["label"])
|
|
case "change_status":
|
|
return compactValues(params["status"])
|
|
case "change_priority":
|
|
return compactValues(params["priority"])
|
|
case "send_message", "add_private_note":
|
|
return compactValues(params["content"], params["message"])
|
|
case "send_email_to_contact", "send_email_transcript":
|
|
return compactValues(params["email"])
|
|
case "send_attachment":
|
|
return compactValues(params["blob_id"], params["attachment_url"])
|
|
case "add_sla":
|
|
return compactValues(params["sla_policy_id"])
|
|
default:
|
|
if values, ok := params["values"]; ok {
|
|
return valuesToInterfaces(values)
|
|
}
|
|
return []interface{}{}
|
|
}
|
|
}
|
|
|
|
func firstValue(values []interface{}) interface{} {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
return values[0]
|
|
}
|
|
|
|
func compactValues(values ...interface{}) []interface{} {
|
|
for _, value := range values {
|
|
if value != nil {
|
|
return []interface{}{value}
|
|
}
|
|
}
|
|
return []interface{}{}
|
|
}
|
|
|
|
func valuesToInterfaces(value interface{}) []interface{} {
|
|
switch typed := value.(type) {
|
|
case []interface{}:
|
|
return typed
|
|
case []string:
|
|
result := make([]interface{}, 0, len(typed))
|
|
for _, item := range typed {
|
|
result = append(result, item)
|
|
}
|
|
return result
|
|
case nil:
|
|
return []interface{}{}
|
|
default:
|
|
return []interface{}{typed}
|
|
}
|
|
}
|
|
|
|
func valuesToStrings(values []interface{}) []string {
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
if value != nil {
|
|
result = append(result, valueToString(value))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func valueToString(value interface{}) string {
|
|
switch typed := value.(type) {
|
|
case string:
|
|
return typed
|
|
case json.Number:
|
|
return typed.String()
|
|
default:
|
|
return jsonNumberSafeString(typed)
|
|
}
|
|
}
|
|
|
|
func jsonNumberSafeString(value interface{}) string {
|
|
bytes, err := json.Marshal(value)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(bytes)
|
|
}
|