package notify import ( "context" "encoding/json" "errors" "fmt" "net/http" "time" "github.com/google/uuid" "github.com/vasyakrg/dns-autoresolver/internal/store" ) // ChannelStore is the narrow store dependency Dispatcher needs: the set of // enabled notification channels for a project. type ChannelStore interface { ListEnabledChannels(ctx context.Context, projectID uuid.UUID) ([]store.Channel, error) } // Decryptor decrypts a channel's stored secret (bot token, signing key, ...). type Decryptor interface { Decrypt(enc string) ([]byte, error) } // Dispatcher fans an Event out to every enabled channel of a project, // picking the Notifier implementation by channel type. A failure on one // channel does not stop delivery to the others; all errors are aggregated // via errors.Join. type Dispatcher struct { store ChannelStore cipher Decryptor byType map[string]Notifier } // NewDispatcher builds a Dispatcher wired with the default Telegram and // Webhook notifiers. func NewDispatcher(store ChannelStore, cipher Decryptor) *Dispatcher { return &Dispatcher{ store: store, cipher: cipher, byType: map[string]Notifier{ "telegram": &Telegram{BaseURL: "https://api.telegram.org", HTTP: &http.Client{Timeout: 15 * time.Second}}, "webhook": &Webhook{HTTP: &http.Client{ Timeout: 15 * time.Second, Transport: newWebhookTransport(false), }}, }, } } // Send delivers ev to every enabled channel of projectID. Errors from // individual channels are aggregated (via errors.Join) rather than aborting // delivery to the remaining channels. func (d *Dispatcher) Send(ctx context.Context, projectID uuid.UUID, ev Event) error { channels, err := d.store.ListEnabledChannels(ctx, projectID) if err != nil { return err } var errs []error for _, ch := range channels { n, ok := d.byType[ch.Type] if !ok { continue } secret := "" if ch.SecretEnc != "" { b, err := d.cipher.Decrypt(ch.SecretEnc) if err != nil { errs = append(errs, err) continue } secret = string(b) } if err := n.Send(ctx, ch.Config, secret, ev); err != nil { errs = append(errs, err) } } return errors.Join(errs...) } // SendTest sends a single synthetic Event directly through the Notifier for // channelType, bypassing project/channel lookup entirely. It satisfies // api.TestSender and backs POST /channels/{cid}/test, letting a user verify // a channel's bot_token/chat_id or webhook URL works before enabling the // schedule — the api layer resolves the channel and decrypts its secret; this // method only performs the actual delivery attempt. func (d *Dispatcher) SendTest(ctx context.Context, channelType string, config json.RawMessage, secret string) error { n, ok := d.byType[channelType] if !ok { return fmt.Errorf("notify: unknown channel type %q", channelType) } ev := Event{ Project: "test", Domain: "test", Status: "test", Summary: "test notification", At: time.Now(), } return n.Send(ctx, config, secret, ev) }