diff --git a/README.md b/README.md
index bd78b4ec..2bcc324b 100644
--- a/README.md
+++ b/README.md
@@ -983,14 +983,18 @@ endpoints:
#### Configuring Ntfy alerts
-| Parameter | Description | Default |
-|:------------------------------|:-------------------------------------------------------------------------------------------|:------------------|
-| `alerting.ntfy` | Configuration for alerts of type `ntfy` | `{}` |
-| `alerting.ntfy.topic` | Topic at which the alert will be sent | Required `""` |
-| `alerting.ntfy.url` | The URL of the target server | `https://ntfy.sh` |
-| `alerting.ntfy.token` | [Access token](https://docs.ntfy.sh/publish/#access-tokens) for restricted topics | `""` |
-| `alerting.ntfy.priority` | The priority of the alert | `3` |
-| `alerting.ntfy.default-alert` | Default alert configuration.
See [Setting a default alert](#setting-a-default-alert) | N/A |
+| Parameter | Description | Default |
+|:---------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------|:------------------|
+| `alerting.ntfy` | Configuration for alerts of type `ntfy` | `{}` |
+| `alerting.ntfy.topic` | Topic at which the alert will be sent | Required `""` |
+| `alerting.ntfy.url` | The URL of the target server | `https://ntfy.sh` |
+| `alerting.ntfy.token` | [Access token](https://docs.ntfy.sh/publish/#access-tokens) for restricted topics | `""` |
+| `alerting.ntfy.email` | E-mail address for additional e-mail notifications | `""` |
+| `alerting.ntfy.click` | Website opened when notification is clicked | `""` |
+| `alerting.ntfy.priority` | The priority of the alert | `3` |
+| `alerting.ntfy.disable-firebase` | Whether message push delivery via firebase should be disabled. [ntfy.sh defaults to enabled](https://docs.ntfy.sh/publish/#disable-firebase) | `false` |
+| `alerting.ntfy.disable-cache` | Whether server side message caching should be disabled. [ntfy.sh defaults to enabled](https://docs.ntfy.sh/publish/#message-caching) | `false` |
+| `alerting.ntfy.default-alert` | Default alert configuration.
See [Setting a default alert](#setting-a-default-alert) | N/A |
[ntfy](https://github.com/binwiederhier/ntfy) is an amazing project that allows you to subscribe to desktop
and mobile notifications, making it an awesome addition to Gatus.
diff --git a/alerting/provider/ntfy/ntfy.go b/alerting/provider/ntfy/ntfy.go
index 1530c10f..784caf39 100644
--- a/alerting/provider/ntfy/ntfy.go
+++ b/alerting/provider/ntfy/ntfy.go
@@ -21,10 +21,14 @@ const (
// AlertProvider is the configuration necessary for sending an alert using Slack
type AlertProvider struct {
- Topic string `yaml:"topic"`
- URL string `yaml:"url,omitempty"` // Defaults to DefaultURL
- Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority
- Token string `yaml:"token,omitempty"` // Defaults to ""
+ Topic string `yaml:"topic"`
+ URL string `yaml:"url,omitempty"` // Defaults to DefaultURL
+ Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority
+ Token string `yaml:"token,omitempty"` // Defaults to ""
+ Email string `yaml:"email,omitempty"` // Defaults to ""
+ Click string `yaml:"click,omitempty"` // Defaults to ""
+ DisableFirebase bool `yaml:"disable-firebase,omitempty"` // Defaults to false
+ DisableCache bool `yaml:"disable-cache,omitempty"` // Defaults to false
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
@@ -56,6 +60,12 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
if len(provider.Token) > 0 {
request.Header.Set("Authorization", "Bearer "+provider.Token)
}
+ if provider.DisableFirebase {
+ request.Header.Set("Firebase", "no")
+ }
+ if provider.DisableCache {
+ request.Header.Set("Cache", "no")
+ }
response, err := client.GetHTTPClient(nil).Do(request)
if err != nil {
return err
@@ -74,6 +84,8 @@ type Body struct {
Message string `json:"message"`
Tags []string `json:"tags"`
Priority int `json:"priority"`
+ Email string `json:"email,omitempty"`
+ Click string `json:"click,omitempty"`
}
// buildRequestBody builds the request body for the provider
@@ -105,6 +117,8 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
Message: message,
Tags: []string{tag},
Priority: provider.Priority,
+ Email: provider.Email,
+ Click: provider.Click,
})
return body
}
diff --git a/alerting/provider/ntfy/ntfy_test.go b/alerting/provider/ntfy/ntfy_test.go
index 3ca3e59d..452533f4 100644
--- a/alerting/provider/ntfy/ntfy_test.go
+++ b/alerting/provider/ntfy/ntfy_test.go
@@ -2,6 +2,9 @@ package ntfy
import (
"encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
"testing"
"github.com/TwiN/gatus/v5/alerting/alert"
@@ -88,6 +91,20 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Resolved: true,
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\n🟢 [CONNECTED] == true\n🟢 [STATUS] == 200","tags":["white_check_mark"],"priority":2}`,
},
+ {
+ Name: "triggered-email",
+ Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com"},
+ Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: false,
+ ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
+ },
+ {
+ Name: "resolved-email",
+ Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 2, Email: "test@example.com", Click: "example.com"},
+ Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: true,
+ ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\n🟢 [CONNECTED] == true\n🟢 [STATUS] == 200","tags":["white_check_mark"],"priority":2,"email":"test@example.com","click":"example.com"}`,
+ },
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
@@ -112,3 +129,99 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
})
}
}
+
+func TestAlertProvider_Send(t *testing.T) {
+ description := "description-1"
+ scenarios := []struct {
+ Name string
+ Provider AlertProvider
+ Alert alert.Alert
+ Resolved bool
+ ExpectedBody string
+ ExpectedHeaders map[string]string
+ }{
+ {
+ Name: "triggered",
+ Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com"},
+ Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: false,
+ ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
+ ExpectedHeaders: map[string]string{
+ "Content-Type": "application/json",
+ },
+ },
+ {
+ Name: "no firebase",
+ Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableFirebase: true},
+ Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: false,
+ ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
+ ExpectedHeaders: map[string]string{
+ "Content-Type": "application/json",
+ "Firebase": "no",
+ },
+ },
+ {
+ Name: "no cache",
+ Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableCache: true},
+ Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: false,
+ ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
+ ExpectedHeaders: map[string]string{
+ "Content-Type": "application/json",
+ "Cache": "no",
+ },
+ },
+ {
+ Name: "neither firebase & cache",
+ Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableFirebase: true, DisableCache: true},
+ Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: false,
+ ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
+ ExpectedHeaders: map[string]string{
+ "Content-Type": "application/json",
+ "Firebase": "no",
+ "Cache": "no",
+ },
+ },
+ }
+ for _, scenario := range scenarios {
+ t.Run(scenario.Name, func(t *testing.T) {
+ // Start a local HTTP server
+ server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ // Test request parameters
+ for header, value := range scenario.ExpectedHeaders {
+ if value != req.Header.Get(header) {
+ t.Errorf("expected: %s, got: %s", value, req.Header.Get(header))
+ }
+ }
+ body, _ := io.ReadAll(req.Body)
+ if string(body) != scenario.ExpectedBody {
+ t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
+ }
+ // Send response to be tested
+ rw.Write([]byte(`OK`))
+ }))
+ // Close the server when test finishes
+ defer server.Close()
+
+ scenario.Provider.URL = server.URL
+ err := scenario.Provider.Send(
+ &endpoint.Endpoint{Name: "endpoint-name"},
+ &scenario.Alert,
+ &endpoint.Result{
+ ConditionResults: []*endpoint.ConditionResult{
+ {Condition: "[CONNECTED] == true", Success: scenario.Resolved},
+ {Condition: "[STATUS] == 200", Success: scenario.Resolved},
+ },
+ },
+ scenario.Resolved,
+ )
+ if err != nil {
+ t.Error("Encountered an error on Send: ", err)
+ }
+
+ })
+ }
+
+}