From 257a4968ec17889c582742843bba21ed000bfc55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 4 Jul 2024 19:21:02 +0200 Subject: [PATCH] Added Pushover notification --- README.md | 8 +- notification/factory.go | 12 ++ notification/pushover.go | 183 +++++++++++++++++++++++ sql/00025.sql | 1 + views/pages/notification/pushover.gotmpl | 44 ++++++ 5 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 notification/pushover.go create mode 100644 sql/00025.sql create mode 100644 views/pages/notification/pushover.gotmpl diff --git a/README.md b/README.md index b922964..193705b 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Problems can be acknowledged if they are known problems that will not be current ## Notifications -Smon has a couple of notification services (currently [[https://ntfy.sh|NTFY]] and script). +Smon has a couple of notification services (currently [[https://ntfy.sh|NTFY]], [[https://pushover.net|Pushover]] and script). Services are added with a prio. The service with the lowest prio is tried first. \ If sending through the service fails, the next service is tried and so on until one succeeds. @@ -134,6 +134,10 @@ What is sent isn't configurable (for now). What is sent is: An URL is provided to the topic which should receive the notifications. +## Pushover + +The user key and API key for the Pushover application is needed. Additionally, a device key can be specified as well. + ## Script The script service is defined with a filename. @@ -161,4 +165,4 @@ A couple of small notes on development. * Add theme to select tag in `/views/pages/configuration.gotmpl`. * Create `/static/less/theme-.less`. * Create `/static/less/.less`. -* Copy a theme directory under `/static/images/` to the new name. \ No newline at end of file +* Copy a theme directory under `/static/images/` to the new name. diff --git a/notification/factory.go b/notification/factory.go index 16c35d4..1506b43 100644 --- a/notification/factory.go +++ b/notification/factory.go @@ -24,6 +24,16 @@ func ServiceFactory(t string, config []byte, prio int, ackURL string, logger *sl } ntfy.SetLogger(logger) return ntfy, nil + + case "PUSHOVER": + pushover, err := NewPushover(config, prio, ackURL) + if err != nil { + err = werr.Wrap(err).WithData(config) + return nil, err + } + pushover.SetLogger(logger) + return pushover, nil + case "SCRIPT": script, err := NewScript(config, prio, ackURL) if err != nil { @@ -41,6 +51,8 @@ func NewInstance(typ string) Service { switch typ { case "NTFY": return new(NTFY) + case "PUSHOVER": + return new(Pushover) case "SCRIPT": return new(Script) } diff --git a/notification/pushover.go b/notification/pushover.go new file mode 100644 index 0000000..9b35232 --- /dev/null +++ b/notification/pushover.go @@ -0,0 +1,183 @@ +package notification + +import ( + // External + werr "git.gibonuddevalla.se/go/wrappederror" + + // Standard + "bytes" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "net/url" + "strconv" + "strings" +) + +type Pushover struct { + UserKey string `json:"user_key"` + APIKey string `json:"api_key"` + DeviceName string `json:"device_name"` + + Prio int + AcknowledgeURL string + logger *slog.Logger + + exists bool + updated Service +} + +func init() { + allServices = append(allServices, &Pushover{}) +} + +func NewPushover(config []byte, prio int, ackURL string) (instance *Pushover, err error) { + instance = new(Pushover) + err = json.Unmarshal(config, &instance) + if err != nil { + err = werr.Wrap(err).WithData(config) + return + } + instance.Prio = prio + instance.AcknowledgeURL = ackURL + return instance, nil +} + +func (po *Pushover) SetLogger(l *slog.Logger) { + po.logger = l +} + +func (po *Pushover) GetType() string { + return "PUSHOVER" +} + +func (po *Pushover) GetPrio() int { + return po.Prio +} + +func (po *Pushover) SetPrio(prio int) { + po.Prio = prio +} + +func (po *Pushover) SetExists(exists bool) { + po.exists = exists +} + +func (po Pushover) Exists() bool { + return po.exists +} + +func (po *Pushover) String() string { + return fmt.Sprintf("%s, %s", po.UserKey, po.APIKey) +} + +func (po Pushover) Send(problemID int, msg []byte) (err error) { + var req *http.Request + var res *http.Response + + pushoverRequest, _ := json.Marshal(map[string]string{ + "token": po.APIKey, + "user": po.UserKey, + "device": po.DeviceName, + "message": string(msg), + }) + + req, err = http.NewRequest("POST", "https://api.pushover.net/1/messages.json", bytes.NewReader(pushoverRequest)) + if err != nil { + err = werr.Wrap(err).WithData(struct { + UserKey string + APIKey string + Msg []byte + }{ + po.UserKey, + po.APIKey, + msg, + }, + ) + return + } + + //ackURL := fmt.Sprintf("http, OK, %s/notification/ack?problemID=%d", po.AcknowledgeURL, problemID) + req.Header.Add("Content-Type", "application/json") + res, err = http.DefaultClient.Do(req) + if err != nil { + err = werr.Wrap(err) + return + } + + body, _ := io.ReadAll(res.Body) + poResp := struct { + Status int + Errors []string + }{} + err = json.Unmarshal(body, &poResp) + if err != nil { + err = werr.Wrap(err).WithData(body) + return + } + + if poResp.Status != 1 { + err = werr.New("%s", strings.Join(poResp.Errors, ", ")) + return + } + + if res.StatusCode != 200 { + err = werr.New("Invalid Pushover response").WithData(body) + return + } + + return +} + +func (po *Pushover) Update(values url.Values) (err error) { + updated := Pushover{} + po.updated = &updated + + updated.Prio, err = strconv.Atoi(values.Get("prio")) + if err != nil { + return werr.Wrap(err) + } + + givenAPIKey := values.Get("api_key") + if strings.TrimSpace(givenAPIKey) == "" { + return werr.New("API key cannot be empty") + } + updated.APIKey = strings.TrimSpace(givenAPIKey) + + givenUserKey := values.Get("user_key") + if strings.TrimSpace(givenUserKey) == "" { + return werr.New("User key cannot be empty") + } + updated.UserKey = strings.TrimSpace(givenUserKey) + + updated.DeviceName = strings.TrimSpace(values.Get("device_name")) + return +} + +func (po *Pushover) Updated() Service { + return po.updated +} + +func (po *Pushover) Commit() { + updatedPushover := po.updated.(*Pushover) + po.Prio = updatedPushover.Prio + po.APIKey = updatedPushover.APIKey + po.UserKey = updatedPushover.UserKey + po.DeviceName = updatedPushover.DeviceName +} + +func (po Pushover) JSON() []byte { + data := struct { + APIKey string `json:"api_key"` + UserKey string `json:"user_key"` + DeviceName string `json:"device_name"` + }{ + po.APIKey, + po.UserKey, + po.DeviceName, + } + j, _ := json.Marshal(data) + return j +} diff --git a/sql/00025.sql b/sql/00025.sql new file mode 100644 index 0000000..8981022 --- /dev/null +++ b/sql/00025.sql @@ -0,0 +1 @@ +ALTER TYPE notification_type ADD VALUE 'PUSHOVER'; diff --git a/views/pages/notification/pushover.gotmpl b/views/pages/notification/pushover.gotmpl new file mode 100644 index 0000000..e483b03 --- /dev/null +++ b/views/pages/notification/pushover.gotmpl @@ -0,0 +1,44 @@ +{{ define "page" }} +

Pushover

+ + +
+ +
+
Prio:
+ + +
User key:
+ + +
API (application) key:
+ + +
Device name (optional):
+ + + +
+
+{{ end }}