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
|
||||
|
||||
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.
|
||||
|
4
main.go
4
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)
|
||||
} // }}}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
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.
|
||||
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
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