Compare commits

...

5 Commits

Author SHA1 Message Date
Magnus Åhall
1215b13d47 Bumped to v32 2024-07-04 19:48:05 +02:00
Magnus Åhall
e10783ec54 Fixed error message when trying to change prio to a used number. 2024-07-04 19:38:36 +02:00
Magnus Åhall
06f88f697c Added description to Pushover notification service. 2024-07-04 19:37:18 +02:00
Magnus Åhall
414ca0a95c Reprioritize notification services when updated. 2024-07-04 19:36:56 +02:00
Magnus Åhall
257a4968ec Added Pushover notification 2024-07-04 19:21:02 +02:00
8 changed files with 271 additions and 4 deletions

View File

@ -120,7 +120,7 @@ Problems can be acknowledged if they are known problems that will not be current
## Notifications ## 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. \ 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. 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. 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 ## Script
The script service is defined with a filename. 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`. * Add theme to select tag in `/views/pages/configuration.gotmpl`.
* Create `/static/less/theme-<theme-name>.less`. * Create `/static/less/theme-<theme-name>.less`.
* Create `/static/less/<theme-name>.less`. * Create `/static/less/<theme-name>.less`.
* Copy a theme directory under `/static/images/` to the new name. * Copy a theme directory under `/static/images/` to the new name.

View File

@ -29,7 +29,7 @@ import (
"time" "time"
) )
const VERSION = "v31" const VERSION = "v32"
var ( var (
logger *slog.Logger logger *slog.Logger
@ -1211,6 +1211,8 @@ func actionConfigurationNotificationUpdate(w http.ResponseWriter, r *http.Reques
notificationManager.AddService(*svc) notificationManager.AddService(*svc)
} }
notificationManager.Reprioritize()
w.Header().Add("Location", "/configuration") w.Header().Add("Location", "/configuration")
w.WriteHeader(302) w.WriteHeader(302)
} // }}} } // }}}

View File

@ -24,6 +24,16 @@ func ServiceFactory(t string, config []byte, prio int, ackURL string, logger *sl
} }
ntfy.SetLogger(logger) ntfy.SetLogger(logger)
return ntfy, nil 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": case "SCRIPT":
script, err := NewScript(config, prio, ackURL) script, err := NewScript(config, prio, ackURL)
if err != nil { if err != nil {
@ -41,6 +51,8 @@ func NewInstance(typ string) Service {
switch typ { switch typ {
case "NTFY": case "NTFY":
return new(NTFY) return new(NTFY)
case "PUSHOVER":
return new(Pushover)
case "SCRIPT": case "SCRIPT":
return new(Script) return new(Script)
} }

View File

@ -39,6 +39,9 @@ func NewManager(logger *slog.Logger) (nm Manager) {
func (nm *Manager) AddService(service Service) { func (nm *Manager) AddService(service Service) {
service.SetExists(true) service.SetExists(true)
nm.services = append(nm.services, service) nm.services = append(nm.services, service)
}
func (nm *Manager) Reprioritize() {
slices.SortFunc(nm.services, func(a, b Service) int { slices.SortFunc(nm.services, func(a, b Service) int {
if a.GetPrio() < b.GetPrio() { if a.GetPrio() < b.GetPrio() {
return -1 return -1

198
notification/pushover.go Normal file
View File

@ -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
}

View File

@ -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. // Check if this is just a duplicated prio, which isn't allowed.
pgErr, isPgErr := err.(*pq.Error) pgErr, isPgErr := err.(*pq.Error)
if isPgErr && pgErr.Code == "23505" { 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( return false, werr.Wrap(err).WithData(

1
sql/00025.sql Normal file
View File

@ -0,0 +1 @@
ALTER TYPE notification_type ADD VALUE 'PUSHOVER';

View File

@ -0,0 +1,47 @@
{{ define "page" }}
<h1>Pushover</h1>
<style>
.grid {
display: grid;
grid-template-columns: min-content 1fr;
grid-gap: 8px 16px;
align-items: center;
margin-top: 32px;
}
input[type=number] {
width: 64px;
padding: 4px;
}
button {
margin-top: 16px;
}
div.grid > div {
white-space: nowrap;
}
</style>
<form action="/configuration/notification/update/{{ if .Data.Service.Exists }}{{ .Data.Service.GetPrio }}{{ else }}-1{{ end }}" method="post">
<input type="hidden" name="type" value="PUSHOVER">
<div class="grid">
<div>Prio:</div>
<input type="number" min=0 name="prio" value="{{ .Data.Service.GetPrio }}">
<div>Description</div>
<input type="text" name="description" value="{{ .Data.Service.Description }}" style="width: 100%">
<div>User key: <span class="error">*</span></div>
<input type="text" name="user_key" value="{{ .Data.Service.UserKey }}" style="width: 100%">
<div>API (application) key: <span class="error">*</span></div>
<input type="text" name="api_key" value="{{ .Data.Service.APIKey }}" style="width: 100%">
<div>Device name:</div>
<input type="text" name="device_name" value="{{ .Data.Service.DeviceName }}" style="width: 100%">
<button style="grid-column: 1 / -1; width: min-content;">OK</button>
</div>
</form>
{{ end }}