diff --git a/config/validate.go b/config/validate.go index 5fcff58..ac865e9 100644 --- a/config/validate.go +++ b/config/validate.go @@ -70,51 +70,64 @@ func (c *Config) Validate() []string { } // Validate Discord - if c.Alerts.Discord.Enabled { - if results := c.validateDiscord(); len(results) > 0 { - validationErrors = append(validationErrors, results...) + for id, profile := range c.Alerts.Discord { + if profile.Enabled { + if results := c.validateDiscord(id); len(results) > 0 { + validationErrors = append(validationErrors, results...) + } } } // Validate Gotify - if c.Alerts.Gotify.Enabled { - if results := c.validateGotify(); len(results) > 0 { - validationErrors = append(validationErrors, results...) + for id, profile := range c.Alerts.Gotify { + if profile.Enabled { + if results := c.validateGotify(id); len(results) > 0 { + validationErrors = append(validationErrors, results...) + } } } // Validate SMTP - if c.Alerts.SMTP.Enabled { - if results := c.validateSMTP(); len(results) > 0 { - validationErrors = append(validationErrors, results...) + for id, profile := range c.Alerts.SMTP { + if profile.Enabled { + if results := c.validateSMTP(id); len(results) > 0 { + validationErrors = append(validationErrors, results...) + } } } - // Validate Telegram - if c.Alerts.Telegram.Enabled { - if results := c.validateTelegram(); len(results) > 0 { - validationErrors = append(validationErrors, results...) + for id, profile := range c.Alerts.Telegram { + if profile.Enabled { + if results := c.validateTelegram(id); len(results) > 0 { + validationErrors = append(validationErrors, results...) + } } } // Validate Pushover - if c.Alerts.Pushover.Enabled { - if results := c.validatePushover(); len(results) > 0 { - validationErrors = append(validationErrors, results...) + for id, profile := range c.Alerts.Pushover { + if profile.Enabled { + if results := c.validatePushover(id); len(results) > 0 { + validationErrors = append(validationErrors, results...) + } } } // Validate Ntfy - if c.Alerts.Ntfy.Enabled { - if results := c.validateNtfy(); len(results) > 0 { - validationErrors = append(validationErrors, results...) + for id, profile := range c.Alerts.Ntfy { + if profile.Enabled { + if results := c.validateNtfy(id); len(results) > 0 { + validationErrors = append(validationErrors, results...) + } } } // Validate Webhook - if c.Alerts.Webhook.Enabled { - if results := c.validateWebhook(); len(results) > 0 { - validationErrors = append(validationErrors, results...) + for id, profile := range c.Alerts.Webhook { + if profile.Enabled { + if results := c.validateWebhook(id); len(results) > 0 { + validationErrors = append(validationErrors, results...) + } } } @@ -410,160 +423,160 @@ func (c *Config) validateLabelFiltering() []string { return labelErrors } -func (c *Config) validateDiscord() []string { +func (c *Config) validateDiscord(id int) []string { var discordErrors []string - log.Debug().Msg("Discord alerting enabled.") - discordStatus := models.NotifierStatus{Enabled: true, Status: "configured, not used yet"} + log.Debug().Msgf("Alerting enabled for Discord profile ID %v", id) + discordStatus := models.NotifierStatus{ID: id, Enabled: true, Status: "configured, not used yet"} Internal.Status.Notifications.Discord = append(Internal.Status.Notifications.Discord, discordStatus) - if c.Alerts.Discord.Webhook == "" { - discordErrors = append(discordErrors, "No Discord webhook specified!") + if c.Alerts.Discord[id].Webhook == "" { + discordErrors = append(discordErrors, fmt.Sprintf("No Discord webhook specified! Profile ID %v", id)) } // Check template syntax - if msg := validateTemplate("Discord", c.Alerts.Discord.Template); msg != "" { - discordErrors = append(discordErrors, msg) + if msg := validateTemplate("Discord", c.Alerts.Discord[id].Template); msg != "" { + discordErrors = append(discordErrors, msg+fmt.Sprintf(" Profile ID %v", id)) } return discordErrors } -func (c *Config) validateGotify() []string { +func (c *Config) validateGotify(id int) []string { var gotifyErrors []string - log.Debug().Msg("Gotify alerting enabled.") - gotifyStatus := models.NotifierStatus{Enabled: true, Status: "configured, not used yet"} + log.Debug().Msgf("Alerting enabled for Gotify profile ID %v", id) + gotifyStatus := models.NotifierStatus{ID: id, Enabled: true, Status: "configured, not used yet"} Internal.Status.Notifications.Gotify = append(Internal.Status.Notifications.Gotify, gotifyStatus) - if c.Alerts.Gotify.Server == "" { - gotifyErrors = append(gotifyErrors, "No Gotify server specified!") + if c.Alerts.Gotify[id].Server == "" { + gotifyErrors = append(gotifyErrors, fmt.Sprintf("No Gotify server specified! Profile ID %v", id)) } - if c.Alerts.Gotify.Token == "" { - gotifyErrors = append(gotifyErrors, "No Gotify token specified!") + if c.Alerts.Gotify[id].Token == "" { + gotifyErrors = append(gotifyErrors, fmt.Sprintf("No Gotify token specified! Profile ID %v", id)) } // Check if Gotify server URL contains protocol, assume HTTP if not specified - if !strings.Contains(c.Alerts.Gotify.Server, "http://") && !strings.Contains(c.Alerts.Gotify.Server, "https://") { - log.Debug().Msg("No protocol specified on Gotify Server. Assuming http://. If this is incorrect, please adjust the config file.") - c.Alerts.Gotify.Server = fmt.Sprintf("http://%s", c.Alerts.Gotify.Server) + if !strings.Contains(c.Alerts.Gotify[id].Server, "http://") && !strings.Contains(c.Alerts.Gotify[id].Server, "https://") { + log.Debug().Msgf("No protocol specified on Gotify Server. Assuming http://. If this is incorrect, please adjust the config file. Profile ID %v", id) + c.Alerts.Gotify[id].Server = fmt.Sprintf("http://%s", c.Alerts.Gotify[id].Server) } // Check template syntax - if msg := validateTemplate("Gotify", c.Alerts.Gotify.Template); msg != "" { - gotifyErrors = append(gotifyErrors, msg) + if msg := validateTemplate("Gotify", c.Alerts.Gotify[id].Template); msg != "" { + gotifyErrors = append(gotifyErrors, msg+fmt.Sprintf(" Profile ID %v", id)) } return gotifyErrors } -func (c *Config) validateSMTP() []string { +func (c *Config) validateSMTP(id int) []string { var smtpErrors []string - log.Debug().Msg("SMTP alerting enabled.") - smtpStatus := models.NotifierStatus{Enabled: true, Status: "configured, not used yet"} + log.Debug().Msgf("Alerting enabled for SMTP profile ID %v", id) + smtpStatus := models.NotifierStatus{ID: id, Enabled: true, Status: "configured, not used yet"} Internal.Status.Notifications.SMTP = append(Internal.Status.Notifications.SMTP, smtpStatus) - if c.Alerts.SMTP.Server == "" { - smtpErrors = append(smtpErrors, "No SMTP server specified!") + if c.Alerts.SMTP[id].Server == "" { + smtpErrors = append(smtpErrors, fmt.Sprintf("No SMTP server specified! Profile ID %v", id)) } - if c.Alerts.SMTP.Recipient == "" { - smtpErrors = append(smtpErrors, "No SMTP recipients specified!") + if c.Alerts.SMTP[id].Recipient == "" { + smtpErrors = append(smtpErrors, fmt.Sprintf("No SMTP recipients specified! Profile ID %v", id)) } - if c.Alerts.SMTP.User != "" && c.Alerts.SMTP.Password == "" { - smtpErrors = append(smtpErrors, "SMTP username in config, but no password provided!") + if c.Alerts.SMTP[id].User != "" && c.Alerts.SMTP[id].Password == "" { + smtpErrors = append(smtpErrors, fmt.Sprintf("SMTP username in config, but no password provided! Profile ID %v", id)) } - if c.Alerts.SMTP.Port == 0 { - c.Alerts.SMTP.Port = 25 + if c.Alerts.SMTP[id].Port == 0 { + c.Alerts.SMTP[id].Port = 25 } // Copy `user` to `from` if `from` not explicitly configured - if c.Alerts.SMTP.From == "" && c.Alerts.SMTP.User != "" { - c.Alerts.SMTP.From = c.Alerts.SMTP.User + if c.Alerts.SMTP[id].From == "" && c.Alerts.SMTP[id].User != "" { + c.Alerts.SMTP[id].From = c.Alerts.SMTP[id].User } // Check template syntax - if msg := validateTemplate("SMTP", c.Alerts.SMTP.Template); msg != "" { - smtpErrors = append(smtpErrors, msg) + if msg := validateTemplate("SMTP", c.Alerts.SMTP[id].Template); msg != "" { + smtpErrors = append(smtpErrors, msg+fmt.Sprintf(" Profile ID %v", id)) } return smtpErrors } -func (c *Config) validateTelegram() []string { +func (c *Config) validateTelegram(id int) []string { var telegramErrors []string - log.Debug().Msg("Telegram alerting enabled.") - telegramStatus := models.NotifierStatus{Enabled: true, Status: "configured, not used yet"} + log.Debug().Msgf("Alerting enabled for Telegram profile ID %v", id) + telegramStatus := models.NotifierStatus{ID: id, Enabled: true, Status: "configured, not used yet"} Internal.Status.Notifications.Telegram = append(Internal.Status.Notifications.Telegram, telegramStatus) - if c.Alerts.Telegram.ChatID == 0 { - telegramErrors = append(telegramErrors, "No Telegram Chat ID specified!") + if c.Alerts.Telegram[id].ChatID == 0 { + telegramErrors = append(telegramErrors, fmt.Sprintf("No Telegram Chat ID specified! Profile ID %v", id)) } - if c.Alerts.Telegram.Token == "" { - telegramErrors = append(telegramErrors, "No Telegram bot token specified!") + if c.Alerts.Telegram[id].Token == "" { + telegramErrors = append(telegramErrors, fmt.Sprintf("No Telegram bot token specified! Profile ID %v", id)) } // Check template syntax - if msg := validateTemplate("Telegram", c.Alerts.Telegram.Template); msg != "" { - telegramErrors = append(telegramErrors, msg) + if msg := validateTemplate("Telegram", c.Alerts.Telegram[id].Template); msg != "" { + telegramErrors = append(telegramErrors, msg+fmt.Sprintf(" Profile ID %v", id)) } return telegramErrors } -func (c *Config) validatePushover() []string { +func (c *Config) validatePushover(id int) []string { var pushoverErrors []string - log.Debug().Msg("Pushover alerting enabled.") - pushoverStatus := models.NotifierStatus{Enabled: true, Status: "configured, not used yet"} + log.Debug().Msgf("Alerting enabled for Pushover profile ID %v", id) + pushoverStatus := models.NotifierStatus{ID: id, Enabled: true, Status: "configured, not used yet"} Internal.Status.Notifications.Pushover = append(Internal.Status.Notifications.Pushover, pushoverStatus) - if c.Alerts.Pushover.Token == "" { - pushoverErrors = append(pushoverErrors, "No Pushover API token specified!") + if c.Alerts.Pushover[id].Token == "" { + pushoverErrors = append(pushoverErrors, fmt.Sprintf("No Pushover API token specified! Profile ID %v", id)) } - if c.Alerts.Pushover.Userkey == "" { - pushoverErrors = append(pushoverErrors, "No Pushover user key specified!") + if c.Alerts.Pushover[id].Userkey == "" { + pushoverErrors = append(pushoverErrors, fmt.Sprintf("No Pushover user key specified! Profile ID %v", id)) } - if c.Alerts.Pushover.Priority < -2 || c.Alerts.Pushover.Priority > 2 { - pushoverErrors = append(pushoverErrors, "Pushover priority must be between -2 and 2!") + if c.Alerts.Pushover[id].Priority < -2 || c.Alerts.Pushover[id].Priority > 2 { + pushoverErrors = append(pushoverErrors, fmt.Sprintf("Pushover priority must be between -2 and 2! Profile ID %v", id)) } // Priority 2 is emergency, needs a retry interval & expiration set - if c.Alerts.Pushover.Priority == 2 { - if c.Alerts.Pushover.Retry == 0 || c.Alerts.Pushover.Expire == 0 { - pushoverErrors = append(pushoverErrors, "Pushover retry interval & expiration must be set with priority 2!") + if c.Alerts.Pushover[id].Priority == 2 { + if c.Alerts.Pushover[id].Retry == 0 || c.Alerts.Pushover[id].Expire == 0 { + pushoverErrors = append(pushoverErrors, fmt.Sprintf("Pushover retry interval & expiration must be set with priority 2! Profile ID %v", id)) } - if c.Alerts.Pushover.Retry < 30 { - pushoverErrors = append(pushoverErrors, "Pushover retry cannot be less than 30 seconds!") + if c.Alerts.Pushover[id].Retry < 30 { + pushoverErrors = append(pushoverErrors, fmt.Sprintf("Pushover retry cannot be less than 30 seconds! Profile ID %v", id)) } } - if c.Alerts.Pushover.TTL < 0 { - pushoverErrors = append(pushoverErrors, "Pushover TTL cannot be negative!") + if c.Alerts.Pushover[id].TTL < 0 { + pushoverErrors = append(pushoverErrors, fmt.Sprintf("Pushover TTL cannot be negative! Profile ID %v", id)) } // Check template syntax - if msg := validateTemplate("Pushover", c.Alerts.Pushover.Template); msg != "" { - pushoverErrors = append(pushoverErrors, msg) + if msg := validateTemplate("Pushover", c.Alerts.Pushover[id].Template); msg != "" { + pushoverErrors = append(pushoverErrors, msg+fmt.Sprintf("Profile ID %v", id)) } return pushoverErrors } -func (c *Config) validateNtfy() []string { +func (c *Config) validateNtfy(id int) []string { var ntfyErrors []string - log.Debug().Msg("Ntfy alerting enabled.") - ntfyStatus := models.NotifierStatus{Enabled: true, Status: "configured, not used yet"} + log.Debug().Msgf("Alerting enabled for Ntfy profile ID %v", id) + ntfyStatus := models.NotifierStatus{ID: id, Enabled: true, Status: "configured, not used yet"} Internal.Status.Notifications.Ntfy = append(Internal.Status.Notifications.Ntfy, ntfyStatus) - if c.Alerts.Ntfy.Server == "" { - ntfyErrors = append(ntfyErrors, "No Ntfy server specified!") + if c.Alerts.Ntfy[id].Server == "" { + ntfyErrors = append(ntfyErrors, fmt.Sprintf("No Ntfy server specified! Profile ID %v", id)) } - if c.Alerts.Ntfy.Topic == "" { - ntfyErrors = append(ntfyErrors, "No Ntfy topic specified!") + if c.Alerts.Ntfy[id].Topic == "" { + ntfyErrors = append(ntfyErrors, fmt.Sprintf("No Ntfy topic specified! Profile ID %v", id)) } // Check template syntax - if msg := validateTemplate("Ntfy", c.Alerts.Ntfy.Template); msg != "" { - ntfyErrors = append(ntfyErrors, msg) + if msg := validateTemplate("Ntfy", c.Alerts.Ntfy[id].Template); msg != "" { + ntfyErrors = append(ntfyErrors, msg+fmt.Sprintf("Profile ID %v", id)) } // Check HTTP header template syntax if msg := validateTemplate("Ntfy HTTP Headers", c.Alerts.General.Title); msg != "" { - ntfyErrors = append(ntfyErrors, msg) + ntfyErrors = append(ntfyErrors, msg+fmt.Sprintf("Profile ID %v", id)) } return ntfyErrors } -func (c *Config) validateWebhook() []string { +func (c *Config) validateWebhook(id int) []string { var webhookErrors []string - log.Debug().Msg("Webhook alerting enabled.") - webhookStatus := models.NotifierStatus{Enabled: true, Status: "configured, not used yet"} + log.Debug().Msgf("Alerting enabled for Webhook profile ID %v", id) + webhookStatus := models.NotifierStatus{ID: id, Enabled: true, Status: "configured, not used yet"} Internal.Status.Notifications.Webhook = append(Internal.Status.Notifications.Webhook, webhookStatus) - if c.Alerts.Webhook.Server == "" { - webhookErrors = append(webhookErrors, "No Webhook server specified!") + if c.Alerts.Webhook[id].Server == "" { + webhookErrors = append(webhookErrors, fmt.Sprintf("No Webhook server specified! Profile ID %v", id)) } // Check HTTP header template syntax if msg := validateTemplate("Webhook HTTP Headers", c.Alerts.General.Title); msg != "" { - webhookErrors = append(webhookErrors, msg) + webhookErrors = append(webhookErrors, msg+fmt.Sprintf("Profile ID %v", id)) } return webhookErrors @@ -571,26 +584,40 @@ func (c *Config) validateWebhook() []string { func (c *Config) validateAlertingEnabled() string { // Check to ensure at least one alert provider is configured - if c.Alerts.Discord.Enabled { - return "" + for _, profile := range c.Alerts.Discord { + if profile.Enabled { + return "" + } } - if c.Alerts.Gotify.Enabled { - return "" + for _, profile := range c.Alerts.Gotify { + if profile.Enabled { + return "" + } } - if c.Alerts.SMTP.Enabled { - return "" + for _, profile := range c.Alerts.SMTP { + if profile.Enabled { + return "" + } } - if c.Alerts.Telegram.Enabled { - return "" + for _, profile := range c.Alerts.Telegram { + if profile.Enabled { + return "" + } } - if c.Alerts.Pushover.Enabled { - return "" + for _, profile := range c.Alerts.Pushover { + if profile.Enabled { + return "" + } } - if c.Alerts.Ntfy.Enabled { - return "" + for _, profile := range c.Alerts.Ntfy { + if profile.Enabled { + return "" + } } - if c.Alerts.Webhook.Enabled { - return "" + for _, profile := range c.Alerts.Webhook { + if profile.Enabled { + return "" + } } return "No alerting methods have been configured. Please check config file syntax!" } diff --git a/config/validate_test.go b/config/validate_test.go index 20ee0f9..17777d8 100644 --- a/config/validate_test.go +++ b/config/validate_test.go @@ -176,18 +176,19 @@ func TestValidateAlertGeneral(t *testing.T) { func TestValidateDiscord(t *testing.T) { config := Config{Alerts: &models.Alerts{}} + config.Alerts.Discord = make([]models.Discord, 1) // Test valid config - config.Alerts.Discord.Webhook = "https://something.test" - result := config.validateDiscord() + config.Alerts.Discord[0].Webhook = "https://something.test" + result := config.validateDiscord(0) expected := 0 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test missing webhook config - config.Alerts.Discord.Webhook = "" - result = config.validateDiscord() + config.Alerts.Discord[0].Webhook = "" + result = config.validateDiscord(0) expected = 1 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) @@ -196,27 +197,28 @@ func TestValidateDiscord(t *testing.T) { func TestValidateGotify(t *testing.T) { config := Config{Alerts: &models.Alerts{}} + config.Alerts.Gotify = make([]models.Gotify, 1) // Test valid config - config.Alerts.Gotify.Server = "https://something.test" - config.Alerts.Gotify.Token = "abcdefg" - result := config.validateGotify() + config.Alerts.Gotify[0].Server = "https://something.test" + config.Alerts.Gotify[0].Token = "abcdefg" + result := config.validateGotify(0) expected := 0 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test missing server config - config.Alerts.Gotify.Server = "" - result = config.validateGotify() + config.Alerts.Gotify[0].Server = "" + result = config.validateGotify(0) expected = 1 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test missing token config - config.Alerts.Gotify.Token = "" - result = config.validateGotify() + config.Alerts.Gotify[0].Token = "" + result = config.validateGotify(0) expected = 1 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) @@ -225,47 +227,48 @@ func TestValidateGotify(t *testing.T) { func TestValidateSMTP(t *testing.T) { config := Config{Alerts: &models.Alerts{}} + config.Alerts.SMTP = make([]models.SMTP, 1) // Test valid config - config.Alerts.SMTP.Server = "192.0.2.10" - config.Alerts.SMTP.Recipient = "someone@none.test" - config.Alerts.SMTP.User = "someuser" - config.Alerts.SMTP.Password = "abcd" - result := config.validateSMTP() + config.Alerts.SMTP[0].Server = "192.0.2.10" + config.Alerts.SMTP[0].Recipient = "someone@none.test" + config.Alerts.SMTP[0].User = "someuser" + config.Alerts.SMTP[0].Password = "abcd" + result := config.validateSMTP(0) expected := 0 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Check Default port set - if config.Alerts.SMTP.Port != 25 { - t.Errorf("Expected: port 25 , Got: %v", config.Alerts.SMTP.Port) + if config.Alerts.SMTP[0].Port != 25 { + t.Errorf("Expected: port 25 , Got: %v", config.Alerts.SMTP[0].Port) } // Check SMTP From is copied - if config.Alerts.SMTP.User != config.Alerts.SMTP.From { - t.Errorf("Expected: %v, Got: %v", config.Alerts.SMTP.User, config.Alerts.SMTP.From) + if config.Alerts.SMTP[0].User != config.Alerts.SMTP[0].From { + t.Errorf("Expected: %v, Got: %v", config.Alerts.SMTP[0].User, config.Alerts.SMTP[0].From) } // Test missing server - config.Alerts.SMTP.Server = "" - result = config.validateSMTP() + config.Alerts.SMTP[0].Server = "" + result = config.validateSMTP(0) expected = 1 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test missing recipient - config.Alerts.SMTP.Recipient = "" - result = config.validateSMTP() + config.Alerts.SMTP[0].Recipient = "" + result = config.validateSMTP(0) expected = 2 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test invalid auth config - config.Alerts.SMTP.Password = "" - result = config.validateSMTP() + config.Alerts.SMTP[0].Password = "" + result = config.validateSMTP(0) expected = 3 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) @@ -274,27 +277,28 @@ func TestValidateSMTP(t *testing.T) { func TestValidateTelegram(t *testing.T) { config := Config{Alerts: &models.Alerts{}} + config.Alerts.Telegram = make([]models.Telegram, 1) // Test valid config - config.Alerts.Telegram.ChatID = 1234 - config.Alerts.Telegram.Token = "abcd" - result := config.validateTelegram() + config.Alerts.Telegram[0].ChatID = 1234 + config.Alerts.Telegram[0].Token = "abcd" + result := config.validateTelegram(0) expected := 0 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test missing Chat ID - config.Alerts.Telegram.ChatID = 0 - result = config.validateTelegram() + config.Alerts.Telegram[0].ChatID = 0 + result = config.validateTelegram(0) expected = 1 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test missing Token - config.Alerts.Telegram.Token = "" - result = config.validateTelegram() + config.Alerts.Telegram[0].Token = "" + result = config.validateTelegram(0) expected = 2 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) @@ -303,53 +307,54 @@ func TestValidateTelegram(t *testing.T) { func TestValidatePushover(t *testing.T) { config := Config{Alerts: &models.Alerts{}} + config.Alerts.Pushover = make([]models.Pushover, 1) // Test valid config - config.Alerts.Pushover.Token = "abcd" - config.Alerts.Pushover.Userkey = "abcd" - config.Alerts.Pushover.Priority = 1 - result := config.validatePushover() + config.Alerts.Pushover[0].Token = "abcd" + config.Alerts.Pushover[0].Userkey = "abcd" + config.Alerts.Pushover[0].Priority = 1 + result := config.validatePushover(0) expected := 0 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test missing token - config.Alerts.Pushover.Token = "" - result = config.validatePushover() + config.Alerts.Pushover[0].Token = "" + result = config.validatePushover(0) expected = 1 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test missing Userkey - config.Alerts.Pushover.Userkey = "" - result = config.validatePushover() + config.Alerts.Pushover[0].Userkey = "" + result = config.validatePushover(0) expected = 2 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test priority 2 missing retry / expiration config - config.Alerts.Pushover.Priority = 2 - result = config.validatePushover() + config.Alerts.Pushover[0].Priority = 2 + result = config.validatePushover(0) expected = 4 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test priority 2 with low retry interval - config.Alerts.Pushover.Retry = 2 - config.Alerts.Pushover.Expire = 10 - result = config.validatePushover() + config.Alerts.Pushover[0].Retry = 2 + config.Alerts.Pushover[0].Expire = 10 + result = config.validatePushover(0) expected = 3 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test negative TTL - config.Alerts.Pushover.TTL = -2 - result = config.validatePushover() + config.Alerts.Pushover[0].TTL = -2 + result = config.validatePushover(0) expected = 4 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) @@ -358,27 +363,28 @@ func TestValidatePushover(t *testing.T) { func TestValidateNtfy(t *testing.T) { config := Config{Alerts: &models.Alerts{}} + config.Alerts.Ntfy = make([]models.Ntfy, 1) // Test valid config - config.Alerts.Ntfy.Server = "https://ntfy.test" - config.Alerts.Ntfy.Topic = "frigate" - result := config.validateNtfy() + config.Alerts.Ntfy[0].Server = "https://ntfy.test" + config.Alerts.Ntfy[0].Topic = "frigate" + result := config.validateNtfy(0) expected := 0 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test missing server config - config.Alerts.Ntfy.Server = "" - result = config.validateNtfy() + config.Alerts.Ntfy[0].Server = "" + result = config.validateNtfy(0) expected = 1 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test missing topic config - config.Alerts.Ntfy.Topic = "" - result = config.validateNtfy() + config.Alerts.Ntfy[0].Topic = "" + result = config.validateNtfy(0) expected = 2 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) @@ -386,18 +392,19 @@ func TestValidateNtfy(t *testing.T) { } func TestValidateWebhook(t *testing.T) { config := Config{Alerts: &models.Alerts{}} + config.Alerts.Webhook = make([]models.Webhook, 1) // Test valid config - config.Alerts.Webhook.Server = "https://webhook.test" - result := config.validateWebhook() + config.Alerts.Webhook[0].Server = "https://webhook.test" + result := config.validateWebhook(0) expected := 0 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) } // Test missing server config - config.Alerts.Webhook.Server = "" - result = config.validateWebhook() + config.Alerts.Webhook[0].Server = "" + result = config.validateWebhook(0) expected = 1 if len(result) != expected { t.Errorf("Expected: %v error(s), Got: %v", expected, result) @@ -407,9 +414,10 @@ func TestValidateWebhook(t *testing.T) { func TestValidateAlertingEnabled(t *testing.T) { config := Config{Alerts: &models.Alerts{}} + config.Alerts.Discord = make([]models.Discord, 1) // Test valid config - config.Alerts.Discord.Enabled = true + config.Alerts.Discord[0].Enabled = true result := config.validateAlertingEnabled() expected := "" if result != expected { @@ -417,7 +425,7 @@ func TestValidateAlertingEnabled(t *testing.T) { } // Test missing server config - config.Alerts.Discord.Enabled = false + config.Alerts.Discord[0].Enabled = false result = config.validateAlertingEnabled() if result == "" { t.Errorf("Expected: error message, Got: %v", result) diff --git a/events/events.go b/events/events.go index 7c1ef70..b885e05 100644 --- a/events/events.go +++ b/events/events.go @@ -32,7 +32,7 @@ func processEvent(event models.Event) { Msgf("Event start time: %s", eventTime) // Check that event passes configured filters - if !checkFilters(event) { + if !checkEventFilters(event) { return } diff --git a/events/filters.go b/events/filters.go index 51cd3cb..411390f 100644 --- a/events/filters.go +++ b/events/filters.go @@ -11,8 +11,8 @@ import ( "github.com/0x2142/frigate-notify/models" ) -// checkFilters processes incoming event through configured filters to determine if it should generate a notification -func checkFilters(event models.Event) bool { +// checkEventFilters processes incoming event through configured filters to determine if it should generate a notification +func checkEventFilters(event models.Event) bool { // Check if notifications are currently disabled if !config.Internal.Status.Notifications.Enabled { log.Info().Msg("Event dropped - Notifications currently disabled.") diff --git a/events/reviews.go b/events/reviews.go index 0029f85..36c7d2c 100644 --- a/events/reviews.go +++ b/events/reviews.go @@ -67,7 +67,7 @@ func processReview(review models.Review) { // Check that event passes configured filters detection.CurrentZones = detection.Zones - if !checkFilters(detection) { + if !checkEventFilters(detection) { reviewFiltered = true break } diff --git a/models/config.go b/models/config.go index da4ae7c..529b9ed 100644 --- a/models/config.go +++ b/models/config.go @@ -47,18 +47,18 @@ type Cameras struct { } type Alerts struct { - General General `fig:"general" json:"general,omitempty" doc:"Common alert settings"` - Quiet Quiet `fig:"quiet" json:"quiet,omitempty" doc:"Alert quiet periods"` - Zones Zones `fig:"zones" json:"zones,omitempty" doc:"Allow/Block zones from alerting"` - Labels Labels `fig:"labels" json:"labels,omitempty" doc:"Allow/Block labels from alerting"` - SubLabels Labels `fig:"sublabels" json:"sublabels,omitempty" doc:"Allow/Block sublabels from alerting"` - Discord Discord `fig:"discord" json:"discord,omitempty" doc:"Discord notification settings"` - Gotify Gotify `fig:"gotify" json:"gotify,omitempty" doc:"Gotify notification settings"` - SMTP SMTP `fig:"smtp" json:"smtp,omitempty" doc:"SMTP notification settings"` - Telegram Telegram `fig:"telegram" json:"telegram,omitempty" doc:"Telegram notification settings"` - Pushover Pushover `fig:"pushover" json:"pushover,omitempty" doc:"Pushover notification settings"` - Ntfy Ntfy `fig:"ntfy" json:"ntfy,omitempty" doc:"Ntfy notification settings"` - Webhook Webhook `fig:"webhook" json:"webhook,omitempty" doc:"Webhook notification settings"` + General General `fig:"general" json:"general,omitempty" doc:"Common alert settings"` + Quiet Quiet `fig:"quiet" json:"quiet,omitempty" doc:"Alert quiet periods"` + Zones Zones `fig:"zones" json:"zones,omitempty" doc:"Allow/Block zones from alerting"` + Labels Labels `fig:"labels" json:"labels,omitempty" doc:"Allow/Block labels from alerting"` + SubLabels Labels `fig:"sublabels" json:"sublabels,omitempty" doc:"Allow/Block sublabels from alerting"` + Discord []Discord `fig:"discord" json:"discord,omitempty" doc:"Discord notification settings"` + Gotify []Gotify `fig:"gotify" json:"gotify,omitempty" doc:"Gotify notification settings"` + SMTP []SMTP `fig:"smtp" json:"smtp,omitempty" doc:"SMTP notification settings"` + Telegram []Telegram `fig:"telegram" json:"telegram,omitempty" doc:"Telegram notification settings"` + Pushover []Pushover `fig:"pushover" json:"pushover,omitempty" doc:"Pushover notification settings"` + Ntfy []Ntfy `fig:"ntfy" json:"ntfy,omitempty" doc:"Ntfy notification settings"` + Webhook []Webhook `fig:"webhook" json:"webhook,omitempty" doc:"Webhook notification settings"` } type General struct { @@ -89,51 +89,64 @@ type Labels struct { Block []string `fig:"block" json:"block,omitempty" doc:"List of labels to always block" default:[]` } +type AlertFilter struct { + Cameras []string `fig:"cameras" json:"cameras,omitempty" doc:"List of cameras that will use this alert provider` + Zones []string `fig:"zones" json:"zones,omitempty" doc:"List of zones that will use this alert provider` + Quiet Quiet `fig:"quiet" json:"quiet,omitempty" doc:"Quiet period for this alert provider"` + Labels []string `fig:"labels" json:"labels,omitempty" doc:"List of labels that will use this alert provider"` + Sublabels []string `fig:"sublabels" json:"sublabels,omitempty" doc:"List of sublabels that will use this alert provider"` +} + type Discord struct { - Enabled bool `fig:"enabled" json:"enabled" enum:"true,false" doc:"Enable notifications via Discord" default:false` - Webhook string `fig:"webhook" json:"webhook,omitempty" doc:"Discord webhook URL to send alerts" default:""` - Template string `fig:"template" json:"template,omitempty" doc:"Custom message template" default:""` + Enabled bool `fig:"enabled" json:"enabled" enum:"true,false" doc:"Enable notifications via Discord" default:false` + Webhook string `fig:"webhook" json:"webhook,omitempty" doc:"Discord webhook URL to send alerts" default:""` + Template string `fig:"template" json:"template,omitempty" doc:"Custom message template" default:""` + Filters AlertFilter `fig:"filters" json:"filters,omitempty" doc:"Filter notifications sent via this provider"` } type Gotify struct { - Enabled bool `fig:"enabled" json:"enabled" enum:"true,false" doc:"Enable notifications via Gotify" default:false` - Server string `fig:"server" json:"server,omitempty" doc:"Gotify server URL" default:""` - Token string `fig:"token" json:"token,omitempty" doc:"Gotify app token" default:""` - Insecure bool `fig:"ignoressl" json:"ignoressl,omitempty" doc:"Ignore TLS/SSL errors" default:false` - Template string `fig:"template" json:"template,omitempty" doc:"Custom message template" default:""` + Enabled bool `fig:"enabled" json:"enabled" enum:"true,false" doc:"Enable notifications via Gotify" default:false` + Server string `fig:"server" json:"server,omitempty" doc:"Gotify server URL" default:""` + Token string `fig:"token" json:"token,omitempty" doc:"Gotify app token" default:""` + Insecure bool `fig:"ignoressl" json:"ignoressl,omitempty" doc:"Ignore TLS/SSL errors" default:false` + Template string `fig:"template" json:"template,omitempty" doc:"Custom message template" default:""` + Filters AlertFilter `fig:"filters" json:"filters,omitempty" doc:"Filter notifications sent via this provider"` } type SMTP struct { - Enabled bool `fig:"enabled" json:"enabled" enum:"true,false" doc:"Enable notifications via SMTP" default:false` - Server string `fig:"server" json:"server,omitempty" doc:"SMTP server hostname or IP address" default:""` - Port int `fig:"port" json:"port,omitempty" minimum:"1" maximum:"65535" doc:"SMTP server port" default:25` - TLS bool `fig:"tls" json:"tls,omitempty" enum:"true,false" doc:"Enable/Disable TLS connection" default:false` - User string `fig:"user" json:"user,omitempty" doc:"SMTP user for authentication" default:""` - Password string `fig:"password" json:"password,omitempty" doc:"SMTP password for authentication" default:""` - From string `fig:"from" json:"from,omitempty" format:"email" doc:"SMTP sender" default:""` - Recipient string `fig:"recipient" json:"recipient,omitempty" format:"email" doc:"SMTP recipient" default:""` - Template string `fig:"template" json:"template,omitempty" doc:"Custom message template" default:""` - Insecure bool `fig:"ignoressl" enum:"true,false" json:"ignoressl,omitempty" default:false` + Enabled bool `fig:"enabled" json:"enabled" enum:"true,false" doc:"Enable notifications via SMTP" default:false` + Server string `fig:"server" json:"server,omitempty" doc:"SMTP server hostname or IP address" default:""` + Port int `fig:"port" json:"port,omitempty" minimum:"1" maximum:"65535" doc:"SMTP server port" default:25` + TLS bool `fig:"tls" json:"tls,omitempty" enum:"true,false" doc:"Enable/Disable TLS connection" default:false` + User string `fig:"user" json:"user,omitempty" doc:"SMTP user for authentication" default:""` + Password string `fig:"password" json:"password,omitempty" doc:"SMTP password for authentication" default:""` + From string `fig:"from" json:"from,omitempty" format:"email" doc:"SMTP sender" default:""` + Recipient string `fig:"recipient" json:"recipient,omitempty" format:"email" doc:"SMTP recipient" default:""` + Template string `fig:"template" json:"template,omitempty" doc:"Custom message template" default:""` + Insecure bool `fig:"ignoressl" enum:"true,false" json:"ignoressl,omitempty" default:false` + Filters AlertFilter `fig:"filters" json:"filters,omitempty" doc:"Filter notifications sent via this provider"` } type Telegram struct { - Enabled bool `fig:"enabled" json:"enabled" enum:"true,false" doc:"Enable notifications via Telegram" default:false` - ChatID int64 `fig:"chatid" json:"chatid,omitempty" minimum:"1" doc:"Telegram chat ID" default:"0"` - Token string `fig:"token" json:"token,omitempty" doc:"Telegram bot token" default:""` - Template string `fig:"template" json:"template,omitempty" doc:"Custom message template" default:""` + Enabled bool `fig:"enabled" json:"enabled" enum:"true,false" doc:"Enable notifications via Telegram" default:false` + ChatID int64 `fig:"chatid" json:"chatid,omitempty" minimum:"1" doc:"Telegram chat ID" default:"0"` + Token string `fig:"token" json:"token,omitempty" doc:"Telegram bot token" default:""` + Template string `fig:"template" json:"template,omitempty" doc:"Custom message template" default:""` + Filters AlertFilter `fig:"filters" json:"filters,omitempty" doc:"Filter notifications sent via this provider"` } type Pushover struct { - Enabled bool `fig:"enabled" json:"enabled" enum:"true,false" doc:"Enable notifications via Pushover" default:false` - Token string `fig:"token" json:"token,omitempty" doc:"Pushover app token" default:""` - Userkey string `fig:"userkey" json:"userkey,omitempty" doc:"Pushover user key" default:""` - Devices string `fig:"devices" json:"devices,omitempty" doc:"Pushover devices to target for notification" default:""` - Sound string `fig:"sound" json:"sound,omitempty" doc:"Pushover notification sound" default:"pushover"` - Priority int `fig:"priority" json:"priority,omitempty" minimum:"-2" maximum:"2" doc:"Pushover message priority" default:"0"` - Retry int `fig:"retry" json:"retry,omitempty" doc:"Retry interval for emergency notifications (Priority 2)" default:"0"` - Expire int `fig:"expire" json:"expire,omitempty" doc:"Expiration timer for emergency notifications (Priority 2)" default:"0"` - TTL int `fig:"ttl" json:"ttl,omitempty" doc:"Time to Live for notification messages" default:"0"` - Template string `fig:"template" json:"template,omitempty" doc:"Custom message template" default:""` + Enabled bool `fig:"enabled" json:"enabled" enum:"true,false" doc:"Enable notifications via Pushover" default:false` + Token string `fig:"token" json:"token,omitempty" doc:"Pushover app token" default:""` + Userkey string `fig:"userkey" json:"userkey,omitempty" doc:"Pushover user key" default:""` + Devices string `fig:"devices" json:"devices,omitempty" doc:"Pushover devices to target for notification" default:""` + Sound string `fig:"sound" json:"sound,omitempty" doc:"Pushover notification sound" default:"pushover"` + Priority int `fig:"priority" json:"priority,omitempty" minimum:"-2" maximum:"2" doc:"Pushover message priority" default:"0"` + Retry int `fig:"retry" json:"retry,omitempty" doc:"Retry interval for emergency notifications (Priority 2)" default:"0"` + Expire int `fig:"expire" json:"expire,omitempty" doc:"Expiration timer for emergency notifications (Priority 2)" default:"0"` + TTL int `fig:"ttl" json:"ttl,omitempty" doc:"Time to Live for notification messages" default:"0"` + Template string `fig:"template" json:"template,omitempty" doc:"Custom message template" default:""` + Filters AlertFilter `fig:"filters" json:"filters,omitempty" doc:"Filter notifications sent via this provider"` } type Ntfy struct { @@ -143,6 +156,7 @@ type Ntfy struct { Insecure bool `fig:"ignoressl" json:"ignoressl,omitempty" doc:"Ignore TLS/SSL errors" default:false` Headers []map[string]string `fig:"headers" json:"headers,omitempty" doc:"HTTP headers to include with Ntfy notifications" default:[]` Template string `fig:"template" json:"template,omitempty" doc:"Custom message template" default:""` + Filters AlertFilter `fig:"filters" json:"filters,omitempty" doc:"Filter notifications sent via this provider"` } type Webhook struct { @@ -153,6 +167,7 @@ type Webhook struct { Params []map[string]string `fix:"params" json:"params,omitempty" doc:"URL parameters for webhook notifications"` Headers []map[string]string `fig:"headers" json:"headers,omitempty" doc:"HTTP headers for webhook notifications"` Template interface{} `fig:"template" json:"template,omitempty" doc:"Custom message template"` + Filters AlertFilter `fig:"filters" json:"filters,omitempty" doc:"Filter notifications sent via this provider"` } type Monitor struct { diff --git a/notifier/alerts.go b/notifier/alerts.go index cafeedb..7f89024 100644 --- a/notifier/alerts.go +++ b/notifier/alerts.go @@ -21,6 +21,11 @@ import ( var TemplateFiles embed.FS +type notifMeta struct { + name string + index int +} + // SendAlert forwards alert information to all enabled alerting methods func SendAlert(event models.Event) { config.Internal.Status.LastNotification = time.Now() @@ -43,26 +48,68 @@ func SendAlert(event models.Event) { } // Send Alerts - if config.ConfigData.Alerts.Discord.Enabled { - go SendDiscordMessage(event, bytes.NewReader(snap)) + // Discord + for id, profile := range config.ConfigData.Alerts.Discord { + if profile.Enabled { + provider := notifMeta{name: "discord", index: id} + if checkAlertFilters(event, profile.Filters, provider) { + go SendDiscordMessage(event, bytes.NewReader(snap), provider) + } + } } - if config.ConfigData.Alerts.Gotify.Enabled { - go SendGotifyPush(event) + // Gotify + for id, profile := range config.ConfigData.Alerts.Gotify { + if profile.Enabled { + provider := notifMeta{name: "gotify", index: id} + if checkAlertFilters(event, profile.Filters, provider) { + go SendGotifyPush(event, provider) + } + } } - if config.ConfigData.Alerts.SMTP.Enabled { - go SendSMTP(event, bytes.NewReader(snap)) + // SMTP + for id, profile := range config.ConfigData.Alerts.SMTP { + if profile.Enabled { + provider := notifMeta{name: "smtp", index: id} + if checkAlertFilters(event, profile.Filters, provider) { + go SendSMTP(event, bytes.NewReader(snap), provider) + } + } } - if config.ConfigData.Alerts.Telegram.Enabled { - go SendTelegramMessage(event, bytes.NewReader(snap)) + // Telegram + for id, profile := range config.ConfigData.Alerts.Telegram { + if profile.Enabled { + provider := notifMeta{name: "telegram", index: id} + if checkAlertFilters(event, profile.Filters, provider) { + go SendTelegramMessage(event, bytes.NewReader(snap), provider) + } + } } - if config.ConfigData.Alerts.Pushover.Enabled { - go SendPushoverMessage(event, bytes.NewReader(snap)) + // Pushover + for id, profile := range config.ConfigData.Alerts.Pushover { + if profile.Enabled { + provider := notifMeta{name: "pushover", index: id} + if checkAlertFilters(event, profile.Filters, provider) { + go SendPushoverMessage(event, bytes.NewReader(snap), provider) + } + } } - if config.ConfigData.Alerts.Ntfy.Enabled { - go SendNtfyPush(event, bytes.NewReader(snap)) + // Ntfy + for id, profile := range config.ConfigData.Alerts.Ntfy { + if profile.Enabled { + provider := notifMeta{name: "ntfy", index: id} + if checkAlertFilters(event, profile.Filters, provider) { + go SendNtfyPush(event, bytes.NewReader(snap), provider) + } + } } - if config.ConfigData.Alerts.Webhook.Enabled { - go SendWebhook(event) + // Webhook + for id, profile := range config.ConfigData.Alerts.Webhook { + if profile.Enabled { + provider := notifMeta{name: "webhook", index: id} + if checkAlertFilters(event, profile.Filters, provider) { + go SendWebhook(event, provider) + } + } } } diff --git a/notifier/discord.go b/notifier/discord.go index ed70743..d2fa7dc 100644 --- a/notifier/discord.go +++ b/notifier/discord.go @@ -14,22 +14,25 @@ import ( ) // SendDiscordMessage pushes alert message to Discord via webhook -func SendDiscordMessage(event models.Event, snapshot io.Reader) { +func SendDiscordMessage(event models.Event, snapshot io.Reader, provider notifMeta) { + profile := config.ConfigData.Alerts.Discord[provider.index] + var err error var message string // Build notification - if config.ConfigData.Alerts.Discord.Template != "" { - message = renderMessage(config.ConfigData.Alerts.Discord.Template, event, "message", "Discord") + if profile.Template != "" { + message = renderMessage(profile.Template, event, "message", "Discord") } else { message = renderMessage("markdown", event, "message", "Discord") } // Connect to Discord - client, err := webhook.NewWithURL(config.ConfigData.Alerts.Discord.Webhook) + client, err := webhook.NewWithURL(profile.Webhook) if err != nil { log.Warn(). Str("event_id", event.ID). Str("provider", "Discord"). + Int("provider_id", provider.index). Err(err). Msg("Unable to send alert") config.Internal.Status.Notifications.Discord[0].NotifFailure(err.Error()) @@ -48,6 +51,7 @@ func SendDiscordMessage(event models.Event, snapshot io.Reader) { msg, err = client.CreateMessage(discord.NewWebhookMessageCreateBuilder().SetEmbeds(embed).SetFiles(image).Build()) log.Trace(). Str("event_id", event.ID). + Int("provider_id", provider.index). Interface("payload", msg). Msg("Send Discord Alert") @@ -56,6 +60,7 @@ func SendDiscordMessage(event models.Event, snapshot io.Reader) { msg, err = client.CreateMessage(discord.NewWebhookMessageCreateBuilder().SetEmbeds(embed).Build()) log.Trace(). Str("event_id", event.ID). + Int("provider_id", provider.index). Interface("payload", msg). Msg("Send Discord Alert") } @@ -63,6 +68,7 @@ func SendDiscordMessage(event models.Event, snapshot io.Reader) { log.Warn(). Str("event_id", event.ID). Str("provider", "Discord"). + Int("provider_id", provider.index). Err(err). Msg("Unable to send alert") config.Internal.Status.Notifications.Discord[0].NotifFailure(err.Error()) @@ -70,6 +76,7 @@ func SendDiscordMessage(event models.Event, snapshot io.Reader) { log.Info(). Str("event_id", event.ID). Str("provider", "Discord"). + Int("provider_id", provider.index). Msg("Alert sent") config.Internal.Status.Notifications.Discord[0].NotifSuccess() } diff --git a/notifier/filters.go b/notifier/filters.go new file mode 100644 index 0000000..86982ee --- /dev/null +++ b/notifier/filters.go @@ -0,0 +1,134 @@ +package notifier + +import ( + "slices" + "time" + + "github.com/0x2142/frigate-notify/models" + "github.com/rs/zerolog/log" +) + +// checkAlertFilters will determine which notification provider is able to send this alert +func checkAlertFilters(event models.Event, filters models.AlertFilter, provider notifMeta) bool { + log.Trace(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Msg("Checking alert filters") + + // Check against quiet hours + currentTime, _ := time.Parse("15:04:05", time.Now().Format("15:04:05")) + start, _ := time.Parse("15:04", filters.Quiet.Start) + end, _ := time.Parse("15:04", filters.Quiet.End) + log.Trace(). + Time("current_time", currentTime). + Time("quiet_start", start). + Time("quiet_end", end). + Str("provider", provider.name). + Int("provider_id", provider.index). + Msg("Check quiet hours") + // Check if quiet period is overnight + if end.Before(start) { + if currentTime.After(start) || currentTime.Before(end) { + log.Debug(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Msg("Notification droppped - Quiet hours") + return false + } + } + // Otherwise check if between start & end times + if currentTime.After(start) && currentTime.Before(end) { + log.Debug(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Msg("Notification droppped - Quiet hours") + return false + } + + // Check filtered cameras + log.Trace(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Str("camera", event.Camera). + Strs("allowed", filters.Cameras). + Msg("Check allowed cameras") + if len(filters.Cameras) >= 1 { + if !slices.Contains(filters.Cameras, event.Camera) { + log.Debug(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Msg("Notification droppped - Camera not on filter list") + return false + } + } + + // Check filtered zones + log.Trace(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Strs("zones", event.CurrentZones). + Strs("allowed", filters.Zones). + Msg("Check allowed zone") + if len(filters.Zones) >= 1 { + matchzone := false + for _, zone := range event.CurrentZones { + if slices.Contains(filters.Zones, zone) { + matchzone = true + } + } + if !matchzone { + log.Debug(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Msg("Notification droppped - Zone not on filter list") + return false + } + } + + // Check filtered Labels + log.Trace(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Str("label", event.Label). + Strs("allowed", filters.Labels). + Msg("Check allowed label") + if len(filters.Labels) >= 1 { + if !slices.Contains(filters.Labels, event.Label) { + log.Debug(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Msg("Notification droppped - Label not on filter list") + return false + } + } + + // Check filtered Sublabels + log.Trace(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Strs("label", event.SubLabel). + Strs("allowed", filters.Sublabels). + Msg("Check allowed sublabel") + if len(filters.Sublabels) >= 1 { + matchsublabel := false + for _, sublabel := range event.SubLabel { + if slices.Contains(filters.Sublabels, sublabel) { + matchsublabel = true + } + } + if !matchsublabel { + log.Debug(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Msg("Notification droppped - Sublabel not on filter list") + return false + } + } + + // Alert permitted if all conditions pass + log.Trace(). + Str("provider", provider.name). + Int("provider_id", provider.index). + Msg("Alert filters passed!") + return true +} diff --git a/notifier/gotify.go b/notifier/gotify.go index bb57106..8205420 100644 --- a/notifier/gotify.go +++ b/notifier/gotify.go @@ -35,7 +35,9 @@ type gotifyPayload struct { } // SendGotifyPush forwards alert messages to Gotify push notification server -func SendGotifyPush(event models.Event) { +func SendGotifyPush(event models.Event, provider notifMeta) { + profile := config.ConfigData.Alerts.Gotify[provider.index] + var snapshotURL string if config.ConfigData.Frigate.PublicURL != "" { snapshotURL = config.ConfigData.Frigate.PublicURL + "/api/events/" + event.ID + "/snapshot.jpg" @@ -44,8 +46,8 @@ func SendGotifyPush(event models.Event) { } // Build notification var message string - if config.ConfigData.Alerts.Gotify.Template != "" { - message = renderMessage(config.ConfigData.Alerts.Gotify.Template, event, "message", "Gotify") + if profile.Template != "" { + message = renderMessage(profile.Template, event, "message", "Gotify") } else { message = renderMessage("markdown", event, "message", "Gotify") } @@ -68,20 +70,22 @@ func SendGotifyPush(event models.Event) { Str("event_id", event.ID). Str("provider", "Gotify"). Err(err). + Int("provider_id", provider.index). Msg("Unable to send alert") config.Internal.Status.Notifications.Gotify[0].NotifFailure(err.Error()) return } - gotifyURL := fmt.Sprintf("%s/message?token=%s&", config.ConfigData.Alerts.Gotify.Server, config.ConfigData.Alerts.Gotify.Token) + gotifyURL := fmt.Sprintf("%s/message?token=%s&", profile.Server, profile.Token) header := map[string]string{"Content-Type": "application/json"} - response, err := util.HTTPPost(gotifyURL, config.ConfigData.Alerts.Gotify.Insecure, data, "", header) + response, err := util.HTTPPost(gotifyURL, profile.Insecure, data, "", header) if err != nil { log.Warn(). Str("event_id", event.ID). Str("provider", "Gotify"). Err(err). + Int("provider_id", provider.index). Msg("Unable to send alert") config.Internal.Status.Notifications.Gotify[0].NotifFailure(err.Error()) return @@ -93,6 +97,7 @@ func SendGotifyPush(event models.Event) { log.Warn(). Str("event_id", event.ID). Str("provider", "Gotify"). + Int("provider_id", provider.index). Msgf("Unable to send alert: %v - %v", errorMessage.Error, errorMessage.ErrorDescription) config.Internal.Status.Notifications.Gotify[0].NotifFailure(errorMessage.ErrorDescription) return @@ -100,6 +105,7 @@ func SendGotifyPush(event models.Event) { log.Info(). Str("event_id", event.ID). Str("provider", "Gotify"). + Int("provider_id", provider.index). Msg("Alert sent") config.Internal.Status.Notifications.Gotify[0].NotifSuccess() } diff --git a/notifier/nfty.go b/notifier/nfty.go index 94896f6..4c58e81 100644 --- a/notifier/nfty.go +++ b/notifier/nfty.go @@ -13,23 +13,25 @@ import ( ) // SendNtfyPush forwards alert messages to Ntfy server -func SendNtfyPush(event models.Event, snapshot io.Reader) { +func SendNtfyPush(event models.Event, snapshot io.Reader, provider notifMeta) { + profile := config.ConfigData.Alerts.Ntfy[provider.index] + // Build notification var message string - if config.ConfigData.Alerts.Ntfy.Template != "" { - message = renderMessage(config.ConfigData.Alerts.Ntfy.Template, event, "message", "Ntfy") + if profile.Template != "" { + message = renderMessage(profile.Template, event, "message", "Ntfy") } else { message = renderMessage("plaintext", event, "message", "Ntfy") } - NtfyURL := fmt.Sprintf("%s/%s", config.ConfigData.Alerts.Ntfy.Server, config.ConfigData.Alerts.Ntfy.Topic) + NtfyURL := fmt.Sprintf("%s/%s", profile.Server, profile.Topic) // Set headers title := renderMessage(config.ConfigData.Alerts.General.Title, event, "title", "Ntfy") var headers []map[string]string headers = append(headers, map[string]string{"Content-Type": "text/markdown"}) headers = append(headers, map[string]string{"X-Title": title}) - headers = append(headers, config.ConfigData.Alerts.Ntfy.Headers...) + headers = append(headers, profile.Headers...) var attachment []byte if event.HasSnapshot { @@ -59,11 +61,12 @@ func SendNtfyPush(event models.Event, snapshot io.Reader) { headers = renderHTTPKV(headers, event, "headers", "Ntfy") - resp, err := util.HTTPPost(NtfyURL, config.ConfigData.Alerts.Ntfy.Insecure, attachment, "", headers...) + resp, err := util.HTTPPost(NtfyURL, profile.Insecure, attachment, "", headers...) if err != nil { log.Warn(). Str("event_id", event.ID). Str("provider", "Ntfy"). + Int("provider_id", provider.index). Err(err). Msg("Unable to send alert") config.Internal.Status.Notifications.Ntfy[0].NotifFailure(err.Error()) @@ -75,6 +78,7 @@ func SendNtfyPush(event models.Event, snapshot io.Reader) { log.Warn(). Str("event_id", event.ID). Str("provider", "Ntfy"). + Int("provider_id", provider.index). Str("error", string(resp)). Msg("Unable to send alert") config.Internal.Status.Notifications.Ntfy[0].NotifFailure(string(resp)) @@ -83,6 +87,7 @@ func SendNtfyPush(event models.Event, snapshot io.Reader) { log.Info(). Str("event_id", event.ID). + Int("provider_id", provider.index). Str("provider", "Ntfy"). Msg("Alert sent") config.Internal.Status.Notifications.Ntfy[0].NotifSuccess() diff --git a/notifier/pushover.go b/notifier/pushover.go index 4db924e..38ec335 100644 --- a/notifier/pushover.go +++ b/notifier/pushover.go @@ -13,45 +13,48 @@ import ( ) // SendPushoverMessage sends alert message through Pushover service -func SendPushoverMessage(event models.Event, snapshot io.Reader) { +func SendPushoverMessage(event models.Event, snapshot io.Reader, provider notifMeta) { + profile := config.ConfigData.Alerts.Pushover[provider.index] + // Build notification var message string - if config.ConfigData.Alerts.Pushover.Template != "" { - message = renderMessage(config.ConfigData.Alerts.Pushover.Template, event, "message", "Pushover") + if profile.Template != "" { + message = renderMessage(profile.Template, event, "message", "Pushover") } else { message = renderMessage("html", event, "message", "Pushover") message = strings.ReplaceAll(message, "
", "") } - push := pushover.New(config.ConfigData.Alerts.Pushover.Token) - recipient := pushover.NewRecipient(config.ConfigData.Alerts.Pushover.Userkey) + push := pushover.New(profile.Token) + recipient := pushover.NewRecipient(profile.Userkey) // Create new message title := renderMessage(config.ConfigData.Alerts.General.Title, event, "title", "Pushover") notif := &pushover.Message{ Message: message, Title: title, - Priority: config.ConfigData.Alerts.Pushover.Priority, - Sound: config.ConfigData.Alerts.Pushover.Sound, + Priority: profile.Priority, + Sound: profile.Sound, HTML: true, - TTL: time.Duration(config.ConfigData.Alerts.Pushover.TTL) * time.Second, + TTL: time.Duration(profile.TTL) * time.Second, } // If emergency priority, set retry / expiration if notif.Priority == 2 { - notif.Retry = time.Duration(config.ConfigData.Alerts.Pushover.Retry) * time.Second - notif.Expire = time.Duration(config.ConfigData.Alerts.Pushover.Expire) * time.Second + notif.Retry = time.Duration(profile.Retry) * time.Second + notif.Expire = time.Duration(profile.Expire) * time.Second } // Add target devices if specified - if config.ConfigData.Alerts.Pushover.Devices != "" { - devices := strings.ReplaceAll(config.ConfigData.Alerts.Pushover.Devices, " ", "") + if profile.Devices != "" { + devices := strings.ReplaceAll(profile.Devices, " ", "") notif.DeviceName = devices } log.Trace(). Interface("payload", notif). Interface("recipient", "--secret removed--"). + Int("provider_id", provider.index). Msg("Send Pushover alert") // Send notification @@ -60,11 +63,13 @@ func SendPushoverMessage(event models.Event, snapshot io.Reader) { response, err := push.SendMessage(notif, recipient) log.Trace(). Interface("payload", response). + Int("provider_id", provider.index). Msg("Pushover response") if err != nil { log.Warn(). Str("event_id", event.ID). Str("provider", "Pushover"). + Int("provider_id", provider.index). Msgf("Unable to send alert: %v", err) config.Internal.Status.Notifications.Pushover[0].NotifFailure(err.Error()) return @@ -73,11 +78,13 @@ func SendPushoverMessage(event models.Event, snapshot io.Reader) { response, err := push.SendMessage(notif, recipient) log.Trace(). Interface("payload", response). + Int("provider_id", provider.index). Msg("Pushover response") if err != nil { log.Warn(). Str("event_id", event.ID). Str("provider", "Pushover"). + Int("provider_id", provider.index). Msgf("Unable to send alert: %v", err) config.Internal.Status.Notifications.Pushover[0].NotifFailure(err.Error()) return @@ -87,6 +94,7 @@ func SendPushoverMessage(event models.Event, snapshot io.Reader) { log.Info(). Str("event_id", event.ID). Str("provider", "Pushover"). + Int("provider_id", provider.index). Msgf("Alert sent") config.Internal.Status.Notifications.Pushover[0].NotifSuccess() diff --git a/notifier/smtp.go b/notifier/smtp.go index 8b5b2f1..e0ae2be 100644 --- a/notifier/smtp.go +++ b/notifier/smtp.go @@ -14,19 +14,21 @@ import ( ) // SendSMTP forwards alert data via email -func SendSMTP(event models.Event, snapshot io.Reader) { +func SendSMTP(event models.Event, snapshot io.Reader, provider notifMeta) { + profile := config.ConfigData.Alerts.SMTP[provider.index] + // Build notification var message string - if config.ConfigData.Alerts.SMTP.Template != "" { - message = renderMessage(config.ConfigData.Alerts.SMTP.Template, event, "message", "SMTP") + if profile.Template != "" { + message = renderMessage(profile.Template, event, "message", "SMTP") } else { message = renderMessage("html", event, "message", "SMTP") } // Set up email alert m := mail.NewMsg() - m.From(config.ConfigData.Alerts.SMTP.From) - m.To(ParseSMTPRecipients()...) + m.From(profile.From) + m.To(ParseSMTPRecipients(profile.Recipient)...) title := renderMessage(config.ConfigData.Alerts.General.Title, event, "title", "SMTP") m.Subject(title) // Attach snapshot if one exists @@ -40,19 +42,19 @@ func SendSMTP(event models.Event, snapshot io.Reader) { time.Sleep(5 * time.Second) // Set up SMTP Connection - c, err := mail.NewClient(config.ConfigData.Alerts.SMTP.Server, mail.WithPort(config.ConfigData.Alerts.SMTP.Port)) + c, err := mail.NewClient(profile.Server, mail.WithPort(profile.Port)) // Add authentication params if needed - if config.ConfigData.Alerts.SMTP.User != "" && config.ConfigData.Alerts.SMTP.Password != "" { + if profile.User != "" && profile.Password != "" { c.SetSMTPAuth(mail.SMTPAuthPlain) - c.SetUsername(config.ConfigData.Alerts.SMTP.User) - c.SetPassword(config.ConfigData.Alerts.SMTP.Password) + c.SetUsername(profile.User) + c.SetPassword(profile.Password) } // Mandatory TLS is enabled by default, so disable TLS if config flag is set - if !config.ConfigData.Alerts.SMTP.TLS { + if !profile.TLS { c.SetTLSPolicy(mail.NoTLS) } // Disable certificate verification if needed - if config.ConfigData.Alerts.SMTP.Insecure { + if profile.Insecure { c.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } @@ -61,6 +63,7 @@ func SendSMTP(event models.Event, snapshot io.Reader) { Str("event_id", event.ID). Str("provider", "SMTP"). Err(err). + Int("provider_id", provider.index). Msg("Unable to send alert") config.Internal.Status.Notifications.SMTP[0].NotifFailure(err.Error()) } @@ -70,11 +73,12 @@ func SendSMTP(event models.Event, snapshot io.Reader) { Strs("recipients", m.GetToString()). Str("subject", title). Interface("payload", message). - Str("server", config.ConfigData.Alerts.SMTP.Server). - Int("port", config.ConfigData.Alerts.SMTP.Port). - Bool("tls", config.ConfigData.Alerts.SMTP.TLS). - Str("username", config.ConfigData.Alerts.SMTP.User). + Str("server", profile.Server). + Int("port", profile.Port). + Bool("tls", profile.TLS). + Str("username", profile.User). Str("password", "--secret removed--"). + Int("provider_id", provider.index). Msg("Send SMTP Alert") // Send message @@ -82,6 +86,7 @@ func SendSMTP(event models.Event, snapshot io.Reader) { log.Warn(). Str("event_id", event.ID). Str("provider", "SMTP"). + Int("provider_id", provider.index). Err(err). Msg("Unable to send alert") config.Internal.Status.Notifications.SMTP[0].NotifFailure(err.Error()) @@ -90,14 +95,15 @@ func SendSMTP(event models.Event, snapshot io.Reader) { log.Info(). Str("event_id", event.ID). Str("provider", "SMTP"). + Int("provider_id", provider.index). Msg("Alert sent") config.Internal.Status.Notifications.SMTP[0].NotifSuccess() } // ParseSMTPRecipients splits individual email addresses from config file -func ParseSMTPRecipients() []string { +func ParseSMTPRecipients(recipientList string) []string { var recipients []string - list := strings.Split(config.ConfigData.Alerts.SMTP.Recipient, ",") + list := strings.Split(recipientList, ",") for _, addr := range list { recipients = append(recipients, strings.TrimSpace(addr)) } diff --git a/notifier/telegram.go b/notifier/telegram.go index b9a67bd..6294165 100644 --- a/notifier/telegram.go +++ b/notifier/telegram.go @@ -12,21 +12,24 @@ import ( ) // SendTelegramMessage sends alert through Telegram to individual users -func SendTelegramMessage(event models.Event, snapshot io.Reader) { +func SendTelegramMessage(event models.Event, snapshot io.Reader, provider notifMeta) { + profile := config.ConfigData.Alerts.Telegram[provider.index] + // Build notification var message string - if config.ConfigData.Alerts.Telegram.Template != "" { - message = renderMessage(config.ConfigData.Alerts.Telegram.Template, event, "message", "Telegram") + if profile.Template != "" { + message = renderMessage(profile.Template, event, "message", "Telegram") } else { message = renderMessage("html", event, "message", "Telegram") message = strings.ReplaceAll(message, "
", "") } - bot, err := tgbotapi.NewBotAPI(config.ConfigData.Alerts.Telegram.Token) + bot, err := tgbotapi.NewBotAPI(profile.Token) if err != nil { log.Warn(). Str("event_id", event.ID). Str("provider", "Telegram"). + Int("provider_id", provider.index). Err(err). Msg("Unable to send alert") config.Internal.Status.Notifications.Telegram[0].NotifFailure(err.Error()) @@ -36,17 +39,19 @@ func SendTelegramMessage(event models.Event, snapshot io.Reader) { if event.HasSnapshot { // Attach & send snapshot - photo := tgbotapi.NewPhoto(config.ConfigData.Alerts.Telegram.ChatID, tgbotapi.FileReader{Name: "Snapshot", Reader: snapshot}) + photo := tgbotapi.NewPhoto(profile.ChatID, tgbotapi.FileReader{Name: "Snapshot", Reader: snapshot}) photo.Caption = message photo.ParseMode = "HTML" response, err := bot.Send(photo) log.Trace(). Interface("content", response). + Int("provider_id", provider.index). Msg("Send Telegram Alert") if err != nil { log.Warn(). Str("event_id", event.ID). Str("provider", "Telegram"). + Int("provider_id", provider.index). Err(err). Msg("Unable to send alert") config.Internal.Status.Notifications.Telegram[0].NotifFailure(err.Error()) @@ -54,16 +59,18 @@ func SendTelegramMessage(event models.Event, snapshot io.Reader) { } } else { // Send plain text message if no snapshot available - msg := tgbotapi.NewMessage(config.ConfigData.Alerts.Telegram.ChatID, message) + msg := tgbotapi.NewMessage(profile.ChatID, message) msg.ParseMode = "HTML" response, err := bot.Send(msg) log.Trace(). Interface("content", response). + Int("provider_id", provider.index). Msg("Send Telegram Alert") if err != nil { log.Warn(). Str("event_id", event.ID). Str("provider", "Telegram"). + Int("provider_id", provider.index). Err(err). Msg("Unable to send alert") config.Internal.Status.Notifications.Telegram[0].NotifFailure(err.Error()) @@ -73,6 +80,7 @@ func SendTelegramMessage(event models.Event, snapshot io.Reader) { log.Info(). Str("event_id", event.ID). Str("provider", "Telegram"). + Int("provider_id", provider.index). Msg("Alert sent") config.Internal.Status.Notifications.Telegram[0].NotifSuccess() } diff --git a/notifier/webhook.go b/notifier/webhook.go index d197264..6e6da16 100644 --- a/notifier/webhook.go +++ b/notifier/webhook.go @@ -12,14 +12,17 @@ import ( ) // SendWebhook sends alert through HTTP POST to target webhook -func SendWebhook(event models.Event) { +func SendWebhook(event models.Event, provider notifMeta) { + profile := config.ConfigData.Alerts.Webhook[provider.index] + // Build notification var message string - payload, err := json.Marshal(config.ConfigData.Alerts.Webhook.Template) + payload, err := json.Marshal(profile.Template) if err != nil { log.Warn(). Str("event_id", event.ID). Str("provider", "Webhook"). + Int("provider_id", provider.index). Err(err). Msg("Unable to send alert") config.Internal.Status.Notifications.Webhook[0].NotifFailure(err.Error()) @@ -31,20 +34,21 @@ func SendWebhook(event models.Event) { message = renderMessage("json", event, "message", "Webhook") } - headers := renderHTTPKV(config.ConfigData.Alerts.Webhook.Headers, event, "headers", "Webhook") - params := renderHTTPKV(config.ConfigData.Alerts.Webhook.Params, event, "params", "Webhook") + headers := renderHTTPKV(profile.Headers, event, "headers", "Webhook") + params := renderHTTPKV(profile.Params, event, "params", "Webhook") paramString := util.BuildHTTPParams(params...) - if strings.ToUpper(config.ConfigData.Alerts.Webhook.Method) == "GET" { - _, err = util.HTTPGet(config.ConfigData.Alerts.Webhook.Server, config.ConfigData.Alerts.Webhook.Insecure, paramString, headers...) + if strings.ToUpper(profile.Method) == "GET" { + _, err = util.HTTPGet(profile.Server, profile.Insecure, paramString, headers...) } else { - _, err = util.HTTPPost(config.ConfigData.Alerts.Webhook.Server, config.ConfigData.Alerts.Webhook.Insecure, []byte(message), paramString, headers...) + _, err = util.HTTPPost(profile.Server, profile.Insecure, []byte(message), paramString, headers...) } if err != nil { log.Warn(). Str("event_id", event.ID). Str("provider", "Webhook"). + Int("provider_id", provider.index). Err(err). Msg("Unable to send alert") config.Internal.Status.Notifications.Webhook[0].NotifFailure(err.Error()) @@ -53,6 +57,7 @@ func SendWebhook(event models.Event) { log.Info(). Str("event_id", event.ID). Str("provider", "Webhook"). + Int("provider_id", provider.index). Msg("Alert sent") config.Internal.Status.Notifications.Webhook[0].NotifSuccess() }