package automation import "fmt" var chatwootToLocalFilterOperator = map[string]string{ "equal_to": "equal", "not_equal_to": "not_equal", } // NormalizeFilterOperator converts Chatwoot frontend operator names to the // internal names used by the condition matcher. func NormalizeFilterOperator(op string) string { if normalized, ok := chatwootToLocalFilterOperator[op]; ok { return normalized } return op } // ChatwootFilterOperator converts internal operator names to the dashboard // values used by Chatwoot's automation builder. func ChatwootFilterOperator(op string) string { switch op { case "equal": return "equal_to" case "not_equal": return "not_equal_to" default: return op } } // NormalizeConditions fills Chatwoot's attribute_key into the internal // Attribute field and normalizes filter operator aliases before validation or // execution. func NormalizeConditions(conditions Conditions) Conditions { for i := range conditions { if conditions[i].Attribute == "" { conditions[i].Attribute = conditions[i].AttributeKey } if conditions[i].AttributeKey == "" { conditions[i].AttributeKey = conditions[i].Attribute } conditions[i].FilterOperator = NormalizeFilterOperator(conditions[i].FilterOperator) } return conditions } // WhitelistedConditionAttributes defines the set of valid condition attributes. // Reference: Chatwoot ConditionValidationService — validates condition keys against whitelisted attributes. var WhitelistedConditionAttributes = map[string]bool{ // Conversation attributes "status": true, "priority": true, "assignee_id": true, "team_id": true, "inbox_id": true, "contact_id": true, "channel_type": true, "labels": true, "contact": true, // Message/content attributes "content": true, "message_type": true, "private_note": true, "email": true, "mail_subject": true, "phone_number": true, "company_name": true, "referer": true, "campaigns": true, "conversation_language": true, // Country/locale attributes (Chatwoot extensions) "country_code": true, "browser_language": true, } // WhitelistedFilterOperators defines the set of valid filter operators. // Reference: Chatwoot filter_operator enum var WhitelistedFilterOperators = map[string]bool{ "equal": true, "equal_to": true, "not_equal": true, "not_equal_to": true, "contains": true, "does_not_contain": true, "is_present": true, "is_not_present": true, "attribute_changed": true, "is_greater_than": true, "is_less_than": true, "starts_with": true, "days_before": true, } // WhitelistedQueryOperators defines the set of valid query operators for combining conditions. var WhitelistedQueryOperators = map[string]bool{ "and": true, "or": true, } // WhitelistedActionTypes defines the set of valid automation action types. // Reference: Chatwoot AutomationRule action_name enum var WhitelistedActionTypes = map[string]bool{ "assign_agent": true, "assign_team": true, // Chatwoot: assign conversation to a team "change_status": true, "add_label": true, "remove_label": true, "send_message": true, "send_attachment": true, "send_email_to_contact": true, "send_email_to_team": true, "send_email_transcript": true, "send_webhook_event": true, "mute_conversation": true, "snooze_conversation": true, // Chatwoot: snooze until a specified time "resolve_conversation": true, // Chatwoot: resolve immediately "open_conversation": true, "pending_conversation": true, "change_priority": true, "add_sla": true, "add_private_note": true, "remove_assigned_agent": true, "remove_assigned_team": true, } // ValidateConditions checks that all conditions use valid attributes, filter operators, // and query operators. // Reference: Chatwoot ConditionValidationService — validates condition keys against whitelisted attributes func ValidateConditions(conditions Conditions) error { conditions = NormalizeConditions(conditions) for i, c := range conditions { // Validate attribute if !WhitelistedConditionAttributes[c.Attribute] { return fmt.Errorf("condition[%d]: invalid attribute '%s'", i, c.Attribute) } // Validate filter operator filterOperator := NormalizeFilterOperator(c.FilterOperator) if !WhitelistedFilterOperators[filterOperator] { return fmt.Errorf("condition[%d]: invalid filter_operator '%s'", i, c.FilterOperator) } // Validate query operator (defaults to "and" if empty) if c.QueryOperator != "" { if !WhitelistedQueryOperators[c.QueryOperator] { return fmt.Errorf("condition[%d]: invalid query_operator '%s'", i, c.QueryOperator) } } // Validate values — attribute_changed requires from/to values if filterOperator == "attribute_changed" { if len(c.Values) == 0 { return fmt.Errorf("condition[%d]: attribute_changed requires at least one value (to)", i) } } if err := validateAttributeOperator(c.Attribute, filterOperator); err != nil { return fmt.Errorf("condition[%d]: %w", i, err) } // is_present / is_not_present should not need values if filterOperator == "is_present" || filterOperator == "is_not_present" { // Values are optional for presence checks — not enforced } else if len(c.Values) == 0 { return fmt.Errorf("condition[%d]: filter_operator '%s' requires at least one value", i, c.FilterOperator) } } return nil } func validateAttributeOperator(attribute string, operator string) error { switch attribute { case "message_type", "private_note": if operator != "equal" && operator != "not_equal" { return fmt.Errorf("attribute '%s' does not support filter_operator '%s'", attribute, operator) } case "content": if operator != "equal" && operator != "not_equal" && operator != "contains" && operator != "does_not_contain" { return fmt.Errorf("attribute 'content' does not support filter_operator '%s'", operator) } } return nil } // ValidateActions checks that all actions use valid action types. // Reference: Chatwoot AutomationRule action_name validation func ValidateActions(actions Actions) error { for i, a := range actions { if !WhitelistedActionTypes[a.ActionName] { return fmt.Errorf("action[%d]: invalid action_name '%s'", i, a.ActionName) } } return nil }