package plugin import ( "log/slog" "github.com/alexei/tinyforge/internal/crypto" "github.com/alexei/tinyforge/internal/notify" ) // DispatchNotificationForWorkload sends `event` to every notification // route configured for the workload. Resolution order: // // 1. workload_notifications rows matching `event.Type` — multi-route // fan-out (e.g. Slack alerts + Discord successes per workload). // 2. If zero matching rows AND the legacy single-URL columns on the // workload row are set, send to that URL — backwards compat for // installs that pre-date the new table. // 3. Otherwise, fall through to settings.notification_url so the global // destination still fires for workloads with no per-row config. // // Secrets are decrypted via deps.EncKey before sending. A failed decrypt // degrades to "send unsigned" with a warning rather than dropping the // notification — the operator still gets the alert, they just need to // re-save the secret. Fire-and-forget: failures are logged inside // deps.Notifier and never bubble up here. // // Callers (static / dockerfile / image / compose plugins) pass an // already-populated Event; this helper does not synthesize the payload // shape, only the routing. func DispatchNotificationForWorkload(deps Deps, w Workload, event notify.Event) { if deps.Notifier == nil { return } rows, err := deps.Store.ListWorkloadNotifications(w.ID) if err != nil { slog.Warn("notify: list workload routes failed", "workload", w.ID, "error", err) rows = nil } matched := 0 for _, n := range rows { if !n.MatchesEventType(event.Type) { continue } matched++ secret := "" if n.Secret != "" { dec, derr := crypto.Decrypt(deps.EncKey, n.Secret) if derr != nil { slog.Warn("notify: decrypt workload secret failed — sending unsigned", "workload", w.ID, "route", n.Name, "error", derr) } else { secret = dec } } deps.Notifier.SendSigned(n.URL, secret, notify.TierSite, event) } if matched > 0 { return } // Legacy fallback: single per-workload destination on workloads.notification_url. if w.NotificationURL != "" { deps.Notifier.SendSigned(w.NotificationURL, w.NotificationSecret, notify.TierSite, event) return } // Global fallback so a one-line config in settings still notifies // every workload without a per-row override. settings, err := deps.Store.GetSettings() if err != nil { slog.Warn("notify: settings lookup for global fallback failed", "workload", w.ID, "error", err) return } if settings.NotificationURL == "" { return } deps.Notifier.SendSigned(settings.NotificationURL, settings.NotificationSecret, notify.TierSettings, event) }