Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user