package alerter import ( "context" "fmt" "log/slog" "time" "gitea.computernetthings.ru/yash/crypto_alert_bot/internal/entities" "gitea.computernetthings.ru/yash/crypto_alert_bot/internal/provider" "github.com/shopspring/decimal" ) type Notifier interface { NotifyAlert(ctx context.Context, userID entities.UserID, alert *entities.Alert, currentPrice decimal.Decimal) error } type Storage interface { AllActiveAlerts(ctx context.Context) ([]entities.Alert, error) DisableAlert(ctx context.Context, id entities.AlertID) error } type Alerter struct { log *slog.Logger cache *alertsCache priceProvider provider.Provider notifier Notifier storage Storage } const interval = time.Minute func New(log *slog.Logger, priceProvider provider.Provider, notifier Notifier, storage Storage) *Alerter { return &Alerter{ log: log, cache: newCache(), priceProvider: priceProvider, notifier: notifier, storage: storage, } } func (a *Alerter) LoadAlerts(ctx context.Context) error { alerts, err := a.storage.AllActiveAlerts(ctx) if err != nil { return fmt.Errorf("failed to load alerts: %w", err) } for i := range alerts { a.cache.Add(&alerts[i]) } a.log.Info("alerts loaded", "count", len(alerts)) return nil } func (a *Alerter) AddAlert(alert *entities.Alert) { a.cache.Add(alert) } func (a *Alerter) RemoveAlert(id entities.AlertID) { a.cache.Remove(id) } func (a *Alerter) Run(ctx context.Context) { go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: } a.log.Info("start checking alerts") if err := a.checkAlerts(ctx); err != nil { a.log.Error("failed to check alerts", "err", err) continue } a.log.Info("alerts checked") } }() } // TODO: parallel checking for different instruments. func (a *Alerter) checkAlerts(ctx context.Context) error { instruments := a.cache.Instruments() for _, instrument := range instruments { price, err := a.priceProvider.Price(ctx, instrument) if err != nil { a.log.Error("failed to get price", "instrument", instrument.ID, "err", err) continue } alerts := a.cache.AlertsByInstrument(instrument.ID) for _, alert := range alerts { switch alert.Condition { case entities.AlertConditionAbove: if price.Ask.GreaterThanOrEqual(alert.Price) { a.triggerAlert(ctx, alert, price.Ask) } case entities.AlertConditionBelow: if price.Bid.LessThanOrEqual(alert.Price) { a.triggerAlert(ctx, alert, price.Bid) } } } } return nil } func (a *Alerter) triggerAlert(ctx context.Context, alert *entities.Alert, currentPrice decimal.Decimal) { if err := a.notifier.NotifyAlert(ctx, alert.UserID, alert, currentPrice); err != nil { a.log.Error("failed to notify alert", "alert_id", alert.ID, "err", err) return } a.cache.Remove(alert.ID) if err := a.storage.DisableAlert(ctx, alert.ID); err != nil { a.log.Error("failed to disable alert in db", "alert_id", alert.ID, "err", err) } }