// Package notify provides notification functionality. package notify import ( "context" "fmt" "sync" "sync/atomic" log "github.com/go-pkgz/lgr" "github.com/umputun/remark/backend/app/store" ) // Service delivers notifications to multiple destinations type Service struct { dataService Store destinations []Destination queue chan request closed uint32 // non-zero means closed. uses uint instead of bool for atomic ctx context.Context cancel context.CancelFunc } // Destination defines interface for a given destination service, like telegram, email and so on type Destination interface { fmt.Stringer Send(ctx context.Context, req request) error } // Store defines the minimal interface accessing stored comments used by notifier type Store interface { Get(locator store.Locator, id string) (store.Comment, error) } type request struct { comment store.Comment parent store.Comment } const defaultQueueSize = 100 const uiNav = "#remark42__comment-" // NewService makes notification service routing comments to all destinations. func NewService(dataService Store, size int, destinations ...Destination) *Service { if size <= 0 { size = defaultQueueSize } ctx, cancel := context.WithCancel(context.Background()) res := Service{ dataService: dataService, queue: make(chan request, size), destinations: destinations, ctx: ctx, cancel: cancel, } if len(destinations) > 0 { go res.do() } log.Printf("[INFO] create notifier service, queue size=%d, destinations=%d", size, len(destinations)) return &res } // Submit comment to internal channel if not busy, drop if can't send func (s *Service) Submit(comment store.Comment) { if len(s.destinations) == 0 || atomic.LoadUint32(&s.closed) != 0 { return } parentComment := store.Comment{} if s.dataService != nil { if p, err := s.dataService.Get(comment.Locator, comment.ParentID); err == nil { parentComment = p } } select { case s.queue <- request{comment: comment, parent: parentComment}: default: log.Printf("[WARN] can't send comment notification to queue, %+v", comment) } } // Close queue channel and wait for completion func (s *Service) Close() { if s.queue != nil { log.Print("[DEBUG] close notifier") close(s.queue) s.cancel() <-s.ctx.Done() } atomic.StoreUint32(&s.closed, 1) } func (s *Service) do() { for c := range s.queue { var wg sync.WaitGroup wg.Add(len(s.destinations)) for _, dest := range s.destinations { go func(d Destination) { if err := d.Send(s.ctx, c); err != nil { log.Printf("[WARN] failed to send to %s, %s", d, err) } wg.Done() }(dest) } wg.Wait() } log.Print("[WARN] terminated notifier") } // NopService is do-nothing notifier, without destinations var NopService = &Service{}