11package pushover
22
33import (
4+ "context"
5+ "encoding/json"
6+ "net/http"
7+ "net/url"
8+ "strconv"
9+ "strings"
410 "time"
511
612 "github.com/crazy-max/diun/v4/internal/model"
713 "github.com/crazy-max/diun/v4/internal/msg"
814 "github.com/crazy-max/diun/v4/internal/notif/notifier"
915 "github.com/crazy-max/diun/v4/pkg/utl"
10- "github.com/gregdel/pushover"
1116 "github.com/pkg/errors"
17+ "github.com/rs/zerolog/log"
1218)
1319
20+ const pushoverAPIURL = "https://api.pushover.net/1/messages.json"
21+
1422// Client represents an active Pushover notification object
1523type Client struct {
1624 * notifier.Notifier
@@ -38,11 +46,15 @@ func (c *Client) Send(entry model.NotifEntry) error {
3846 token , err := utl .GetSecret (c .cfg .Token , c .cfg .TokenFile )
3947 if err != nil {
4048 return errors .Wrap (err , "cannot retrieve token secret for Pushover notifier" )
49+ } else if token == "" {
50+ return errors .New ("Pushover API token cannot be empty" )
4151 }
4252
4353 recipient , err := utl .GetSecret (c .cfg .Recipient , c .cfg .RecipientFile )
4454 if err != nil {
4555 return errors .Wrap (err , "cannot retrieve recipient secret for Pushover notifier" )
56+ } else if recipient == "" {
57+ return errors .New ("Pushover recipient cannot be empty" )
4658 }
4759
4860 message , err := msg .New (msg.Options {
@@ -60,16 +72,77 @@ func (c *Client) Send(entry model.NotifEntry) error {
6072 return err
6173 }
6274
63- _ , err = pushover .New (token ).SendMessage (& pushover.Message {
64- Title : string (title ),
65- Message : string (body ),
66- Priority : c .cfg .Priority ,
67- Sound : c .cfg .Sound ,
68- URL : c .meta .URL ,
69- URLTitle : c .meta .Name ,
70- Timestamp : time .Now ().Unix (),
71- HTML : true ,
72- }, pushover .NewRecipient (recipient ))
73-
74- return err
75+ cancelCtx , cancel := context .WithCancelCause (context .Background ())
76+ 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
77+ defer func () { cancel (errors .WithStack (context .Canceled )) }()
78+
79+ form := url.Values {}
80+ form .Add ("token" , token )
81+ form .Add ("user" , recipient )
82+ form .Add ("title" , string (title ))
83+ form .Add ("message" , string (body ))
84+ form .Add ("priority" , strconv .Itoa (c .cfg .Priority ))
85+ if c .cfg .Sound != "" {
86+ form .Add ("sound" , c .cfg .Sound )
87+ }
88+ if c .meta .URL != "" {
89+ form .Add ("url" , c .meta .URL )
90+ }
91+ if c .meta .Name != "" {
92+ form .Add ("url_title" , c .meta .Name )
93+ }
94+ form .Add ("timestamp" , strconv .FormatInt (time .Now ().Unix (), 10 ))
95+ form .Add ("html" , "1" )
96+
97+ hc := http.Client {}
98+ req , err := http .NewRequestWithContext (timeoutCtx , "POST" , pushoverAPIURL , strings .NewReader (form .Encode ()))
99+ if err != nil {
100+ return err
101+ }
102+ req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
103+ req .Header .Set ("User-Agent" , c .meta .UserAgent )
104+
105+ resp , err := hc .Do (req )
106+ if err != nil {
107+ return err
108+ }
109+ defer resp .Body .Close ()
110+
111+ if resp .Header != nil {
112+ var appLimit , appRemaining int
113+ var appReset time.Time
114+ if limit := resp .Header .Get ("X-Limit-App-Limit" ); limit != "" {
115+ if i , err := strconv .Atoi (limit ); err == nil {
116+ appLimit = i
117+ }
118+ }
119+ if remaining := resp .Header .Get ("X-Limit-App-Remaining" ); remaining != "" {
120+ if i , err := strconv .Atoi (remaining ); err == nil {
121+ appRemaining = i
122+ }
123+ }
124+ if reset := resp .Header .Get ("X-Limit-App-Reset" ); reset != "" {
125+ if i , err := strconv .Atoi (reset ); err == nil {
126+ appReset = time .Unix (int64 (i ), 0 )
127+ }
128+ }
129+ log .Debug ().Msgf ("Pushover app limit: %d, remaining: %d, reset: %s" , appLimit , appRemaining , appReset )
130+ }
131+
132+ var respBody struct {
133+ Status int `json:"status"`
134+ Request string `json:"request"`
135+ Errors []string `json:"errors"`
136+ User string `json:"user"`
137+ Token string `json:"token"`
138+ }
139+
140+ if err = json .NewDecoder (resp .Body ).Decode (& respBody ); err != nil {
141+ return errors .Wrapf (err , "cannot decode JSON body response for HTTP %d %s status: %+v" , resp .StatusCode , http .StatusText (resp .StatusCode ), respBody )
142+ }
143+ if respBody .Status != 1 {
144+ return errors .Errorf ("Pushover API call failed with status %d: %v" , respBody .Status , respBody .Errors )
145+ }
146+
147+ return nil
75148}
0 commit comments