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
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-<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"
)
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)
} // }}}

View File

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

View File

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

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.
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(

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