switch to http client for pushover notifier

This commit is contained in:
CrazyMax
2025-08-31 15:11:33 +02:00
parent 5f67b0d8ea
commit cf2edc7db2
20 changed files with 105 additions and 1114 deletions

View File

@@ -160,6 +160,7 @@ for <code>{{ .Entry.Manifest.Platform }}</code> platform.
Pushover: &model.NotifPushover{
Token: "uQiRzpo4DXghDmr9QzzfQu27cmVRsG",
Recipient: "gznej3rKEVAvPUxu9vvNnqpmZpokzF",
Timeout: utl.NewDuration(10 * time.Second),
TemplateTitle: model.NotifDefaultTemplateTitle,
TemplateBody: model.NotifDefaultTemplateBody,
},

View File

@@ -1,15 +1,22 @@
package model
import (
"time"
"github.com/crazy-max/diun/v4/pkg/utl"
)
// NotifPushover holds Pushover notification configuration details
type NotifPushover struct {
Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"`
TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"`
Recipient string `yaml:"recipient,omitempty" json:"recipient,omitempty" validate:"omitempty"`
RecipientFile string `yaml:"recipientFile,omitempty" json:"recipientFile,omitempty" validate:"omitempty,file"`
Priority int `yaml:"priority,omitempty" json:"priority,omitempty" validate:"omitempty,min=-2,max=2"`
Sound string `yaml:"sound,omitempty" json:"sound,omitempty" validate:"omitempty"`
TemplateTitle string `yaml:"templateTitle,omitempty" json:"templateTitle,omitempty" validate:"required"`
TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"`
Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"`
TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"`
Recipient string `yaml:"recipient,omitempty" json:"recipient,omitempty" validate:"omitempty"`
RecipientFile string `yaml:"recipientFile,omitempty" json:"recipientFile,omitempty" validate:"omitempty,file"`
Priority int `yaml:"priority,omitempty" json:"priority,omitempty" validate:"omitempty,min=-2,max=2"`
Sound string `yaml:"sound,omitempty" json:"sound,omitempty" validate:"omitempty"`
Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"`
TemplateTitle string `yaml:"templateTitle,omitempty" json:"templateTitle,omitempty" validate:"required"`
TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"`
}
// GetDefaults gets the default values
@@ -21,6 +28,7 @@ func (s *NotifPushover) GetDefaults() *NotifPushover {
// SetDefaults sets the default values
func (s *NotifPushover) SetDefaults() {
s.Timeout = utl.NewDuration(10 * time.Second)
s.TemplateTitle = NotifDefaultTemplateTitle
s.TemplateBody = NotifDefaultTemplateBody
}

View File

@@ -1,16 +1,24 @@
package pushover
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/crazy-max/diun/v4/internal/model"
"github.com/crazy-max/diun/v4/internal/msg"
"github.com/crazy-max/diun/v4/internal/notif/notifier"
"github.com/crazy-max/diun/v4/pkg/utl"
"github.com/gregdel/pushover"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
const pushoverAPIURL = "https://api.pushover.net/1/messages.json"
// Client represents an active Pushover notification object
type Client struct {
*notifier.Notifier
@@ -38,11 +46,15 @@ func (c *Client) Send(entry model.NotifEntry) error {
token, err := utl.GetSecret(c.cfg.Token, c.cfg.TokenFile)
if err != nil {
return errors.Wrap(err, "cannot retrieve token secret for Pushover notifier")
} else if token == "" {
return errors.New("Pushover API token cannot be empty")
}
recipient, err := utl.GetSecret(c.cfg.Recipient, c.cfg.RecipientFile)
if err != nil {
return errors.Wrap(err, "cannot retrieve recipient secret for Pushover notifier")
} else if recipient == "" {
return errors.New("Pushover recipient cannot be empty")
}
message, err := msg.New(msg.Options{
@@ -60,16 +72,77 @@ func (c *Client) Send(entry model.NotifEntry) error {
return err
}
_, err = pushover.New(token).SendMessage(&pushover.Message{
Title: string(title),
Message: string(body),
Priority: c.cfg.Priority,
Sound: c.cfg.Sound,
URL: c.meta.URL,
URLTitle: c.meta.Name,
Timestamp: time.Now().Unix(),
HTML: true,
}, pushover.NewRecipient(recipient))
cancelCtx, cancel := context.WithCancelCause(context.Background())
timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, *c.cfg.Timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent
defer func() { cancel(errors.WithStack(context.Canceled)) }()
return err
form := url.Values{}
form.Add("token", token)
form.Add("user", recipient)
form.Add("title", string(title))
form.Add("message", string(body))
form.Add("priority", strconv.Itoa(c.cfg.Priority))
if c.cfg.Sound != "" {
form.Add("sound", c.cfg.Sound)
}
if c.meta.URL != "" {
form.Add("url", c.meta.URL)
}
if c.meta.Name != "" {
form.Add("url_title", c.meta.Name)
}
form.Add("timestamp", strconv.FormatInt(time.Now().Unix(), 10))
form.Add("html", "1")
hc := http.Client{}
req, err := http.NewRequestWithContext(timeoutCtx, "POST", pushoverAPIURL, strings.NewReader(form.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", c.meta.UserAgent)
resp, err := hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Header != nil {
var appLimit, appRemaining int
var appReset time.Time
if limit := resp.Header.Get("X-Limit-App-Limit"); limit != "" {
if i, err := strconv.Atoi(limit); err == nil {
appLimit = i
}
}
if remaining := resp.Header.Get("X-Limit-App-Remaining"); remaining != "" {
if i, err := strconv.Atoi(remaining); err == nil {
appRemaining = i
}
}
if reset := resp.Header.Get("X-Limit-App-Reset"); reset != "" {
if i, err := strconv.Atoi(reset); err == nil {
appReset = time.Unix(int64(i), 0)
}
}
log.Debug().Msgf("Pushover app limit: %d, remaining: %d, reset: %s", appLimit, appRemaining, appReset)
}
var respBody struct {
Status int `json:"status"`
Request string `json:"request"`
Errors []string `json:"errors"`
User string `json:"user"`
Token string `json:"token"`
}
if err = json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
return errors.Wrapf(err, "cannot decode JSON body response for HTTP %d %s status: %+v", resp.StatusCode, http.StatusText(resp.StatusCode), respBody)
}
if respBody.Status != 1 {
return errors.Errorf("Pushover API call failed with status %d: %v", respBody.Status, respBody.Errors)
}
return nil
}