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/main.go b/main.go index dfca0a2..3a93ed8 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,7 @@ import ( "time" ) -const VERSION = "v31" +const VERSION = "v32" var ( logger *slog.Logger @@ -1211,6 +1211,8 @@ func actionConfigurationNotificationUpdate(w http.ResponseWriter, r *http.Reques notificationManager.AddService(*svc) } + notificationManager.Reprioritize() + w.Header().Add("Location", "/configuration") w.WriteHeader(302) } // }}} 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/pkg.go b/notification/pkg.go index efa6712..948d7b3 100644 --- a/notification/pkg.go +++ b/notification/pkg.go @@ -39,6 +39,9 @@ func NewManager(logger *slog.Logger) (nm Manager) { func (nm *Manager) AddService(service Service) { service.SetExists(true) nm.services = append(nm.services, service) +} + +func (nm *Manager) Reprioritize() { slices.SortFunc(nm.services, func(a, b Service) int { if a.GetPrio() < b.GetPrio() { return -1 diff --git a/notification/pushover.go b/notification/pushover.go new file mode 100644 index 0000000..023ff9d --- /dev/null +++ b/notification/pushover.go @@ -0,0 +1,198 @@ +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 { + Description string + 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 { + if po.Description != "" { + return po.Description + } + + 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 + + // Prio + updated.Prio, err = strconv.Atoi(values.Get("prio")) + if err != nil { + return werr.Wrap(err) + } + + // Description + updated.Description = strings.TrimSpace(values.Get("description")) + + // API (application) key + givenAPIKey := values.Get("api_key") + if strings.TrimSpace(givenAPIKey) == "" { + return werr.New("API key cannot be empty") + } + updated.APIKey = strings.TrimSpace(givenAPIKey) + + // User key + givenUserKey := values.Get("user_key") + if strings.TrimSpace(givenUserKey) == "" { + return werr.New("User key cannot be empty") + } + updated.UserKey = strings.TrimSpace(givenUserKey) + + // Device name + 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.Description = updatedPushover.Description + po.APIKey = updatedPushover.APIKey + po.UserKey = updatedPushover.UserKey + po.DeviceName = updatedPushover.DeviceName +} + +func (po Pushover) JSON() []byte { + data := struct { + Description string + APIKey string `json:"api_key"` + UserKey string `json:"user_key"` + DeviceName string `json:"device_name"` + }{ + po.Description, + po.APIKey, + po.UserKey, + po.DeviceName, + } + j, _ := json.Marshal(data) + return j +} diff --git a/notification_manager.go b/notification_manager.go index 1690d8e..28afa62 100644 --- a/notification_manager.go +++ b/notification_manager.go @@ -101,7 +101,7 @@ func UpdateNotificationService(svc notification.Service) (created bool, err erro // Check if this is just a duplicated prio, which isn't allowed. pgErr, isPgErr := err.(*pq.Error) if isPgErr && pgErr.Code == "23505" { - return false, werr.New("Prio %d is already used by another service", svc.GetPrio()) + return false, werr.New("Prio %d is already used by another service", svc.Updated().GetPrio()) } return false, werr.Wrap(err).WithData( 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..f4dd438 --- /dev/null +++ b/views/pages/notification/pushover.gotmpl @@ -0,0 +1,47 @@ +{{ define "page" }} +

Pushover

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