102 lines
2.9 KiB
Go
102 lines
2.9 KiB
Go
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)
|
|
}
|