Repository: TwiN/gatus

Issue:
Support Devices for Pushover Alerts
### Describe the feature request

The current Pushover provider does not allow targeting a specific [device](https://pushover.net/api#identifiers)—Pushover itself supports this via an optional device parameter.

### Why do you personally want this feature to be implemented?

_No response_

### How long have you been using this project?

_No response_

### Additional information

_No response_

Discussion:

Solution patch:
diff --git a/README.md b/README.md
index 282e6e2f..955bc5c9 100644
--- a/README.md
+++ b/README.md
@@ -1229,17 +1229,18 @@ endpoints:
 
 
 #### Configuring Pushover alerts
-| Parameter                             | Description                                                                                     | Default                      |
-|:--------------------------------------|:------------------------------------------------------------------------------------------------|:-----------------------------|
-| `alerting.pushover`                   | Configuration for alerts of type `pushover`                                                     | `{}`                         |
-| `alerting.pushover.application-token` | Pushover application token                                                                      | `""`                         |
-| `alerting.pushover.user-key`          | User or group key                                                                               | `""`                         |
-| `alerting.pushover.title`             | Fixed title for all messages sent via Pushover                                                  | `"Gatus: <endpoint>"` |
-| `alerting.pushover.priority`          | Priority of all messages, ranging from -2 (very low) to 2 (emergency)                           | `0`                          |
-| `alerting.pushover.resolved-priority` | Override the priority of messages on resolved, ranging from -2 (very low) to 2 (emergency)      | `0`                          |
-| `alerting.pushover.sound`             | Sound of all messages<br />See [sounds](https://pushover.net/api#sounds) for all valid choices. | `""`                         |
-| `alerting.pushover.ttl`               | Set the Time-to-live of the message to be automatically deleted from pushover notifications     | `0`                          |
-| `alerting.pushover.default-alert`     | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert)      | N/A                          |
+| Parameter                             | Description                                                                                              | Default                     |
+|:--------------------------------------|:---------------------------------------------------------------------------------------------------------|:----------------------------|
+| `alerting.pushover`                   | Configuration for alerts of type `pushover`                                                              | `{}`                        |
+| `alerting.pushover.application-token` | Pushover application token                                                                               | `""`                        |
+| `alerting.pushover.user-key`          | User or group key                                                                                        | `""`                        |
+| `alerting.pushover.title`             | Fixed title for all messages sent via Pushover                                                           | `"Gatus: <endpoint>"`       |
+| `alerting.pushover.priority`          | Priority of all messages, ranging from -2 (very low) to 2 (emergency)                                    | `0`                         |
+| `alerting.pushover.resolved-priority` | Override the priority of messages on resolved, ranging from -2 (very low) to 2 (emergency)               | `0`                         |
+| `alerting.pushover.sound`             | Sound of all messages<br />See [sounds](https://pushover.net/api#sounds) for all valid choices.          | `""`                        |
+| `alerting.pushover.ttl`               | Set the Time-to-live of the message to be automatically deleted from pushover notifications              | `0`                         |
+| `alerting.pushover.device`            | Device to send the message to (optional)<br/>See [devices](https://pushover.net/api#identifiers) for details | `""` (all devices)|
+| `alerting.pushover.default-alert`     | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert)               | N/A                         |
 
 ```yaml
 alerting:
diff --git a/alerting/provider/pushover/pushover.go b/alerting/provider/pushover/pushover.go
index a3935d77..efa3be6d 100644
--- a/alerting/provider/pushover/pushover.go
+++ b/alerting/provider/pushover/pushover.go
@@ -23,6 +23,7 @@ var (
 	ErrInvalidApplicationToken = errors.New("application-token must be 30 characters long")
 	ErrInvalidUserKey          = errors.New("user-key must be 30 characters long")
 	ErrInvalidPriority         = errors.New("priority and resolved-priority must be between -2 and 2")
+	ErrInvalidDevice           = errors.New("device name must have 25 characters or less")
 )
 
 type Config struct {
@@ -53,6 +54,10 @@ type Config struct {
 	// If priority is 2 then this parameter is ignored
 	// default: 0
 	TTL int `yaml:"ttl,omitempty"`
+
+	// Device to send the message to (see: https://pushover.net/api#devices)
+	// default: "" (all devices)
+	Device string `yaml:"device,omitempty"`
 }
 
 func (cfg *Config) Validate() error {
@@ -71,6 +76,9 @@ func (cfg *Config) Validate() error {
 	if cfg.Priority < -2 || cfg.Priority > 2 || cfg.ResolvedPriority < -2 || cfg.ResolvedPriority > 2 {
 		return ErrInvalidPriority
 	}
+    if len(cfg.Device) > 25 {
+        return ErrInvalidDevice
+    }
 	return nil
 }
 
@@ -96,6 +104,9 @@ func (cfg *Config) Merge(override *Config) {
 	if override.TTL > 0 {
 		cfg.TTL = override.TTL
 	}
+    if len(override.Device) > 0 {
+        cfg.Device = override.Device
+    }
 }
 
 // AlertProvider is the configuration necessary for sending an alert using Pushover
@@ -145,6 +156,7 @@ type Body struct {
 	Html     int    `json:"html"`
 	Sound    string `json:"sound,omitempty"`
 	TTL      int    `json:"ttl,omitempty"`
+	Device   string `json:"device,omitempty"`
 }
 
 // buildRequestBody builds the request body for the provider
@@ -183,6 +195,7 @@ func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoi
 		Html:     1,
 		Sound:    cfg.Sound,
 		TTL:      cfg.TTL,
+		Device:   cfg.Device,
 	})
 	return body
 }



Test patch:
diff --git a/alerting/provider/pushover/pushover_test.go b/alerting/provider/pushover/pushover_test.go
index 7131d91b..4ced2e89 100644
--- a/alerting/provider/pushover/pushover_test.go
+++ b/alerting/provider/pushover/pushover_test.go
@@ -176,6 +176,13 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
 			Resolved:     true,
 			ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\\n✅ - [CONNECTED] == true\\n✅ - [STATUS] == 200\",\"priority\":2,\"html\":1,\"ttl\":3600}",
 		},
+        {
+            Name:       "with-device",
+            Provider:   AlertProvider{DefaultConfig: Config{ApplicationToken: "TokenWithLengthOf30Characters2", UserKey: "TokenWithLengthOf30Characters5", Title: "Gatus Notifications", Priority: 2, ResolvedPriority: 2, TTL: 3600, Device: "iphone15pro",}},
+            Alert:      alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
+            Resolved:   true,
+            ExpectedBody: "{\"token\":\"TokenWithLengthOf30Characters2\",\"user\":\"TokenWithLengthOf30Characters5\",\"title\":\"Gatus Notifications\",\"message\":\"An alert for \\u003cb\\u003eendpoint-name\\u003c/b\\u003e has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\\n✅ - [CONNECTED] == true\\n✅ - [STATUS] == 200\",\"priority\":2,\"html\":1,\"ttl\":3600,\"device\":\"iphone15pro\"}",
+        },
 	}
 	for _, scenario := range scenarios {
 		t.Run(scenario.Name, func(t *testing.T) {