diff --git a/docs/notif/pushover.md b/docs/notif/pushover.md index 03a81d13..6a118661 100644 --- a/docs/notif/pushover.md +++ b/docs/notif/pushover.md @@ -12,6 +12,7 @@ You can send notifications using [Pushover](https://pushover.net/). recipient: gznej3rKEVAvPUxu9vvNnqpmZpokzF priority: -2 sound: none + timeout: 10s templateTitle: "{{ .Entry.Image }} released" templateBody: | Docker tag {{ .Entry.Image }} which you subscribed to through {{ .Entry.Provider }} provider has been released. @@ -25,6 +26,7 @@ You can send notifications using [Pushover](https://pushover.net/). | `recipientFile` | | Use content of secret file as User key if `recipient` not defined | | `priority` | | Priority of the notification | | `sound` | | Notification sound to be used | +| `timeout` | `10s` | Timeout specifies a time limit for the request to be made | | `templateTitle`[^1] | See [below](#default-templatetitle) | [Notification template](../faq.md#notification-template) for message title | | `templateBody`[^1] | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body | @@ -35,6 +37,7 @@ You can send notifications using [Pushover](https://pushover.net/). * `DIUN_NOTIF_PUSHOVER_RECIPIENTFILE` * `DIUN_NOTIF_PUSHOVER_PRIORITY` * `DIUN_NOTIF_PUSHOVER_SOUND` + * `DIUN_NOTIF_PUSHOVER_TIMEOUT` * `DIUN_NOTIF_PUSHOVER_TEMPLATETITLE` * `DIUN_NOTIF_PUSHOVER_TEMPLATEBODY` diff --git a/go.mod b/go.mod index cb57080e..b4acfca3 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,6 @@ require ( github.com/eclipse/paho.mqtt.golang v1.5.0 github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df github.com/go-playground/validator/v10 v10.27.0 - github.com/gregdel/pushover v1.3.1 github.com/hashicorp/nomad/api v0.0.0-20231213195942-64e3dca9274b // v1.7.2 github.com/jedib0t/go-pretty/v6 v6.6.8 github.com/matcornic/hermes/v2 v2.1.0 diff --git a/go.sum b/go.sum index e8d8f71e..68016043 100644 --- a/go.sum +++ b/go.sum @@ -167,8 +167,6 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/gregdel/pushover v1.3.1 h1:4bMLITOZ15+Zpi6qqoGqOPuVHCwSUvMCgVnN5Xhilfo= -github.com/gregdel/pushover v1.3.1/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 1a282697..4f950896 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -160,6 +160,7 @@ for {{ .Entry.Manifest.Platform }} platform. Pushover: &model.NotifPushover{ Token: "uQiRzpo4DXghDmr9QzzfQu27cmVRsG", Recipient: "gznej3rKEVAvPUxu9vvNnqpmZpokzF", + Timeout: utl.NewDuration(10 * time.Second), TemplateTitle: model.NotifDefaultTemplateTitle, TemplateBody: model.NotifDefaultTemplateBody, }, diff --git a/internal/model/notif_pushover.go b/internal/model/notif_pushover.go index 76907449..5affe97d 100644 --- a/internal/model/notif_pushover.go +++ b/internal/model/notif_pushover.go @@ -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 } diff --git a/internal/notif/pushover/client.go b/internal/notif/pushover/client.go index 2f0ddafb..f942b160 100644 --- a/internal/notif/pushover/client.go +++ b/internal/notif/pushover/client.go @@ -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 } diff --git a/vendor/github.com/gregdel/pushover/.travis.yml b/vendor/github.com/gregdel/pushover/.travis.yml deleted file mode 100644 index 0668178b..00000000 --- a/vendor/github.com/gregdel/pushover/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: go -go: -- 1.15.2 -before_install: -- go get github.com/axw/gocov/gocov -- go get github.com/mattn/goveralls -- if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi -script: -- $HOME/gopath/bin/goveralls -service=travis-ci diff --git a/vendor/github.com/gregdel/pushover/LICENSE b/vendor/github.com/gregdel/pushover/LICENSE deleted file mode 100644 index e0932e54..00000000 --- a/vendor/github.com/gregdel/pushover/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) Grégoire Delattre - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/gregdel/pushover/README.md b/vendor/github.com/gregdel/pushover/README.md deleted file mode 100644 index 7e26ac72..00000000 --- a/vendor/github.com/gregdel/pushover/README.md +++ /dev/null @@ -1,134 +0,0 @@ -pushover -========= - -[![GoDoc](https://godoc.org/github.com/gregdel/pushover?status.svg)](http://godoc.org/github.com/gregdel/pushover) -[![Build Status](https://travis-ci.org/gregdel/pushover.svg?branch=master)](https://travis-ci.org/gregdel/pushover) -[![Coverage Status](https://coveralls.io/repos/gregdel/pushover/badge.svg?branch=master&service=github)](https://coveralls.io/github/gregdel/pushover?branch=master) -[![Go Report Card](https://goreportcard.com/badge/github.com/gregdel/pushover)](https://goreportcard.com/report/github.com/gregdel/pushover) - -pushover is a wrapper around the Superblock's Pushover API written in go. -Based on their [documentation](https://pushover.net/api). It's a convenient way to send notifications from a go program with only a few lines of code. - -## Messages - -### Send a simple message - -Here is a simple example for sending a notification to a recipient. A recipient can be a user or a group. There is no real difference, they both use a notification token. - -```go -package main - -import ( - "log" - - "github.com/gregdel/pushover" -) - -func main() { - // Create a new pushover app with a token - app := pushover.New("uQiRzpo4DXghDmr9QzzfQu27cmVRsG") - - // Create a new recipient - recipient := pushover.NewRecipient("gznej3rKEVAvPUxu9vvNnqpmZpokzF") - - // Create the message to send - message := pushover.NewMessage("Hello !") - - // Send the message to the recipient - response, err := app.SendMessage(message, recipient) - if err != nil { - log.Panic(err) - } - - // Print the response if you want - log.Println(response) -} -``` - -### Send a message with a title - -There is a simple way to create a message with a title. Instead of using pushover.NewMessage you can use pushover.NewMessageWithTitle. - -```go -message := pushover.NewMessageWithTitle("My awesome message", "My title") -``` - -### Send a fancy message - -If you want a more detailed message you can still do it. - -```go -message := &pushover.Message{ - Message: "My awesome message", - Title: "My title", - Priority: pushover.PriorityEmergency, - URL: "http://google.com", - URLTitle: "Google", - Timestamp: time.Now().Unix(), - Retry: 60 * time.Second, - Expire: time.Hour, - DeviceName: "SuperDevice", - CallbackURL: "http://yourapp.com/callback", - Sound: pushover.SoundCosmic, -} -``` - -### Send a message with an attachment - -You can send an image attachment along with the message. - -```go -file, err := os.Open("/some/image.png") -if err != nil { - panic(err) -} -defer file.Close() - -message := pushover.NewMessage("Hello !") -if err := message.AddAttachment(file); err != nil { - panic(err) -} -``` - -## Callbacks and receipts - -If you're using an emergency notification you'll have to specify a retry period and an expiration delay. You can get the receipt details using the token in the message response. - - -```go -... -response, err := app.SendMessage(message, recipient) -if err != nil { - log.Panic(err) -} - -receiptDetails, err := app.GetReceiptDetails(response.Receipt) -if err != nil { - log.Panic(err) -} - -fmt.Println("Acknowledged status :", receiptDetails.Acknowledged) -``` - -You can also cancel an emergency notification before the expiration time. - -```go -response, err := app.CancelEmergencyNotification(response.Receipt) -if err != nil { - log.Panic(err) -} -``` - -## User verification - -If you want to validate that the recipient token is valid. - -```go -... -recipientDetails, err := app.GetRecipientDetails(recipient) -if err != nil { - log.Panic(err) -} - -fmt.Println(recipientDetails) -``` diff --git a/vendor/github.com/gregdel/pushover/errors.go b/vendor/github.com/gregdel/pushover/errors.go deleted file mode 100644 index 452397d0..00000000 --- a/vendor/github.com/gregdel/pushover/errors.go +++ /dev/null @@ -1,18 +0,0 @@ -package pushover - -import ( - "strings" -) - -// Errors represents the errors returned by pushover. -type Errors []string - -// Error represents the error as a string. -func (e Errors) Error() string { - ret := "" - if len(e) > 0 { - ret = "Errors:\n" - ret += strings.Join(e, "\n") - } - return ret -} diff --git a/vendor/github.com/gregdel/pushover/glances.go b/vendor/github.com/gregdel/pushover/glances.go deleted file mode 100644 index 0a0771de..00000000 --- a/vendor/github.com/gregdel/pushover/glances.go +++ /dev/null @@ -1,118 +0,0 @@ -package pushover - -import ( - "fmt" - "strconv" - "strings" - "unicode/utf8" -) - -const ( - // GlancesAllDevices can be passed as a device name to send a glances-message to all devices - GlancesAllDevices = "" - // GlancesMessageMaxTitleLength is the max title length in a pushover glance update - GlancesMessageMaxTitleLength = 100 - // GlancesMessageMaxTextLength is the max text length in a pushover glance update - GlancesMessageMaxTextLength = 100 - // GlancesMessageMaxSubtextLength is the max subtext length in a pushover glance update - GlancesMessageMaxSubtextLength = 100 -) - -// Glance represents a pushover glances update request. -type Glance struct { - // Title(max 100): a description of the data being shown, such as "Widgets Sold" - Title *string - // Text(max 100): the main line of data, used on most screens - Text *string - // Subtext(max 100): a second line of data - Subtext *string - // Count(can be negative): shown on smaller screens; useful for simple counts - Count *int - // Percent(0-100): shown on some screens as a progress bar/circle - Percent *int - DeviceName string -} - -// Int returns the pointer of the input i -// Used to check for the Glance API if a parameter was left empty or if none was specified -func Int(i int) *int { - return &i -} - -// String returns the pointer of the input s -// Used to check for the Glance API if a parameter was left empty or if none was specified -func String(s string) *string { - return &s -} - -func (m *Glance) validate() error { - // check if data is present - if m.Title == nil && m.Text == nil && m.Subtext == nil && m.Count == nil && m.Percent == nil { - return ErrGlancesMissingData - } - if m.Title != nil && utf8.RuneCountInString(*m.Title) > GlancesMessageMaxTitleLength { - return ErrGlancesTitleTooLong - } - if m.Text != nil && utf8.RuneCountInString(*m.Text) > GlancesMessageMaxTextLength { - return ErrGlancesTextTooLong - } - if m.Subtext != nil && utf8.RuneCountInString(*m.Subtext) > GlancesMessageMaxSubtextLength { - return ErrGlancesSubtextTooLong - } - if m.Percent != nil && (*m.Percent < 0 || *m.Percent > 100) { - return ErrGlancesInvalidPercent - } - // Test device name - if m.DeviceName != "" { - // Accept comma separated device names - devices := strings.Split(m.DeviceName, ",") - for _, d := range devices { - if !deviceNameRegexp.MatchString(d) { - return ErrInvalidDeviceName - } - } - } - return nil -} - -// send sends the message using the pushover and the recipient tokens. -func (m *Glance) send(pToken, rToken string) (*Response, error) { - url := fmt.Sprintf("%s/glances.json", APIEndpoint) - - params := map[string]string{ - "token": pToken, - "user": rToken, - } - if m.DeviceName != "" { - params["device"] = m.DeviceName - } - - // data - if m.Count != nil { - params["count"] = strconv.Itoa(*m.Count) - } - if m.Percent != nil { - params["percent"] = strconv.Itoa(*m.Percent) - } - if m.Title != nil { - params["title"] = *m.Title - } - if m.Text != nil { - params["text"] = *m.Text - } - if m.Subtext != nil { - params["subtext"] = *m.Subtext - } - - req, err := newURLEncodedRequest("POST", url, params) - if err != nil { - return nil, err - } - - resp := new(Response) - if err = do(req, resp, true); err != nil { - return nil, err - } - - return resp, nil -} diff --git a/vendor/github.com/gregdel/pushover/helpers.go b/vendor/github.com/gregdel/pushover/helpers.go deleted file mode 100644 index e95b3895..00000000 --- a/vendor/github.com/gregdel/pushover/helpers.go +++ /dev/null @@ -1,45 +0,0 @@ -package pushover - -import ( - "encoding/json" - "fmt" - "time" -) - -// Helper to unmarshal a timestamp as string to a time.Time. -type timestamp struct{ *time.Time } - -func (t *timestamp) UnmarshalJSON(data []byte) error { - var i int64 - if err := json.Unmarshal(data, &i); err != nil { - return err - } - - if i > 0 { - unixTime := time.Unix(i, 0) - *t = timestamp{&unixTime} - } - - return nil -} - -// Helper to unmarshal a int as a boolean. -type intBool bool - -func (i *intBool) UnmarshalJSON(data []byte) error { - var v int64 - if err := json.Unmarshal(data, &v); err != nil { - return err - } - - switch v { - case 0: - *i = false - case 1: - *i = true - default: - return fmt.Errorf("failed to unmarshal int to bool") - } - - return nil -} diff --git a/vendor/github.com/gregdel/pushover/limit.go b/vendor/github.com/gregdel/pushover/limit.go deleted file mode 100644 index 9ac72336..00000000 --- a/vendor/github.com/gregdel/pushover/limit.go +++ /dev/null @@ -1,57 +0,0 @@ -package pushover - -import ( - "net/http" - "strconv" - "time" -) - -// Limit represents the limitation of the application. This information is -// fetched when posting a new message. -// Headers example: -// X-Limit-App-Limit: 7500 -// X-Limit-App-Remaining: 7496 -// X-Limit-App-Reset: 1393653600 -type Limit struct { - // Total number of messages you can send during a month. - Total int - // Remaining number of messages you can send until the next reset. - Remaining int - // NextReset is the time when all the app counters will be reseted. - NextReset time.Time -} - -func newLimit(headers http.Header) (*Limit, error) { - headersStrings := []string{ - "X-Limit-App-Limit", - "X-Limit-App-Remaining", - "X-Limit-App-Reset", - } - headersValues := map[string]int{} - - for _, header := range headersStrings { - // Check if the header is present - h, ok := headers[header] - if !ok { - return nil, ErrInvalidHeaders - } - - // The header must have only one element - if len(h) != 1 { - return nil, ErrInvalidHeaders - } - - i, err := strconv.Atoi(h[0]) - if err != nil { - return nil, err - } - - headersValues[header] = i - } - - return &Limit{ - Total: headersValues["X-Limit-App-Limit"], - Remaining: headersValues["X-Limit-App-Remaining"], - NextReset: time.Unix(int64(headersValues["X-Limit-App-Reset"]), 0), - }, nil -} diff --git a/vendor/github.com/gregdel/pushover/message.go b/vendor/github.com/gregdel/pushover/message.go deleted file mode 100644 index 30ddda81..00000000 --- a/vendor/github.com/gregdel/pushover/message.go +++ /dev/null @@ -1,253 +0,0 @@ -package pushover - -import ( - "bytes" - "fmt" - "io" - "mime/multipart" - "net/http" - "regexp" - "strconv" - "strings" - "time" - "unicode/utf8" -) - -var deviceNameRegexp *regexp.Regexp - -func init() { - deviceNameRegexp = regexp.MustCompile(`^[A-Za-z0-9_-]{1,25}$`) -} - -// Message represents a pushover message. -type Message struct { - // Required - Message string - - // Optional - Title string - Priority int - URL string - URLTitle string - Timestamp int64 - Retry time.Duration - Expire time.Duration - CallbackURL string - DeviceName string - Sound string - HTML bool - Monospace bool - TTL time.Duration - - // attachment - attachment io.Reader -} - -// NewMessage returns a simple new message. -func NewMessage(message string) *Message { - return &Message{Message: message} -} - -// NewMessageWithTitle returns a simple new message with a title. -func NewMessageWithTitle(message, title string) *Message { - return &Message{Message: message, Title: title} -} - -// AddAttachment adds an attachment to the message it's programmer's -// responsibility to close the reader. -func (m *Message) AddAttachment(attachment io.Reader) error { - m.attachment = attachment - return nil -} - -// Validate the message values. -func (m *Message) validate() error { - // Message should no be empty - if m.Message == "" { - return ErrMessageEmpty - } - - // Validate message length - if utf8.RuneCountInString(m.Message) > MessageMaxLength { - return ErrMessageTooLong - } - - // Validate Title field length - if utf8.RuneCountInString(m.Title) > MessageTitleMaxLength { - return ErrMessageTitleTooLong - } - - // Validate URL field - if utf8.RuneCountInString(m.URL) > MessageURLMaxLength { - return ErrMessageURLTooLong - } - - // Validate URL title field - if utf8.RuneCountInString(m.URLTitle) > MessageURLTitleMaxLength { - return ErrMessageURLTitleTooLong - } - - // URLTitle should not be set with an empty URL - if m.URL == "" && m.URLTitle != "" { - return ErrEmptyURL - } - - // Validate priorities - if m.Priority > PriorityEmergency || m.Priority < PriorityLowest { - return ErrInvalidPriority - } - - // Validate emergency priority - if m.Priority == PriorityEmergency { - if m.Retry == 0 || m.Expire == 0 { - return ErrMissingEmergencyParameter - } - } - - // Test device name - if m.DeviceName != "" { - // Accept comma separated device names - devices := strings.Split(m.DeviceName, ",") - for _, d := range devices { - if !deviceNameRegexp.MatchString(d) { - return ErrInvalidDeviceName - } - } - } - - return nil -} - -// Return a map filled with the relevant data. -func (m *Message) toMap(pToken, rToken string) map[string]string { - ret := map[string]string{ - "token": pToken, - "user": rToken, - "message": m.Message, - "priority": strconv.Itoa(m.Priority), - } - - if m.Title != "" { - ret["title"] = m.Title - } - - if m.URL != "" { - ret["url"] = m.URL - } - - if m.URLTitle != "" { - ret["url_title"] = m.URLTitle - } - - if m.Sound != "" { - ret["sound"] = m.Sound - } - - if m.DeviceName != "" { - ret["device"] = m.DeviceName - } - - if m.Timestamp != 0 { - ret["timestamp"] = strconv.FormatInt(m.Timestamp, 10) - } - - if m.HTML { - ret["html"] = "1" - } - - if m.Monospace { - ret["monospace"] = "1" - } - - if m.Priority == PriorityEmergency { - ret["retry"] = strconv.FormatFloat(m.Retry.Seconds(), 'f', -1, 64) - ret["expire"] = strconv.FormatFloat(m.Expire.Seconds(), 'f', -1, 64) - if m.CallbackURL != "" { - ret["callback"] = m.CallbackURL - } - } - - if m.TTL != 0 { - ret["ttl"] = strconv.FormatFloat(m.TTL.Seconds(), 'f', -1, 64) - } - - return ret -} - -// Send sends the message using the pushover and the recipient tokens. -func (m *Message) send(pToken, rToken string) (*Response, error) { - url := fmt.Sprintf("%s/messages.json", APIEndpoint) - - var f func(string, string, string) (*http.Request, error) - if m.attachment == nil { - // Use a URL-encoded request if there's no need to attach files - f = m.urlEncodedRequest - } else { - // Use a multipart request if a file should be sent - f = m.multipartRequest - } - - // Post the from and check the headers of the response - req, err := f(pToken, rToken, url) - if err != nil { - return nil, err - } - - resp := &Response{} - if err := do(req, resp, true); err != nil { - return nil, err - } - - return resp, nil -} - -// multipartRequest returns a new multipart POST request with a file attached. -func (m *Message) multipartRequest(pToken, rToken, url string) (*http.Request, error) { - body := &bytes.Buffer{} - - if m.attachment == nil { - return nil, ErrMissingAttachment - } - - // Write the body as multipart form data - w := multipart.NewWriter(body) - - // Write the file in the body - fw, err := w.CreateFormFile("attachment", "attachment") - if err != nil { - return nil, err - } - - written, err := io.Copy(fw, m.attachment) - if err != nil { - return nil, err - } - - if written > MessageMaxAttachmentByte { - return nil, ErrMessageAttachmentTooLarge - } - - // Handle params - for k, v := range m.toMap(pToken, rToken) { - if err := w.WriteField(k, v); err != nil { - return nil, err - } - } - - if err := w.Close(); err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", url, body) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", w.FormDataContentType()) - - return req, nil -} - -// urlEncodedRequest returns a new url encoded request. -func (m *Message) urlEncodedRequest(pToken, rToken, endpoint string) (*http.Request, error) { - return newURLEncodedRequest("POST", endpoint, m.toMap(pToken, rToken)) -} diff --git a/vendor/github.com/gregdel/pushover/pushover.go b/vendor/github.com/gregdel/pushover/pushover.go deleted file mode 100644 index 2fcf6e89..00000000 --- a/vendor/github.com/gregdel/pushover/pushover.go +++ /dev/null @@ -1,236 +0,0 @@ -// Package pushover provides a wrapper around the Pushover API -package pushover - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "regexp" -) - -// Regexp validation. -var tokenRegexp *regexp.Regexp - -func init() { - tokenRegexp = regexp.MustCompile(`^[A-Za-z0-9]{30}$`) -} - -// APIEndpoint is the API base URL for any request. -var APIEndpoint = "https://api.pushover.net/1" - -// Pushover custom errors. -var ( - ErrHTTPPushover = errors.New("pushover: http error") - ErrEmptyToken = errors.New("pushover: empty API token") - ErrEmptyURL = errors.New("pushover: empty URL, URLTitle needs an URL") - ErrEmptyRecipientToken = errors.New("pushover: empty recipient token") - ErrInvalidRecipientToken = errors.New("pushover: invalid recipient token") - ErrInvalidHeaders = errors.New("pushover: invalid headers in server response") - ErrInvalidPriority = errors.New("pushover: invalid priority") - ErrInvalidToken = errors.New("pushover: invalid API token") - ErrMessageEmpty = errors.New("pushover: message empty") - ErrMessageTitleTooLong = errors.New("pushover: message title too long") - ErrMessageTooLong = errors.New("pushover: message too long") - ErrMessageAttachmentTooLarge = errors.New("pushover: message attachment is too large") - ErrMessageURLTitleTooLong = errors.New("pushover: message URL title too long") - ErrMessageURLTooLong = errors.New("pushover: message URL too long") - ErrMissingAttachment = errors.New("pushover: missing attachment") - ErrMissingEmergencyParameter = errors.New("pushover: missing emergency parameter") - ErrInvalidDeviceName = errors.New("pushover: invalid device name") - ErrEmptyReceipt = errors.New("pushover: empty receipt") - ErrGlancesMissingData = errors.New("pushover: glance update data missing") - ErrGlancesTitleTooLong = errors.New("pushover: glance title too long") - ErrGlancesTextTooLong = errors.New("pushover: glance text too long") - ErrGlancesSubtextTooLong = errors.New("pushover: glance subtext too long") - ErrGlancesInvalidPercent = errors.New("pushover: glance percent must be in range of 0-100") -) - -// API limitations. -const ( - // MessageMaxLength is the max message number of characters. - MessageMaxLength = 1024 - // MessageTitleMaxLength is the max title number of characters. - MessageTitleMaxLength = 250 - // MessageURLMaxLength is the max URL number of characters. - MessageURLMaxLength = 512 - // MessageURLTitleMaxLength is the max URL title number of characters. - MessageURLTitleMaxLength = 100 - // MessageMaxAttachmentByte is the max attachment size in byte. - MessageMaxAttachmentByte = 2621440 -) - -// Message priorities -const ( - PriorityLowest = -2 - PriorityLow = -1 - PriorityNormal = 0 - PriorityHigh = 1 - PriorityEmergency = 2 -) - -// Sounds -const ( - SoundPushover = "pushover" - SoundBike = "bike" - SoundBugle = "bugle" - SoundCashRegister = "cashregister" - SoundClassical = "classical" - SoundCosmic = "cosmic" - SoundFalling = "falling" - SoundGamelan = "gamelan" - SoundIncoming = "incoming" - SoundIntermission = "intermission" - SoundMagic = "magic" - SoundMechanical = "mechanical" - SoundPianobar = "pianobar" - SoundSiren = "siren" - SoundSpaceAlarm = "spacealarm" - SoundTugBoat = "tugboat" - SoundAlien = "alien" - SoundClimb = "climb" - SoundPersistent = "persistent" - SoundEcho = "echo" - SoundUpDown = "updown" - SoundVibrate = "vibrate" - SoundNone = "none" -) - -// Pushover is the representation of an app using the pushover API. -type Pushover struct { - token string -} - -// New returns a new app to talk to the pushover API. -func New(token string) *Pushover { - return &Pushover{token} -} - -// Validate Pushover token. -func (p *Pushover) validate() error { - // Check empty token - if p.token == "" { - return ErrEmptyToken - } - - // Check invalid token - if !tokenRegexp.MatchString(p.token) { - return ErrInvalidToken - } - return nil -} - -// SendMessage is used to send message to a recipient. -func (p *Pushover) SendMessage(message *Message, recipient *Recipient) (*Response, error) { - // Validate pushover - if err := p.validate(); err != nil { - return nil, err - } - - // Validate recipient - if err := recipient.validate(); err != nil { - return nil, err - } - - // Validate message - if err := message.validate(); err != nil { - return nil, err - } - - return message.send(p.token, recipient.token) -} - -// SendGlanceUpdate is used to send glance updates to a recipient. -// It can be used to display widgets on a smart watch -func (p *Pushover) SendGlanceUpdate(msg *Glance, rec *Recipient) (*Response, error) { - // Validate pushover - if err := p.validate(); err != nil { - return nil, err - } - - // Validate rec - if err := rec.validate(); err != nil { - return nil, err - } - - // Validate msg - if err := msg.validate(); err != nil { - return nil, err - } - - return msg.send(p.token, rec.token) -} - -// GetReceiptDetails return detailed information about a receipt. This is used -// used to check the acknowledged status of an Emergency notification. -func (p *Pushover) GetReceiptDetails(receipt string) (*ReceiptDetails, error) { - url := fmt.Sprintf("%s/receipts/%s.json?token=%s", APIEndpoint, receipt, p.token) - - if receipt == "" { - return nil, ErrEmptyReceipt - } - - // Send request - resp, err := http.Get(url) - if err != nil { - return nil, err - } - - // Decode the JSON response - var details *ReceiptDetails - if err = json.NewDecoder(resp.Body).Decode(&details); err != nil { - return nil, err - } - - return details, nil -} - -// GetRecipientDetails allows to check if a recipient exists, if it's a group -// and the devices associated to this recipient. The Errors field of the -// RecipientDetails object will contain an error if the recipient is not valid -// in the Pushover API. -func (p *Pushover) GetRecipientDetails(recipient *Recipient) (*RecipientDetails, error) { - endpoint := fmt.Sprintf("%s/users/validate.json", APIEndpoint) - - // Validate pushover - if err := p.validate(); err != nil { - return nil, err - } - - // Validate recipient - if err := recipient.validate(); err != nil { - return nil, err - } - - req, err := newURLEncodedRequest("POST", endpoint, - map[string]string{"token": p.token, "user": recipient.token}) - if err != nil { - return nil, err - } - - var response RecipientDetails - if err := do(req, &response, false); err != nil { - return nil, err - } - - return &response, nil -} - -// CancelEmergencyNotification helps stop a notification retry in case of a -// notification with an Emergency priority before reaching the expiration time. -// It requires the response receipt in order to stop the right notification. -func (p *Pushover) CancelEmergencyNotification(receipt string) (*Response, error) { - endpoint := fmt.Sprintf("%s/receipts/%s/cancel.json", APIEndpoint, receipt) - - req, err := newURLEncodedRequest("POST", endpoint, map[string]string{"token": p.token}) - if err != nil { - return nil, err - } - - response := &Response{} - if err := do(req, response, false); err != nil { - return nil, err - } - - return response, nil -} diff --git a/vendor/github.com/gregdel/pushover/receipt_details.go b/vendor/github.com/gregdel/pushover/receipt_details.go deleted file mode 100644 index 38a21290..00000000 --- a/vendor/github.com/gregdel/pushover/receipt_details.go +++ /dev/null @@ -1,59 +0,0 @@ -package pushover - -import ( - "bytes" - "encoding/json" - "time" -) - -// ReceiptDetails represents the receipt informations in case of emergency -// priority. -type ReceiptDetails struct { - Status int - Acknowledged bool - AcknowledgedBy string - Expired bool - CalledBack bool - ID string - AcknowledgedAt *time.Time - LastDeliveredAt *time.Time - ExpiresAt *time.Time - CalledBackAt *time.Time -} - -// UnmarshalJSON is a custom unmarshal function to handle timestamps and -// boolean as int and convert them to the right type. -func (r *ReceiptDetails) UnmarshalJSON(data []byte) error { - dataBytes := bytes.NewReader(data) - var aux struct { - ID string `json:"request"` - Status int `json:"status"` - Acknowledged intBool `json:"acknowledged"` - AcknowledgedBy string `json:"acknowledged_by"` - Expired intBool `json:"expired"` - CalledBack intBool `json:"called_back"` - AcknowledgedAt *timestamp `json:"acknowledged_at"` - LastDeliveredAt *timestamp `json:"last_delivered_at"` - ExpiresAt *timestamp `json:"expires_at"` - CalledBackAt *timestamp `json:"called_back_at"` - } - - // Decode json into the aux struct - if err := json.NewDecoder(dataBytes).Decode(&aux); err != nil { - return err - } - - // Set the RecipientDetails with the right types - r.Status = aux.Status - r.Acknowledged = bool(aux.Acknowledged) - r.AcknowledgedBy = aux.AcknowledgedBy - r.Expired = bool(aux.Expired) - r.CalledBack = bool(aux.CalledBack) - r.ID = aux.ID - r.AcknowledgedAt = aux.AcknowledgedAt.Time - r.LastDeliveredAt = aux.LastDeliveredAt.Time - r.ExpiresAt = aux.ExpiresAt.Time - r.CalledBackAt = aux.CalledBackAt.Time - - return nil -} diff --git a/vendor/github.com/gregdel/pushover/recipient.go b/vendor/github.com/gregdel/pushover/recipient.go deleted file mode 100644 index 2074078c..00000000 --- a/vendor/github.com/gregdel/pushover/recipient.go +++ /dev/null @@ -1,43 +0,0 @@ -package pushover - -import "regexp" - -var recipientRegexp *regexp.Regexp - -func init() { - recipientRegexp = regexp.MustCompile(`^[A-Za-z0-9]{30}$`) -} - -// Recipient represents the a recipient to notify. -type Recipient struct { - token string -} - -// NewRecipient is the representation of the recipient to notify. -func NewRecipient(token string) *Recipient { - return &Recipient{token} -} - -// Validates recipient token. -func (r *Recipient) validate() error { - // Check empty token - if r.token == "" { - return ErrEmptyRecipientToken - } - - // Check invalid token - if !recipientRegexp.MatchString(r.token) { - return ErrInvalidRecipientToken - } - return nil -} - -// RecipientDetails represents the receipt informations in case of emergency -// priority. -type RecipientDetails struct { - Status int `json:"status"` - Group int `json:"group"` - Devices []string `json:"devices"` - RequestID string `json:"request"` - Errors Errors `json:"errors"` -} diff --git a/vendor/github.com/gregdel/pushover/request.go b/vendor/github.com/gregdel/pushover/request.go deleted file mode 100644 index 0be7c29a..00000000 --- a/vendor/github.com/gregdel/pushover/request.go +++ /dev/null @@ -1,69 +0,0 @@ -package pushover - -import ( - "encoding/json" - "net/http" - "net/url" - "strings" -) - -// do is a generic function to send a request to the API. -func do(req *http.Request, resType interface{}, returnHeaders bool) error { - client := http.DefaultClient - - // Send request - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - // Only 500 errors will not respond a readable result - if resp.StatusCode >= http.StatusInternalServerError { - return ErrHTTPPushover - } - - // Decode the JSON response - if err := json.NewDecoder(resp.Body).Decode(&resType); err != nil { - return err - } - - // Check if the unmarshaled data is a response - r, ok := resType.(*Response) - if !ok { - return nil - } - - // Check response status - if r.Status != 1 { - return r.Errors - } - - // The headers are only returned when posting a new notification - if returnHeaders { - // Get app limits from headers - appLimits, err := newLimit(resp.Header) - if err != nil { - return err - } - r.Limit = appLimits - } - - return nil -} - -// urlEncodedRequest returns a new url encoded request. -func newURLEncodedRequest(method, endpoint string, params map[string]string) (*http.Request, error) { - urlValues := url.Values{} - for k, v := range params { - urlValues.Add(k, v) - } - - req, err := http.NewRequest(method, endpoint, strings.NewReader(urlValues.Encode())) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - return req, nil -} diff --git a/vendor/github.com/gregdel/pushover/response.go b/vendor/github.com/gregdel/pushover/response.go deleted file mode 100644 index f22cd2e7..00000000 --- a/vendor/github.com/gregdel/pushover/response.go +++ /dev/null @@ -1,26 +0,0 @@ -package pushover - -import "fmt" - -// Response represents a response from the API. -type Response struct { - Status int `json:"status"` - ID string `json:"request"` - Errors Errors `json:"errors"` - Receipt string `json:"receipt"` - Limit *Limit -} - -// String represents a printable form of the response. -func (r Response) String() string { - ret := fmt.Sprintf("Status: %d\n", r.Status) - ret += fmt.Sprintf("Request id: %s\n", r.ID) - if r.Receipt != "" { - ret += fmt.Sprintf("Receipt: %s\n", r.Receipt) - } - if r.Limit != nil { - ret += fmt.Sprintf("Usage %d/%d messages\nNext reset : %s", - r.Limit.Remaining, r.Limit.Total, r.Limit.NextReset) - } - return ret -} diff --git a/vendor/modules.txt b/vendor/modules.txt index dd84aeca..00952609 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -278,9 +278,6 @@ github.com/gorilla/mux # github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 ## explicit; go 1.20 github.com/gorilla/websocket -# github.com/gregdel/pushover v1.3.1 -## explicit; go 1.14 -github.com/gregdel/pushover # github.com/hashicorp/cronexpr v1.1.2 ## explicit github.com/hashicorp/cronexpr