diff --git a/.res/notif-gotify.png b/.res/notif-gotify.png new file mode 100644 index 00000000..136e1044 Binary files /dev/null and b/.res/notif-gotify.png differ diff --git a/doc/configuration.md b/doc/configuration.md index a093252d..a64cdca7 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -49,6 +49,12 @@ notif: Content-Type: application/json Authorization: Token123456 timeout: 10 + gotify: + enable: false + endpoint: http://gotify.foo.com + token: Token123456 + priority: 1 + timeout: 10 regopts: someregistryoptions: @@ -146,6 +152,13 @@ providers: * `headers`: Map of additional headers to be sent. * `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`). +* `gotify` + * `enable`: Enable gotify notification (default: `false`). + * `endpoint`: Gotify base URL (e.g. `http://gotify.foo.com`). **required** + * `token`: Application token. **required** + * `priority`: The priority of the message. + * `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`). + ### regopts * `username`: Registry username. diff --git a/doc/notifications.md b/doc/notifications.md index 934cf08b..fbf7be08 100644 --- a/doc/notifications.md +++ b/doc/notifications.md @@ -1,10 +1,17 @@ # Notifications +* [Gotify](#gotify) * [Mail](#mail) * [Slack](#slack) * [Telegram](#telegram) * [Webhook](#webhook) +## Gotify + +Notifications can be sent using a [Gotify](https://gotify.net/) instance. [Follow the instructions](https://gotify.net/docs/install) to set up a Gotify server. + +![](../.res/notif-gotify.png) + ## Mail Here is an email sample if you add `mail` notification: diff --git a/internal/config/config.go b/internal/config/config.go index 0db7d95f..1270ceea 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -67,6 +67,10 @@ func Load(flags model.Flags, version string) (*Config, error) { Method: "GET", Timeout: 10, }, + Gotify: model.NotifGotify{ + Enable: false, + Timeout: 10, + }, }, } diff --git a/internal/config/config.test.yml b/internal/config/config.test.yml index c03cd6c0..3cf2594e 100644 --- a/internal/config/config.test.yml +++ b/internal/config/config.test.yml @@ -36,6 +36,12 @@ notif: Content-Type: application/json Authorization: Token123456 timeout: 10 + gotify: + enable: false + endpoint: http://gotify.foo.com + token: Token123456 + priority: 1 + timeout: 10 regopts: someregopts: diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 8744e45b..d4f59d81 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -78,6 +78,13 @@ func TestLoad(t *testing.T) { }, Timeout: 10, }, + Gotify: model.NotifGotify{ + Enable: false, + Endpoint: "http://gotify.foo.com", + Token: "Token123456", + Priority: 1, + Timeout: 10, + }, }, RegOpts: map[string]model.RegOpts{ "someregopts": { diff --git a/internal/model/notif.go b/internal/model/notif.go index 78fe826a..2539fdda 100644 --- a/internal/model/notif.go +++ b/internal/model/notif.go @@ -18,6 +18,7 @@ type Notif struct { Slack NotifSlack `yaml:"slack,omitempty"` Telegram NotifTelegram `yaml:"telegram,omitempty"` Webhook NotifWebhook `yaml:"webhook,omitempty"` + Gotify NotifGotify `yaml:"gotify,omitempty"` } // NotifMail holds mail notification configuration details @@ -56,3 +57,12 @@ type NotifWebhook struct { Headers map[string]string `yaml:"headers,omitempty"` Timeout int `yaml:"timeout,omitempty"` } + +// NotifGotify holds gotify notification configuration details +type NotifGotify struct { + Enable bool `yaml:"enable,omitempty"` + Endpoint string `yaml:"endpoint,omitempty"` + Token string `yaml:"token,omitempty"` + Priority int `yaml:"priority,omitempty"` + Timeout int `yaml:"timeout,omitempty"` +} diff --git a/internal/notif/client.go b/internal/notif/client.go index 8f17d73c..d803ec39 100644 --- a/internal/notif/client.go +++ b/internal/notif/client.go @@ -2,6 +2,7 @@ package notif import ( "github.com/crazy-max/diun/internal/model" + "github.com/crazy-max/diun/internal/notif/gotify" "github.com/crazy-max/diun/internal/notif/mail" "github.com/crazy-max/diun/internal/notif/notifier" "github.com/crazy-max/diun/internal/notif/slack" @@ -38,6 +39,9 @@ func New(config model.Notif, app model.App) (*Client, error) { if config.Webhook.Enable { c.notifiers = append(c.notifiers, webhook.New(config.Webhook, app)) } + if config.Gotify.Enable { + c.notifiers = append(c.notifiers, gotify.New(config.Gotify, app)) + } log.Debug().Msgf("%d notifier(s) created", len(c.notifiers)) return c, nil diff --git a/internal/notif/gotify/client.go b/internal/notif/gotify/client.go new file mode 100644 index 00000000..698993a9 --- /dev/null +++ b/internal/notif/gotify/client.go @@ -0,0 +1,101 @@ +package gotify + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "strconv" + "strings" + "text/template" + "time" + + "github.com/crazy-max/diun/internal/model" + "github.com/crazy-max/diun/internal/notif/notifier" +) + +// Client represents an active gotify notification object +type Client struct { + *notifier.Notifier + cfg model.NotifGotify + app model.App +} + +// New creates a new gotify notification instance +func New(config model.NotifGotify, app model.App) notifier.Notifier { + return notifier.Notifier{ + Handler: &Client{ + cfg: config, + app: app, + }, + } +} + +// Name returns notifier's name +func (c *Client) Name() string { + return "gotify" +} + +// Send creates and sends a gotify notification with an entry +func (c *Client) Send(entry model.NotifEntry) error { + hc := http.Client{ + Timeout: time.Duration(c.cfg.Timeout) * time.Second, + } + + title := fmt.Sprintf("Image update for %s", entry.Image.String()) + if entry.Status == model.ImageStatusNew { + title = fmt.Sprintf("New image %s has been added", entry.Image.String()) + } + + var msgBuf bytes.Buffer + msgTpl := template.Must(template.New("gotify").Parse(`Docker 🐳 tag {{ .Image.Domain }}/{{ .Image.Path }}:{{ .Image.Tag }} which you subscribed to through {{ .Provider }} provider has been {{ if (eq .Status "new") }}newly added{{ else }}updated{{ end }}.`)) + if err := msgTpl.Execute(&msgBuf, entry); err != nil { + return err + } + + data := url.Values{} + data.Set("message", msgBuf.String()) + data.Set("title", title) + data.Set("priority", strconv.Itoa(c.cfg.Priority)) + + u, err := url.Parse(c.cfg.Endpoint) + if err != nil { + return err + } + u.Path = path.Join(u.Path, "message") + + q := u.Query() + q.Set("token", c.cfg.Token) + u.RawQuery = q.Encode() + + req, err := http.NewRequest("POST", u.String(), strings.NewReader(data.Encode())) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) + req.Header.Set("User-Agent", fmt.Sprintf("%s %s", c.app.Name, c.app.Version)) + + resp, err := hc.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + var errBody struct { + Error string `json:"error"` + ErrorCode int `json:"errorCode"` + ErrorDescription string `json:"errorDescription"` + } + err := json.NewDecoder(resp.Body).Decode(&errBody) + if err != nil { + return err + } + return fmt.Errorf("%d %s: %s", errBody.ErrorCode, errBody.Error, errBody.ErrorDescription) + } + + return nil +} diff --git a/internal/notif/webhook/client.go b/internal/notif/webhook/client.go index 8267f64f..1f0581b4 100644 --- a/internal/notif/webhook/client.go +++ b/internal/notif/webhook/client.go @@ -65,7 +65,7 @@ func (c *Client) Send(entry model.NotifEntry) error { return err } - req, err := http.NewRequest(c.cfg.Method, c.cfg.Endpoint, bytes.NewBuffer([]byte(body))) + req, err := http.NewRequest(c.cfg.Method, c.cfg.Endpoint, bytes.NewBuffer(body)) if err != nil { return err }