package notification import ( "crypto/rand" "encoding/hex" "fmt" "log/slog" "time" "prism/service/util" ) type NotificationSender interface { Send(sub *Subscription, notif Notification) error } type linkedChecker interface { IsLinked() (bool, error) } type signalAutoConfigurer interface { CreateDefaultSignalSubscription(appName string) (*SignalSubscription, error) } type telegramSender interface { GetChatID() int64 } type Dispatcher struct { store *Store senders map[Channel]NotificationSender logger *slog.Logger } func NewDispatcher(store *Store, logger *slog.Logger) *Dispatcher { return &Dispatcher{ store: store, senders: make(map[Channel]NotificationSender), logger: logger, } } func (d *Dispatcher) RegisterSender(channel Channel, sender NotificationSender) { d.senders[channel] = sender } func (d *Dispatcher) GetStore() *Store { return d.store } func (d *Dispatcher) HasSignal() bool { _, ok := d.senders[ChannelSignal] return ok } func (d *Dispatcher) HasTelegram() bool { _, ok := d.senders[ChannelTelegram] return ok } func (d *Dispatcher) GetAvailableChannels() []Channel { var channels []Channel if d.HasSignal() { channels = append(channels, ChannelSignal) } if d.HasTelegram() { channels = append(channels, ChannelTelegram) } channels = append(channels, ChannelWebPush) return channels } func (d *Dispatcher) IsValidChannel(channel Channel) bool { return channel.IsAvailable(d.HasSignal(), d.HasTelegram()) } func (d *Dispatcher) Send(appName string, notif Notification) error { app, err := d.store.GetApp(appName) if err != nil { return util.LogError(d.logger, "Failed to get app", err, "app", appName) } if app == nil { d.logger.Info("Registering new app and auto-configuring subscription", "app", appName) app, err = d.CreateAppWithDefaultSubscription(appName) if err != nil { d.logger.Warn("Failed to auto-configure subscription", "app", appName, "error", err) return nil } } if len(app.Subscriptions) == 0 { d.logger.Info("App has no subscriptions, attempting to auto-configure", "app", appName) app, err = d.CreateAppWithDefaultSubscription(appName) if err != nil { d.logger.Warn("Failed to auto-configure subscription", "app", appName, "error", err) return nil } } var lastErr error successCount := 0 for _, sub := range app.Subscriptions { sender, ok := d.senders[sub.Channel] if !ok { d.logger.Debug("Skipping subscription for disabled channel", "channel", sub.Channel, "subscriptionID", sub.ID) continue } if err := d.sendWithRetry(sender, &sub, notif, appName, sub.ID); err != nil { lastErr = err } else { successCount++ } } if successCount == 0 && lastErr != nil { return lastErr } return nil } func (d *Dispatcher) sendWithRetry(sender NotificationSender, sub *Subscription, notif Notification, appName, subscriptionID string) error { maxRetries := 10 baseDelay := 500 * time.Millisecond var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { err := sender.Send(sub, notif) if err == nil { if attempt > 0 { d.logger.Info("Notification sent after retry", "app", appName, "subscriptionID", subscriptionID, "attempt", attempt+1) } return nil } lastErr = err if IsPermanent(err) { d.logger.Error("Permanent error, not retrying", "app", appName, "subscriptionID", subscriptionID, "error", err) return err } if attempt < maxRetries-1 { delay := baseDelay * time.Duration(1<