Files
gochat/internal/automation/condition_validator.go
T

199 lines
6.4 KiB
Go

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
}