Add support for multiple instances of the same notifier type
This commit is contained in:
+21
-19
@@ -66,18 +66,19 @@ enum NotificationStatus {
|
||||
message Notification {
|
||||
string id = 1;
|
||||
NotificationType type = 2;
|
||||
Priority priority = 3;
|
||||
NotificationStatus status = 4;
|
||||
string subject = 5;
|
||||
string body = 6;
|
||||
repeated string recipients = 7;
|
||||
map<string, string> metadata = 8;
|
||||
google.protobuf.Timestamp created_at = 9;
|
||||
google.protobuf.Timestamp scheduled_for = 10;
|
||||
google.protobuf.Timestamp sent_at = 11;
|
||||
int32 retry_count = 12;
|
||||
int32 max_retries = 13;
|
||||
string last_error = 14;
|
||||
string account = 3; // Optional account name for multi-account configs
|
||||
Priority priority = 4;
|
||||
NotificationStatus status = 5;
|
||||
string subject = 6;
|
||||
string body = 7;
|
||||
repeated string recipients = 8;
|
||||
map<string, string> metadata = 9;
|
||||
google.protobuf.Timestamp created_at = 10;
|
||||
google.protobuf.Timestamp scheduled_for = 11;
|
||||
google.protobuf.Timestamp sent_at = 12;
|
||||
int32 retry_count = 13;
|
||||
int32 max_retries = 14;
|
||||
string last_error = 15;
|
||||
}
|
||||
|
||||
// NotificationResult represents the outcome of sending a notification
|
||||
@@ -93,13 +94,14 @@ message NotificationResult {
|
||||
// SendNotificationRequest sends a single notification
|
||||
message SendNotificationRequest {
|
||||
NotificationType type = 1;
|
||||
Priority priority = 2;
|
||||
string subject = 3;
|
||||
string body = 4;
|
||||
repeated string recipients = 5;
|
||||
map<string, string> metadata = 6;
|
||||
google.protobuf.Timestamp scheduled_for = 7;
|
||||
int32 max_retries = 8;
|
||||
string account = 2; // Optional account name for multi-account configs
|
||||
Priority priority = 3;
|
||||
string subject = 4;
|
||||
string body = 5;
|
||||
repeated string recipients = 6;
|
||||
map<string, string> metadata = 7;
|
||||
google.protobuf.Timestamp scheduled_for = 8;
|
||||
int32 max_retries = 9;
|
||||
}
|
||||
|
||||
// SendNotificationResponse returns the result of sending a notification
|
||||
|
||||
@@ -201,18 +201,19 @@ type Notification struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Type NotificationType `protobuf:"varint,2,opt,name=type,proto3,enum=notifier.v1.NotificationType" json:"type,omitempty"`
|
||||
Priority Priority `protobuf:"varint,3,opt,name=priority,proto3,enum=notifier.v1.Priority" json:"priority,omitempty"`
|
||||
Status NotificationStatus `protobuf:"varint,4,opt,name=status,proto3,enum=notifier.v1.NotificationStatus" json:"status,omitempty"`
|
||||
Subject string `protobuf:"bytes,5,opt,name=subject,proto3" json:"subject,omitempty"`
|
||||
Body string `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
|
||||
Recipients []string `protobuf:"bytes,7,rep,name=recipients,proto3" json:"recipients,omitempty"`
|
||||
Metadata map[string]string `protobuf:"bytes,8,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
ScheduledFor *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=scheduled_for,json=scheduledFor,proto3" json:"scheduled_for,omitempty"`
|
||||
SentAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=sent_at,json=sentAt,proto3" json:"sent_at,omitempty"`
|
||||
RetryCount int32 `protobuf:"varint,12,opt,name=retry_count,json=retryCount,proto3" json:"retry_count,omitempty"`
|
||||
MaxRetries int32 `protobuf:"varint,13,opt,name=max_retries,json=maxRetries,proto3" json:"max_retries,omitempty"`
|
||||
LastError string `protobuf:"bytes,14,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"`
|
||||
Account string `protobuf:"bytes,3,opt,name=account,proto3" json:"account,omitempty"` // Optional account name for multi-account configs
|
||||
Priority Priority `protobuf:"varint,4,opt,name=priority,proto3,enum=notifier.v1.Priority" json:"priority,omitempty"`
|
||||
Status NotificationStatus `protobuf:"varint,5,opt,name=status,proto3,enum=notifier.v1.NotificationStatus" json:"status,omitempty"`
|
||||
Subject string `protobuf:"bytes,6,opt,name=subject,proto3" json:"subject,omitempty"`
|
||||
Body string `protobuf:"bytes,7,opt,name=body,proto3" json:"body,omitempty"`
|
||||
Recipients []string `protobuf:"bytes,8,rep,name=recipients,proto3" json:"recipients,omitempty"`
|
||||
Metadata map[string]string `protobuf:"bytes,9,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
ScheduledFor *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=scheduled_for,json=scheduledFor,proto3" json:"scheduled_for,omitempty"`
|
||||
SentAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=sent_at,json=sentAt,proto3" json:"sent_at,omitempty"`
|
||||
RetryCount int32 `protobuf:"varint,13,opt,name=retry_count,json=retryCount,proto3" json:"retry_count,omitempty"`
|
||||
MaxRetries int32 `protobuf:"varint,14,opt,name=max_retries,json=maxRetries,proto3" json:"max_retries,omitempty"`
|
||||
LastError string `protobuf:"bytes,15,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -261,6 +262,13 @@ func (x *Notification) GetType() NotificationType {
|
||||
return NotificationType_NOTIFICATION_TYPE_UNSPECIFIED
|
||||
}
|
||||
|
||||
func (x *Notification) GetAccount() string {
|
||||
if x != nil {
|
||||
return x.Account
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Notification) GetPriority() Priority {
|
||||
if x != nil {
|
||||
return x.Priority
|
||||
@@ -434,13 +442,14 @@ func (x *NotificationResult) GetProviderResponse() map[string]string {
|
||||
type SendNotificationRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type NotificationType `protobuf:"varint,1,opt,name=type,proto3,enum=notifier.v1.NotificationType" json:"type,omitempty"`
|
||||
Priority Priority `protobuf:"varint,2,opt,name=priority,proto3,enum=notifier.v1.Priority" json:"priority,omitempty"`
|
||||
Subject string `protobuf:"bytes,3,opt,name=subject,proto3" json:"subject,omitempty"`
|
||||
Body string `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"`
|
||||
Recipients []string `protobuf:"bytes,5,rep,name=recipients,proto3" json:"recipients,omitempty"`
|
||||
Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
ScheduledFor *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=scheduled_for,json=scheduledFor,proto3" json:"scheduled_for,omitempty"`
|
||||
MaxRetries int32 `protobuf:"varint,8,opt,name=max_retries,json=maxRetries,proto3" json:"max_retries,omitempty"`
|
||||
Account string `protobuf:"bytes,2,opt,name=account,proto3" json:"account,omitempty"` // Optional account name for multi-account configs
|
||||
Priority Priority `protobuf:"varint,3,opt,name=priority,proto3,enum=notifier.v1.Priority" json:"priority,omitempty"`
|
||||
Subject string `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"`
|
||||
Body string `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
|
||||
Recipients []string `protobuf:"bytes,6,rep,name=recipients,proto3" json:"recipients,omitempty"`
|
||||
Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
ScheduledFor *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=scheduled_for,json=scheduledFor,proto3" json:"scheduled_for,omitempty"`
|
||||
MaxRetries int32 `protobuf:"varint,9,opt,name=max_retries,json=maxRetries,proto3" json:"max_retries,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -482,6 +491,13 @@ func (x *SendNotificationRequest) GetType() NotificationType {
|
||||
return NotificationType_NOTIFICATION_TYPE_UNSPECIFIED
|
||||
}
|
||||
|
||||
func (x *SendNotificationRequest) GetAccount() string {
|
||||
if x != nil {
|
||||
return x.Account
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SendNotificationRequest) GetPriority() Priority {
|
||||
if x != nil {
|
||||
return x.Priority
|
||||
@@ -1375,29 +1391,30 @@ var File_api_grpc_notifier_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_api_grpc_notifier_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x17api/grpc/notifier.proto\x12\vnotifier.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x9f\x05\n" +
|
||||
"\x17api/grpc/notifier.proto\x12\vnotifier.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb9\x05\n" +
|
||||
"\fNotification\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x121\n" +
|
||||
"\x04type\x18\x02 \x01(\x0e2\x1d.notifier.v1.NotificationTypeR\x04type\x121\n" +
|
||||
"\bpriority\x18\x03 \x01(\x0e2\x15.notifier.v1.PriorityR\bpriority\x127\n" +
|
||||
"\x06status\x18\x04 \x01(\x0e2\x1f.notifier.v1.NotificationStatusR\x06status\x12\x18\n" +
|
||||
"\asubject\x18\x05 \x01(\tR\asubject\x12\x12\n" +
|
||||
"\x04body\x18\x06 \x01(\tR\x04body\x12\x1e\n" +
|
||||
"\x04type\x18\x02 \x01(\x0e2\x1d.notifier.v1.NotificationTypeR\x04type\x12\x18\n" +
|
||||
"\aaccount\x18\x03 \x01(\tR\aaccount\x121\n" +
|
||||
"\bpriority\x18\x04 \x01(\x0e2\x15.notifier.v1.PriorityR\bpriority\x127\n" +
|
||||
"\x06status\x18\x05 \x01(\x0e2\x1f.notifier.v1.NotificationStatusR\x06status\x12\x18\n" +
|
||||
"\asubject\x18\x06 \x01(\tR\asubject\x12\x12\n" +
|
||||
"\x04body\x18\a \x01(\tR\x04body\x12\x1e\n" +
|
||||
"\n" +
|
||||
"recipients\x18\a \x03(\tR\n" +
|
||||
"recipients\x18\b \x03(\tR\n" +
|
||||
"recipients\x12C\n" +
|
||||
"\bmetadata\x18\b \x03(\v2'.notifier.v1.Notification.MetadataEntryR\bmetadata\x129\n" +
|
||||
"\bmetadata\x18\t \x03(\v2'.notifier.v1.Notification.MetadataEntryR\bmetadata\x129\n" +
|
||||
"\n" +
|
||||
"created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12?\n" +
|
||||
"\rscheduled_for\x18\n" +
|
||||
" \x01(\v2\x1a.google.protobuf.TimestampR\fscheduledFor\x123\n" +
|
||||
"\asent_at\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\x06sentAt\x12\x1f\n" +
|
||||
"\vretry_count\x18\f \x01(\x05R\n" +
|
||||
"created_at\x18\n" +
|
||||
" \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12?\n" +
|
||||
"\rscheduled_for\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\fscheduledFor\x123\n" +
|
||||
"\asent_at\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\x06sentAt\x12\x1f\n" +
|
||||
"\vretry_count\x18\r \x01(\x05R\n" +
|
||||
"retryCount\x12\x1f\n" +
|
||||
"\vmax_retries\x18\r \x01(\x05R\n" +
|
||||
"\vmax_retries\x18\x0e \x01(\x05R\n" +
|
||||
"maxRetries\x12\x1d\n" +
|
||||
"\n" +
|
||||
"last_error\x18\x0e \x01(\tR\tlastError\x1a;\n" +
|
||||
"last_error\x18\x0f \x01(\tR\tlastError\x1a;\n" +
|
||||
"\rMetadataEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe5\x02\n" +
|
||||
@@ -1410,18 +1427,19 @@ const file_api_grpc_notifier_proto_rawDesc = "" +
|
||||
"\x11provider_response\x18\x06 \x03(\v25.notifier.v1.NotificationResult.ProviderResponseEntryR\x10providerResponse\x1aC\n" +
|
||||
"\x15ProviderResponseEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbc\x03\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd6\x03\n" +
|
||||
"\x17SendNotificationRequest\x121\n" +
|
||||
"\x04type\x18\x01 \x01(\x0e2\x1d.notifier.v1.NotificationTypeR\x04type\x121\n" +
|
||||
"\bpriority\x18\x02 \x01(\x0e2\x15.notifier.v1.PriorityR\bpriority\x12\x18\n" +
|
||||
"\asubject\x18\x03 \x01(\tR\asubject\x12\x12\n" +
|
||||
"\x04body\x18\x04 \x01(\tR\x04body\x12\x1e\n" +
|
||||
"\x04type\x18\x01 \x01(\x0e2\x1d.notifier.v1.NotificationTypeR\x04type\x12\x18\n" +
|
||||
"\aaccount\x18\x02 \x01(\tR\aaccount\x121\n" +
|
||||
"\bpriority\x18\x03 \x01(\x0e2\x15.notifier.v1.PriorityR\bpriority\x12\x18\n" +
|
||||
"\asubject\x18\x04 \x01(\tR\asubject\x12\x12\n" +
|
||||
"\x04body\x18\x05 \x01(\tR\x04body\x12\x1e\n" +
|
||||
"\n" +
|
||||
"recipients\x18\x05 \x03(\tR\n" +
|
||||
"recipients\x18\x06 \x03(\tR\n" +
|
||||
"recipients\x12N\n" +
|
||||
"\bmetadata\x18\x06 \x03(\v22.notifier.v1.SendNotificationRequest.MetadataEntryR\bmetadata\x12?\n" +
|
||||
"\rscheduled_for\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\fscheduledFor\x12\x1f\n" +
|
||||
"\vmax_retries\x18\b \x01(\x05R\n" +
|
||||
"\bmetadata\x18\a \x03(\v22.notifier.v1.SendNotificationRequest.MetadataEntryR\bmetadata\x12?\n" +
|
||||
"\rscheduled_for\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\fscheduledFor\x12\x1f\n" +
|
||||
"\vmax_retries\x18\t \x01(\x05R\n" +
|
||||
"maxRetries\x1a;\n" +
|
||||
"\rMetadataEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
// SendNotificationRequest is the REST API request for sending a notification
|
||||
type SendNotificationRequest struct {
|
||||
Type string `json:"type"`
|
||||
Account string `json:"account,omitempty"` // Optional account name for multi-account configs
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
@@ -47,6 +48,7 @@ func (r *SendNotificationRequest) ToNotification() *domain.Notification {
|
||||
return &domain.Notification{
|
||||
ID: uuid.New().String(),
|
||||
Type: domain.NotificationType(r.Type),
|
||||
Account: r.Account,
|
||||
Priority: domain.Priority(r.Priority),
|
||||
Status: domain.StatusPending,
|
||||
Subject: r.Subject,
|
||||
@@ -79,6 +81,7 @@ type SendBatchNotificationsResponse struct {
|
||||
type Notification struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Account string `json:"account,omitempty"`
|
||||
Priority int `json:"priority"`
|
||||
Status string `json:"status"`
|
||||
Subject string `json:"subject"`
|
||||
@@ -98,6 +101,7 @@ func NotificationFromDomain(n *domain.Notification) Notification {
|
||||
return Notification{
|
||||
ID: n.ID,
|
||||
Type: string(n.Type),
|
||||
Account: n.Account,
|
||||
Priority: int(n.Priority),
|
||||
Status: string(n.Status),
|
||||
Subject: n.Subject,
|
||||
|
||||
+36
-21
@@ -69,8 +69,8 @@ func main() {
|
||||
|
||||
logger.Infof("Supported notification types: %v", factory.SupportedTypes())
|
||||
|
||||
// Create notification service
|
||||
svc := service.NewNotificationService(factory, q, cfg.Queue.WorkerCount)
|
||||
// Create notification service (pass config as account resolver)
|
||||
svc := service.NewNotificationService(factory, q, cfg.Queue.WorkerCount, cfg)
|
||||
|
||||
// Start workers
|
||||
if err := svc.Start(ctx); err != nil {
|
||||
@@ -132,45 +132,60 @@ func main() {
|
||||
func registerNotifiers(cfg *config.Config, factory *notifier.Factory, logger *logging.Logger) {
|
||||
if cfg.Notifiers.Stdout {
|
||||
stdoutNotifier := notifier.NewStdoutNotifier()
|
||||
if err := factory.RegisterNotifier(domain.TypeStdout, stdoutNotifier); err != nil {
|
||||
if err := factory.RegisterNotifier(domain.TypeStdout, "", stdoutNotifier); err != nil {
|
||||
logger.Fatalf("Failed to register stdout notifier: %v", err)
|
||||
}
|
||||
logger.Info("Registered stdout notifier")
|
||||
}
|
||||
|
||||
if cfg.Notifiers.SMTP != nil {
|
||||
smtpNotifier, err := notifier.NewSMTPNotifier(cfg.Notifiers.SMTP)
|
||||
// Register SMTP notifiers (now supports multiple accounts)
|
||||
for accountName, smtpConfig := range cfg.Notifiers.SMTP {
|
||||
smtpNotifier, err := notifier.NewSMTPNotifier(smtpConfig)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to create SMTP notifier: %v", err)
|
||||
logger.Warnf("Failed to create SMTP notifier for account '%s': %v", accountName, err)
|
||||
} else {
|
||||
if err := factory.RegisterNotifier(domain.TypeEmail, smtpNotifier); err != nil {
|
||||
logger.Fatalf("Failed to register SMTP notifier: %v", err)
|
||||
if err := factory.RegisterNotifier(domain.TypeEmail, accountName, smtpNotifier); err != nil {
|
||||
logger.Fatalf("Failed to register SMTP notifier for account '%s': %v", accountName, err)
|
||||
}
|
||||
logger.Info("Registered SMTP notifier")
|
||||
defaultStr := ""
|
||||
if smtpConfig.Default {
|
||||
defaultStr = " (default)"
|
||||
}
|
||||
logger.Infof("Registered SMTP notifier for account '%s'%s", accountName, defaultStr)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Notifiers.Slack != nil {
|
||||
slackNotifier, err := notifier.NewSlackNotifier(cfg.Notifiers.Slack)
|
||||
// Register Slack notifiers (now supports multiple accounts)
|
||||
for accountName, slackConfig := range cfg.Notifiers.Slack {
|
||||
slackNotifier, err := notifier.NewSlackNotifier(slackConfig)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to create Slack notifier: %v", err)
|
||||
logger.Warnf("Failed to create Slack notifier for account '%s': %v", accountName, err)
|
||||
} else {
|
||||
if err := factory.RegisterNotifier(domain.TypeSlack, slackNotifier); err != nil {
|
||||
logger.Fatalf("Failed to register Slack notifier: %v", err)
|
||||
if err := factory.RegisterNotifier(domain.TypeSlack, accountName, slackNotifier); err != nil {
|
||||
logger.Fatalf("Failed to register Slack notifier for account '%s': %v", accountName, err)
|
||||
}
|
||||
logger.Info("Registered Slack notifier")
|
||||
defaultStr := ""
|
||||
if slackConfig.Default {
|
||||
defaultStr = " (default)"
|
||||
}
|
||||
logger.Infof("Registered Slack notifier for account '%s'%s", accountName, defaultStr)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Notifiers.Ntfy != nil {
|
||||
ntfyNotifier, err := notifier.NewNtfyNotifier(cfg.Notifiers.Ntfy)
|
||||
// Register Ntfy notifiers (now supports multiple accounts)
|
||||
for accountName, ntfyConfig := range cfg.Notifiers.Ntfy {
|
||||
ntfyNotifier, err := notifier.NewNtfyNotifier(ntfyConfig)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to create Ntfy notifier: %v", err)
|
||||
logger.Warnf("Failed to create Ntfy notifier for account '%s': %v", accountName, err)
|
||||
} else {
|
||||
if err := factory.RegisterNotifier(domain.TypeNtfy, ntfyNotifier); err != nil {
|
||||
logger.Fatalf("Failed to register Ntfy notifier: %v", err)
|
||||
if err := factory.RegisterNotifier(domain.TypeNtfy, accountName, ntfyNotifier); err != nil {
|
||||
logger.Fatalf("Failed to register Ntfy notifier for account '%s': %v", accountName, err)
|
||||
}
|
||||
logger.Info("Registered Ntfy notifier")
|
||||
defaultStr := ""
|
||||
if ntfyConfig.Default {
|
||||
defaultStr = " (default)"
|
||||
}
|
||||
logger.Infof("Registered Ntfy notifier for account '%s'%s", accountName, defaultStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-32
@@ -34,44 +34,63 @@ notifiers:
|
||||
# Enable stdout notifier (useful for development/debugging)
|
||||
stdout: true
|
||||
|
||||
# SMTP email configuration
|
||||
# SMTP email configuration (supports multiple accounts)
|
||||
smtp:
|
||||
host: "smtp.gmail.com"
|
||||
port: 587
|
||||
username: "your-email@gmail.com"
|
||||
password: "your-app-password"
|
||||
from: "notifications@yourservice.com"
|
||||
use_tls: true
|
||||
# Personal email account (marked as default)
|
||||
personal:
|
||||
host: "smtp.gmail.com"
|
||||
port: 587
|
||||
username: "your-personal@gmail.com"
|
||||
password: "your-app-password"
|
||||
from: "your-personal@gmail.com"
|
||||
use_tls: true
|
||||
default: true # This account will be used if no account is specified in the API request
|
||||
|
||||
# Slack configuration
|
||||
# Work email account
|
||||
# work:
|
||||
# host: "smtp.company.com"
|
||||
# port: 587
|
||||
# username: "you@company.com"
|
||||
# password: "your-work-password"
|
||||
# from: "notifications@company.com"
|
||||
# use_tls: true
|
||||
# default: false
|
||||
|
||||
# Slack configuration (supports multiple workspaces/webhooks)
|
||||
slack:
|
||||
webhook_url: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
|
||||
# token: "xoxb-your-bot-token" # Alternative to webhook
|
||||
# channel: "#notifications" # Default channel
|
||||
username: "Notifier Bot"
|
||||
icon_emoji: ":bell:"
|
||||
# Channel-specific webhooks
|
||||
# webhooks:
|
||||
# "#alerts": "https://hooks.slack.com/services/ALERTS/WEBHOOK"
|
||||
# "#monitoring": "https://hooks.slack.com/services/MONITORING/WEBHOOK"
|
||||
# Main workspace (marked as default)
|
||||
main:
|
||||
webhook_url: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
|
||||
username: "Notifier Bot"
|
||||
icon_emoji: ":bell:"
|
||||
default: true # This workspace will be used if no account is specified
|
||||
|
||||
# Ntfy configuration
|
||||
# Additional workspace example
|
||||
# team-a:
|
||||
# webhook_url: "https://hooks.slack.com/services/TEAM-A/WEBHOOK/URL"
|
||||
# username: "Team A Bot"
|
||||
# icon_emoji: ":rocket:"
|
||||
# # Channel-specific webhooks within this workspace
|
||||
# webhooks:
|
||||
# "#alerts": "https://hooks.slack.com/services/ALERTS/WEBHOOK"
|
||||
# "#monitoring": "https://hooks.slack.com/services/MONITORING/WEBHOOK"
|
||||
|
||||
# Ntfy configuration (supports multiple servers)
|
||||
ntfy:
|
||||
server_url: "https://ntfy.sh"
|
||||
# Public ntfy.sh server (marked as default)
|
||||
public:
|
||||
server_url: "https://ntfy.sh"
|
||||
# token: "tk_your_access_token" # Token-based auth (recommended)
|
||||
# default_topic: "notifications"
|
||||
default: true # This server will be used if no account is specified
|
||||
|
||||
# Token-based authentication (recommended for ntfy.sh)
|
||||
# Supports both access tokens (tk_...) and publish tokens
|
||||
# token: "tk_your_access_token"
|
||||
|
||||
# Basic authentication (alternative to token)
|
||||
# username: "your-username"
|
||||
# password: "your-password"
|
||||
|
||||
# Default topic if recipient not specified
|
||||
# default_topic: "default-notifications"
|
||||
|
||||
# Skip TLS verification (for self-hosted servers with self-signed certs)
|
||||
# insecure_skip_verify: false
|
||||
# Private ntfy server example
|
||||
# private:
|
||||
# server_url: "https://ntfy.mycompany.com"
|
||||
# username: "your-username"
|
||||
# password: "your-password"
|
||||
# default_topic: "company-notifications"
|
||||
# insecure_skip_verify: false # Set to true for self-signed certs
|
||||
|
||||
logging:
|
||||
level: "info" # Options: debug, info, warn, error
|
||||
|
||||
+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