Feat triggers v0.2.2 (#6)
All checks were successful
All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #6.
This commit is contained in:
163
server/triggers/event.go
Normal file
163
server/triggers/event.go
Normal file
@@ -0,0 +1,163 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user