Compare commits
5 Commits
5d24baedac
...
1215b13d47
Author | SHA1 | Date | |
---|---|---|---|
|
1215b13d47 | ||
|
e10783ec54 | ||
|
06f88f697c | ||
|
414ca0a95c | ||
|
257a4968ec |
@ -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.
|
||||||
|
4
main.go
4
main.go
@ -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)
|
||||||
} // }}}
|
} // }}}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
198
notification/pushover.go
Normal 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
|
||||||
|
}
|
@ -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
1
sql/00025.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE notification_type ADD VALUE 'PUSHOVER';
|
47
views/pages/notification/pushover.gotmpl
Normal file
47
views/pages/notification/pushover.gotmpl
Normal 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 }}
|
Loading…
Reference in New Issue
Block a user