Add support for multiple instances of the same notifier type
This commit is contained in:
+47
-10
@@ -29,10 +29,10 @@ type ServerConfig struct {
|
||||
|
||||
// NotifiersConfig contains configuration for all notifier types
|
||||
type NotifiersConfig struct {
|
||||
SMTP *notifier.SMTPConfig `mapstructure:"smtp"`
|
||||
Slack *notifier.SlackConfig `mapstructure:"slack"`
|
||||
Ntfy *notifier.NtfyConfig `mapstructure:"ntfy"`
|
||||
Stdout bool `mapstructure:"stdout"` // Enable stdout notifier
|
||||
SMTP map[string]*notifier.SMTPConfig `mapstructure:"smtp"`
|
||||
Slack map[string]*notifier.SlackConfig `mapstructure:"slack"`
|
||||
Ntfy map[string]*notifier.NtfyConfig `mapstructure:"ntfy"`
|
||||
Stdout bool `mapstructure:"stdout"` // Enable stdout notifier
|
||||
}
|
||||
|
||||
// LoggingConfig contains logging configuration
|
||||
@@ -185,9 +185,9 @@ func (c *Config) Validate() error {
|
||||
// HasAnyNotifier checks if at least one notifier is configured
|
||||
func (c *Config) HasAnyNotifier() bool {
|
||||
return c.Notifiers.Stdout ||
|
||||
c.Notifiers.SMTP != nil ||
|
||||
c.Notifiers.Slack != nil ||
|
||||
c.Notifiers.Ntfy != nil
|
||||
len(c.Notifiers.SMTP) > 0 ||
|
||||
len(c.Notifiers.Slack) > 0 ||
|
||||
len(c.Notifiers.Ntfy) > 0
|
||||
}
|
||||
|
||||
// GetEnabledNotifiers returns a list of enabled notifier types
|
||||
@@ -197,15 +197,52 @@ func (c *Config) GetEnabledNotifiers() []domain.NotificationType {
|
||||
if c.Notifiers.Stdout {
|
||||
enabled = append(enabled, domain.TypeStdout)
|
||||
}
|
||||
if c.Notifiers.SMTP != nil {
|
||||
if len(c.Notifiers.SMTP) > 0 {
|
||||
enabled = append(enabled, domain.TypeEmail)
|
||||
}
|
||||
if c.Notifiers.Slack != nil {
|
||||
if len(c.Notifiers.Slack) > 0 {
|
||||
enabled = append(enabled, domain.TypeSlack)
|
||||
}
|
||||
if c.Notifiers.Ntfy != nil {
|
||||
if len(c.Notifiers.Ntfy) > 0 {
|
||||
enabled = append(enabled, domain.TypeNtfy)
|
||||
}
|
||||
|
||||
return enabled
|
||||
}
|
||||
|
||||
// GetDefaultAccount returns the default account name for a notifier type, or the first account if no default is set
|
||||
func (c *Config) GetDefaultAccount(notifierType domain.NotificationType) string {
|
||||
switch notifierType {
|
||||
case domain.TypeEmail:
|
||||
for name, cfg := range c.Notifiers.SMTP {
|
||||
if cfg.Default {
|
||||
return name
|
||||
}
|
||||
}
|
||||
// Return first account if no default is set
|
||||
for name := range c.Notifiers.SMTP {
|
||||
return name
|
||||
}
|
||||
case domain.TypeSlack:
|
||||
for name, cfg := range c.Notifiers.Slack {
|
||||
if cfg.Default {
|
||||
return name
|
||||
}
|
||||
}
|
||||
// Return first account if no default is set
|
||||
for name := range c.Notifiers.Slack {
|
||||
return name
|
||||
}
|
||||
case domain.TypeNtfy:
|
||||
for name, cfg := range c.Notifiers.Ntfy {
|
||||
if cfg.Default {
|
||||
return name
|
||||
}
|
||||
}
|
||||
// Return first account if no default is set
|
||||
for name := range c.Notifiers.Ntfy {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -44,6 +44,10 @@ type Notification struct {
|
||||
// Type specifies which notifier should handle this notification
|
||||
Type NotificationType `json:"type"`
|
||||
|
||||
// Account specifies which named account/instance to use for this notifier type (optional)
|
||||
// If not specified, the default account for the notifier type will be used
|
||||
Account string `json:"account,omitempty"`
|
||||
|
||||
// Priority determines urgency and retry behavior
|
||||
Priority Priority `json:"priority"`
|
||||
|
||||
|
||||
@@ -21,14 +21,18 @@ type Notifier interface {
|
||||
|
||||
// NotifierFactory creates notifier instances based on configuration
|
||||
type NotifierFactory interface {
|
||||
// Create creates a notifier for the given type
|
||||
Create(notificationType NotificationType) (Notifier, error)
|
||||
// Create creates a notifier for the given type and account
|
||||
// If account is empty, the default account for the type will be used
|
||||
Create(notificationType NotificationType, account string) (Notifier, error)
|
||||
|
||||
// RegisterNotifier registers a custom notifier implementation
|
||||
RegisterNotifier(notificationType NotificationType, notifier Notifier) error
|
||||
RegisterNotifier(notificationType NotificationType, account string, notifier Notifier) error
|
||||
|
||||
// SupportedTypes returns all supported notification types
|
||||
SupportedTypes() []NotificationType
|
||||
|
||||
// GetAccounts returns all registered accounts for a given notification type
|
||||
GetAccounts(notificationType NotificationType) []string
|
||||
}
|
||||
|
||||
// NotificationService is the high-level service interface for managing notifications
|
||||
|
||||
@@ -10,24 +10,38 @@ import (
|
||||
|
||||
// Factory creates and manages notifier instances
|
||||
type Factory struct {
|
||||
notifiers map[domain.NotificationType]domain.Notifier
|
||||
// Map of "type:account" -> notifier instance
|
||||
notifiers map[string]domain.Notifier
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewFactory creates a new notifier factory
|
||||
func NewFactory() *Factory {
|
||||
return &Factory{
|
||||
notifiers: make(map[domain.NotificationType]domain.Notifier),
|
||||
notifiers: make(map[string]domain.Notifier),
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a notifier for the given type
|
||||
func (f *Factory) Create(notificationType domain.NotificationType) (domain.Notifier, error) {
|
||||
// makeKey creates a compound key from notification type and account
|
||||
func makeKey(notificationType domain.NotificationType, account string) string {
|
||||
if account == "" {
|
||||
// For backward compatibility, if account is empty, just use the type
|
||||
return string(notificationType)
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", notificationType, account)
|
||||
}
|
||||
|
||||
// Create creates a notifier for the given type and account
|
||||
func (f *Factory) Create(notificationType domain.NotificationType, account string) (domain.Notifier, error) {
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
|
||||
notifier, exists := f.notifiers[notificationType]
|
||||
key := makeKey(notificationType, account)
|
||||
notifier, exists := f.notifiers[key]
|
||||
if !exists {
|
||||
if account != "" {
|
||||
return nil, fmt.Errorf("unsupported notification type: %s with account: %s", notificationType, account)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported notification type: %s", notificationType)
|
||||
}
|
||||
|
||||
@@ -35,31 +49,65 @@ func (f *Factory) Create(notificationType domain.NotificationType) (domain.Notif
|
||||
}
|
||||
|
||||
// RegisterNotifier registers a custom notifier implementation
|
||||
func (f *Factory) RegisterNotifier(notificationType domain.NotificationType, notifier domain.Notifier) error {
|
||||
func (f *Factory) RegisterNotifier(notificationType domain.NotificationType, account string, notifier domain.Notifier) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if _, exists := f.notifiers[notificationType]; exists {
|
||||
key := makeKey(notificationType, account)
|
||||
if _, exists := f.notifiers[key]; exists {
|
||||
if account != "" {
|
||||
return fmt.Errorf("notifier already registered for type: %s with account: %s", notificationType, account)
|
||||
}
|
||||
return fmt.Errorf("notifier already registered for type: %s", notificationType)
|
||||
}
|
||||
|
||||
f.notifiers[notificationType] = notifier
|
||||
f.notifiers[key] = notifier
|
||||
return nil
|
||||
}
|
||||
|
||||
// SupportedTypes returns all supported notification types
|
||||
// SupportedTypes returns all supported notification types (unique types only)
|
||||
func (f *Factory) SupportedTypes() []domain.NotificationType {
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
|
||||
types := make([]domain.NotificationType, 0, len(f.notifiers))
|
||||
for t := range f.notifiers {
|
||||
typeMap := make(map[domain.NotificationType]bool)
|
||||
for key := range f.notifiers {
|
||||
// Extract the type from the key (type:account)
|
||||
var notifType domain.NotificationType
|
||||
if n, err := fmt.Sscanf(key, "%s:", ¬ifType); err == nil && n > 0 {
|
||||
typeMap[notifType] = true
|
||||
} else {
|
||||
// Backward compatibility: key might just be the type
|
||||
typeMap[domain.NotificationType(key)] = true
|
||||
}
|
||||
}
|
||||
|
||||
types := make([]domain.NotificationType, 0, len(typeMap))
|
||||
for t := range typeMap {
|
||||
types = append(types, t)
|
||||
}
|
||||
|
||||
return types
|
||||
}
|
||||
|
||||
// GetAccounts returns all registered accounts for a given notification type
|
||||
func (f *Factory) GetAccounts(notificationType domain.NotificationType) []string {
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
|
||||
accounts := []string{}
|
||||
prefix := string(notificationType) + ":"
|
||||
|
||||
for key := range f.notifiers {
|
||||
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
|
||||
account := key[len(prefix):]
|
||||
accounts = append(accounts, account)
|
||||
}
|
||||
}
|
||||
|
||||
return accounts
|
||||
}
|
||||
|
||||
// BaseNotifier provides common functionality for all notifiers
|
||||
type BaseNotifier struct {
|
||||
notificationType domain.NotificationType
|
||||
|
||||
@@ -32,6 +32,9 @@ type NtfyConfig struct {
|
||||
|
||||
// InsecureSkipVerify skips TLS verification (for self-hosted servers with self-signed certs)
|
||||
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
|
||||
|
||||
// Default marks this instance as default
|
||||
Default bool `mapstructure:"default"`
|
||||
}
|
||||
|
||||
// NtfyNotifier sends notifications via ntfy.sh
|
||||
|
||||
@@ -19,6 +19,7 @@ type SlackConfig struct {
|
||||
Username string `mapstructure:"username"`
|
||||
IconEmoji string `mapstructure:"icon_emoji"`
|
||||
Webhooks map[string]string `mapstructure:"webhooks"` // Channel-specific webhooks
|
||||
Default bool `mapstructure:"default"` // Mark this instance as default
|
||||
}
|
||||
|
||||
// SlackNotifier sends notifications to Slack
|
||||
|
||||
@@ -18,6 +18,7 @@ type SMTPConfig struct {
|
||||
Password string `mapstructure:"password"`
|
||||
From string `mapstructure:"from"`
|
||||
UseTLS bool `mapstructure:"use_tls"`
|
||||
Default bool `mapstructure:"default"` // Mark this instance as default
|
||||
}
|
||||
|
||||
// SMTPNotifier sends notifications via email using SMTP
|
||||
|
||||
+27
-14
@@ -9,29 +9,36 @@ import (
|
||||
"github.com/igodwin/notifier/internal/domain"
|
||||
)
|
||||
|
||||
// AccountResolver is an interface for resolving default accounts
|
||||
type AccountResolver interface {
|
||||
GetDefaultAccount(notifierType domain.NotificationType) string
|
||||
}
|
||||
|
||||
// NotificationService implements the domain.NotificationService interface
|
||||
type NotificationService struct {
|
||||
factory domain.NotifierFactory
|
||||
queue domain.Queue
|
||||
notifications map[string]*domain.Notification
|
||||
mu sync.RWMutex
|
||||
workerCount int
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
factory domain.NotifierFactory
|
||||
queue domain.Queue
|
||||
accountResolver AccountResolver
|
||||
notifications map[string]*domain.Notification
|
||||
mu sync.RWMutex
|
||||
workerCount int
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewNotificationService creates a new notification service
|
||||
func NewNotificationService(factory domain.NotifierFactory, queue domain.Queue, workerCount int) *NotificationService {
|
||||
func NewNotificationService(factory domain.NotifierFactory, queue domain.Queue, workerCount int, accountResolver AccountResolver) *NotificationService {
|
||||
if workerCount <= 0 {
|
||||
workerCount = 10
|
||||
}
|
||||
|
||||
return &NotificationService{
|
||||
factory: factory,
|
||||
queue: queue,
|
||||
notifications: make(map[string]*domain.Notification),
|
||||
workerCount: workerCount,
|
||||
stopChan: make(chan struct{}),
|
||||
factory: factory,
|
||||
queue: queue,
|
||||
accountResolver: accountResolver,
|
||||
notifications: make(map[string]*domain.Notification),
|
||||
workerCount: workerCount,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,8 +97,14 @@ func (s *NotificationService) worker(ctx context.Context, id int) {
|
||||
func (s *NotificationService) processNotification(ctx context.Context, msg *domain.QueueMessage) {
|
||||
notification := msg.Notification
|
||||
|
||||
// Resolve account if not specified
|
||||
account := notification.Account
|
||||
if account == "" && s.accountResolver != nil {
|
||||
account = s.accountResolver.GetDefaultAccount(notification.Type)
|
||||
}
|
||||
|
||||
// Get the appropriate notifier
|
||||
notifier, err := s.factory.Create(notification.Type)
|
||||
notifier, err := s.factory.Create(notification.Type, account)
|
||||
if err != nil {
|
||||
notification.Status = domain.StatusFailed
|
||||
notification.LastError = fmt.Sprintf("failed to create notifier: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user