package triggers import ( "context" "encoding/json" "fmt" "log" "time" "go.starlark.net/starlark" "switchboard-core/events" "switchboard-core/models" "switchboard-core/sandbox" ) // wireEventTrigger subscribes to the bus pattern for an event trigger. func (e *Engine) wireEventTrigger(t *models.Trigger) { if t.EventPattern == "" { return } triggerID := t.ID packageID := t.PackageID entryPoint := t.EntryPoint pattern := t.EventPattern unsub := e.bus.Subscribe(pattern, func(ev events.Event) { // Fire asynchronously — never block the event bus go e.fireEventTrigger(triggerID, packageID, entryPoint, ev) }) e.mu.Lock() e.unsubs[triggerID] = unsub e.mu.Unlock() } // fireEventTrigger invokes the Starlark handler for an event trigger. func (e *Engine) fireEventTrigger(triggerID, packageID, entryPoint string, ev events.Event) { start := time.Now() ctx := e.ctx if ctx == nil { ctx = context.Background() } // Re-check trigger is still enabled trigger, err := e.stores.Triggers.GetByID(ctx, triggerID) if err != nil || trigger == nil || !trigger.Enabled { return } // Load package pkg, err := e.stores.Packages.Get(ctx, packageID) if err != nil || pkg == nil || pkg.Status != models.PackageStatusActive { return } // Check triggers.register permission if !e.hasPermission(ctx, packageID, models.ExtPermTriggersRegister) { return } // Build Starlark context dict ctxDict := starlark.NewDict(6) _ = ctxDict.SetKey(starlark.String("trigger_type"), starlark.String("event")) _ = ctxDict.SetKey(starlark.String("trigger_id"), starlark.String(triggerID)) _ = ctxDict.SetKey(starlark.String("event_label"), starlark.String(ev.Label)) _ = ctxDict.SetKey(starlark.String("event_ts"), starlark.MakeInt64(ev.Ts)) if ev.Room != "" { _ = ctxDict.SetKey(starlark.String("event_room"), starlark.String(ev.Room)) } if len(ev.Payload) > 0 { var payloadMap map[string]any if json.Unmarshal(ev.Payload, &payloadMap) == nil { _ = ctxDict.SetKey(starlark.String("event_payload"), mapToStarlark(payloadMap)) } else { _ = ctxDict.SetKey(starlark.String("event_payload"), starlark.String(string(ev.Payload))) } } // Call entry point with full sandbox context _, output, callErr := e.runner.CallEntryPoint(ctx, pkg, entryPoint, starlark.Tuple{ctxDict}, nil, nil) duration := int(time.Since(start).Milliseconds()) errStr := "" if callErr != nil { errStr = callErr.Error() log.Printf(" ⚠️ trigger[event] %s/%s error: %v", packageID, entryPoint, callErr) } // Update fire state _ = e.stores.Triggers.UpdateFireState(ctx, triggerID, start, errStr) // Log execution e.logExecution(triggerID, "", start, duration, callErr == nil, errStr, output) e.publishEvent("trigger.fired", triggerID) } // hasPermission checks if a package has a specific granted permission. func (e *Engine) hasPermission(ctx context.Context, packageID, permission string) bool { if e.stores.ExtPermissions == nil { return false } granted, err := e.stores.ExtPermissions.GrantedForPackage(ctx, packageID) if err != nil { return false } for _, p := range granted { if p == permission { return true } } return false } // mapToStarlark converts a map[string]any to a Starlark dict. func mapToStarlark(m map[string]any) *starlark.Dict { d := starlark.NewDict(len(m)) for k, v := range m { _ = d.SetKey(starlark.String(k), goToStarlark(v)) } return d } // goToStarlark converts a Go value to a Starlark value. func goToStarlark(v any) starlark.Value { switch val := v.(type) { case nil: return starlark.None case bool: return starlark.Bool(val) case int: return starlark.MakeInt(val) case int64: return starlark.MakeInt64(val) case float64: return starlark.Float(val) case string: return starlark.String(val) case map[string]any: return mapToStarlark(val) case []any: elems := make([]starlark.Value, len(val)) for i, e := range val { elems[i] = goToStarlark(e) } return starlark.NewList(elems) default: return starlark.String(fmt.Sprintf("%v", val)) } } // RunContext builds a sandbox.RunContext for a trigger invocation. func triggerRunContext(userID, teamID string) *sandbox.RunContext { if userID == "" { return nil } return &sandbox.RunContext{ UserID: userID, TeamID: teamID, } }