graphicek/server/integrations/ntfy.go

159 lines
3.8 KiB
Go

package integrations
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
type NtfyIntegration struct{}
var ntfyHTTPClient = &http.Client{
Timeout: 15 * time.Second,
}
type NtfyNotificationChannelConfig struct {
Endpoint string `json:"endpoint"`
AuthToken string `json:"authToken"`
}
func (s NtfyIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig string) error {
config := NtfyNotificationChannelConfig{}
err := json.Unmarshal([]byte(rawConfig), &config)
if err != nil {
return fmt.Errorf("failed to parse ntfy integration config - %w", err)
}
if err := s.validate(config); err != nil {
return err
}
switch evt.Type {
case ContactPointEventAlertTriggered:
data := evt.AlertTriggeredEvent
if data.SensorValueCondition != nil {
text := fmt.Sprintf("%s is at {value}", data.Sensor.Name)
if data.Alert.CustomMessage != "" {
text = data.Alert.CustomMessage
}
text = strings.Replace(text, "{value}", strconv.FormatFloat(data.LastValue, 'f', -1, 64), -1)
return s.send(config, "Alert Triggered", text)
}
if data.SensorLastContactCondition != nil {
text := fmt.Sprintf(
"%s has not reported in for last %s %s",
data.Sensor.Name,
strconv.FormatFloat(data.SensorLastContactCondition.Value, 'f', -1, 64),
data.SensorLastContactCondition.ValueUnit,
)
if data.Alert.CustomMessage != "" {
text = data.Alert.CustomMessage
}
return s.send(config, "Alert Triggered", text)
}
return nil
case ContactPointEventAlertResolved:
data := evt.AlertResolvedEvent
if data.SensorValueCondition != nil {
text := fmt.Sprintf("%s is at {value}", data.Sensor.Name)
if data.Alert.CustomResolvedMessage != "" {
text = data.Alert.CustomResolvedMessage
}
text = strings.Replace(text, "{value}", strconv.FormatFloat(data.LastValue, 'f', -1, 64), -1)
return s.send(config, "Alert Resolved", text)
}
if data.SensorLastContactCondition != nil {
text := fmt.Sprintf(
"%s has reported in last %s %s",
data.Sensor.Name,
strconv.FormatFloat(data.SensorLastContactCondition.Value, 'f', -1, 64),
data.SensorLastContactCondition.ValueUnit,
)
if data.Alert.CustomResolvedMessage != "" {
text = data.Alert.CustomResolvedMessage
}
return s.send(config, "Alert Resolved", text)
}
return nil
case ContactPointEventTest:
return s.send(config, "Test", "Test message from Basic Sensor Receiver")
}
return nil
}
func (s NtfyIntegration) ValidateConfig(rawConfig string) error {
config := NtfyNotificationChannelConfig{}
err := json.Unmarshal([]byte(rawConfig), &config)
if err != nil {
return err
}
return s.validate(config)
}
func (s NtfyIntegration) validate(config NtfyNotificationChannelConfig) error {
if strings.TrimSpace(config.Endpoint) == "" {
return fmt.Errorf("ntfy endpoint is required")
}
return nil
}
func (s NtfyIntegration) send(config NtfyNotificationChannelConfig, title string, text string) error {
url := strings.TrimSpace(config.Endpoint)
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(text))
if err != nil {
return fmt.Errorf("failed to create ntfy request - %w", err)
}
req.Header.Set("Content-Type", "text/plain; charset=utf-8")
req.Header.Set("Title", title)
if config.AuthToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", config.AuthToken))
}
resp, err := ntfyHTTPClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send ntfy notification - %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
return fmt.Errorf("ntfy returned status %d", resp.StatusCode)
}
return fmt.Errorf("ntfy returned status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
return nil
}