Wire automation dispatcher to IMAP sync, drive mutations, and contact CRUD. Add webhook event_types and mail/drive/contacts scope filters (migration 30).
283 lines
6.6 KiB
Go
283 lines
6.6 KiB
Go
package rules
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
const WorkflowVersion = 1
|
|
|
|
type RuleKind string
|
|
|
|
const (
|
|
RuleKindRule RuleKind = "rule"
|
|
RuleKindFunction RuleKind = "function"
|
|
)
|
|
|
|
type TriggerType string
|
|
|
|
const (
|
|
TriggerMessageReceived TriggerType = "message_received"
|
|
TriggerLabelAdded TriggerType = "label_added"
|
|
TriggerLabelRemoved TriggerType = "label_removed"
|
|
TriggerDriveFileCreated TriggerType = "drive_file_created"
|
|
TriggerDriveFileUpdated TriggerType = "drive_file_updated"
|
|
TriggerDriveFileDeleted TriggerType = "drive_file_deleted"
|
|
TriggerDriveFileMoved TriggerType = "drive_file_moved"
|
|
TriggerDriveShareUpdated TriggerType = "drive_share_updated"
|
|
TriggerContactCreated TriggerType = "contact_created"
|
|
TriggerContactUpdated TriggerType = "contact_updated"
|
|
TriggerContactDeleted TriggerType = "contact_deleted"
|
|
)
|
|
|
|
type Trigger struct {
|
|
Type TriggerType `json:"type"`
|
|
FolderID string `json:"folder_id,omitempty"`
|
|
Label string `json:"label,omitempty"`
|
|
AccountID string `json:"account_id,omitempty"`
|
|
FolderPath string `json:"folder_path,omitempty"`
|
|
ContactLabel string `json:"contact_label,omitempty"`
|
|
}
|
|
|
|
type TriggerGroup struct {
|
|
Operator string `json:"operator"` // "or"
|
|
Groups []TriggerAnd `json:"groups"`
|
|
}
|
|
|
|
type TriggerAnd struct {
|
|
Operator string `json:"operator"` // "and"
|
|
Items []Trigger `json:"items"`
|
|
}
|
|
|
|
type ExecVariable struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"` // string, number, boolean
|
|
Default string `json:"default,omitempty"`
|
|
}
|
|
|
|
type WorkflowNode struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Position json.RawMessage `json:"position,omitempty"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
|
|
type WorkflowEdge struct {
|
|
ID string `json:"id"`
|
|
Source string `json:"source"`
|
|
Target string `json:"target"`
|
|
SourceHandle string `json:"sourceHandle,omitempty"`
|
|
}
|
|
|
|
type Workflow struct {
|
|
Version int `json:"version"`
|
|
Kind RuleKind `json:"kind"`
|
|
Triggers TriggerGroup `json:"triggers"`
|
|
Variables []ExecVariable `json:"variables,omitempty"`
|
|
Nodes []WorkflowNode `json:"nodes"`
|
|
Edges []WorkflowEdge `json:"edges"`
|
|
}
|
|
|
|
type ConditionNodeData struct {
|
|
Field string `json:"field"`
|
|
Operator string `json:"operator"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type LabelCheckNodeData struct {
|
|
Label string `json:"label"`
|
|
Operator string `json:"operator"` // has, not_has
|
|
}
|
|
|
|
type SwitchCase struct {
|
|
Value string `json:"value"`
|
|
Label string `json:"label,omitempty"`
|
|
}
|
|
|
|
type SwitchNodeData struct {
|
|
Field string `json:"field"`
|
|
Cases []SwitchCase `json:"cases"`
|
|
}
|
|
|
|
type LLMCheckNodeData struct {
|
|
Prompt string `json:"prompt"`
|
|
Provider string `json:"provider,omitempty"`
|
|
Model string `json:"model,omitempty"`
|
|
}
|
|
|
|
type ActionItem struct {
|
|
Type string `json:"type"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type ActionsNodeData struct {
|
|
Actions []ActionItem `json:"actions"`
|
|
}
|
|
|
|
type SetVarNodeData struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type CallRuleNodeData struct {
|
|
RuleID string `json:"rule_id"`
|
|
}
|
|
|
|
type EventContext struct {
|
|
Type TriggerType
|
|
FolderID string
|
|
Label string
|
|
FolderPath string
|
|
ContactLabel string
|
|
// Drive payload (when domain is drive)
|
|
DriveFileName string
|
|
DriveFilePath string
|
|
DriveMimeType string
|
|
DriveFileSize int64
|
|
DriveIsFolder bool
|
|
// Contact payload (when domain is contacts)
|
|
ContactID string
|
|
ContactBookID string
|
|
ContactName string
|
|
ContactEmail string
|
|
ContactPhone string
|
|
ContactOrg string
|
|
}
|
|
|
|
func ParseWorkflow(raw []byte) (*Workflow, error) {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return nil, nil
|
|
}
|
|
var wf Workflow
|
|
if err := json.Unmarshal(raw, &wf); err != nil {
|
|
return nil, fmt.Errorf("parse workflow: %w", err)
|
|
}
|
|
if wf.Version == 0 {
|
|
wf.Version = WorkflowVersion
|
|
}
|
|
if wf.Kind == "" {
|
|
wf.Kind = RuleKindRule
|
|
}
|
|
return &wf, nil
|
|
}
|
|
|
|
func (wf *Workflow) nodeMap() map[string]WorkflowNode {
|
|
m := make(map[string]WorkflowNode, len(wf.Nodes))
|
|
for _, n := range wf.Nodes {
|
|
m[n.ID] = n
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (wf *Workflow) outgoingEdges(nodeID string) []WorkflowEdge {
|
|
var out []WorkflowEdge
|
|
for _, e := range wf.Edges {
|
|
if e.Source == nodeID {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (wf *Workflow) nextNode(nodeID, handle string) string {
|
|
for _, e := range wf.Edges {
|
|
if e.Source == nodeID && e.SourceHandle == handle {
|
|
return e.Target
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (wf *Workflow) nextDefault(nodeID string) string {
|
|
for _, e := range wf.Edges {
|
|
if e.Source == nodeID && e.SourceHandle == "" {
|
|
return e.Target
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (wf *Workflow) findStartNode() string {
|
|
for _, n := range wf.Nodes {
|
|
if n.Type == "start" {
|
|
return n.ID
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func matchesTriggers(triggers TriggerGroup, msg *Message, evt *EventContext) bool {
|
|
if len(triggers.Groups) == 0 {
|
|
return true
|
|
}
|
|
for _, group := range triggers.Groups {
|
|
if matchesTriggerAnd(group, msg, evt) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchesTriggerAnd(group TriggerAnd, msg *Message, evt *EventContext) bool {
|
|
if len(group.Items) == 0 {
|
|
return true
|
|
}
|
|
for _, t := range group.Items {
|
|
if !matchTrigger(t, msg, evt) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func matchTrigger(t Trigger, msg *Message, evt *EventContext) bool {
|
|
switch t.Type {
|
|
case TriggerMessageReceived:
|
|
if evt != nil && evt.Type != TriggerMessageReceived && evt.Type != "" {
|
|
return false
|
|
}
|
|
if t.AccountID != "" && msg.AccountID != "" && t.AccountID != msg.AccountID {
|
|
return false
|
|
}
|
|
if t.FolderID != "" && msg.FolderID != "" && t.FolderID != msg.FolderID {
|
|
return false
|
|
}
|
|
return true
|
|
case TriggerLabelAdded:
|
|
if evt == nil || evt.Type != TriggerLabelAdded {
|
|
return false
|
|
}
|
|
if t.Label != "" && t.Label != evt.Label {
|
|
return false
|
|
}
|
|
return true
|
|
case TriggerLabelRemoved:
|
|
if evt == nil || evt.Type != TriggerLabelRemoved {
|
|
return false
|
|
}
|
|
if t.Label != "" && t.Label != evt.Label {
|
|
return false
|
|
}
|
|
return true
|
|
case TriggerDriveFileCreated, TriggerDriveFileUpdated, TriggerDriveFileDeleted, TriggerDriveFileMoved, TriggerDriveShareUpdated:
|
|
if evt == nil || evt.Type != t.Type {
|
|
return false
|
|
}
|
|
if t.FolderPath != "" && evt.FolderPath != "" && !strings.HasPrefix(evt.DriveFilePath, t.FolderPath) {
|
|
return false
|
|
}
|
|
return true
|
|
case TriggerContactCreated, TriggerContactUpdated, TriggerContactDeleted:
|
|
if evt == nil || evt.Type != t.Type {
|
|
return false
|
|
}
|
|
if t.ContactLabel != "" && evt.ContactLabel != "" && t.ContactLabel != evt.ContactLabel {
|
|
return false
|
|
}
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|