Discord notif (#111)

Co-authored-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2020-06-26 16:06:47 +00:00
committed by GitHub
parent 2665c90d28
commit d06ec05c1c
12 changed files with 266 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

30
docs/notif/discord.md Normal file
View File

@@ -0,0 +1,30 @@
# Discord notifications
Allow to send notifications to your Discord channel.
## Configuration
!!! example "File"
```yaml
notif:
discord:
webhookURL: https://discordapp.com/api/webhooks/1234567890/Abcd-eFgh-iJklmNo_pqr
timeout: 10s
```
!!! abstract "Environment variables"
* `DIUN_NOTIF_DISCORD_WEBHOOK`
* `DIUN_NOTIF_DISCORD_TIMEOUT`
| Name | Default | Description |
|--------------------|---------------|---------------|
| `webhookURL`[^1] | | Discord [incoming webhook URL](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) |
| `timeout` | `10s` | Timeout specifies a time limit for the request to be made |
## Sample
![](../assets/notif/discord-1.png)
![](../assets/notif/discord-2.png)
[^1]: Value required

View File

@@ -50,6 +50,10 @@ func TestLoadFile(t *testing.T) {
Password: "guest",
Queue: "queue",
},
Discord: &model.NotifDiscord{
WebhookURL: "https://discordapp.com/api/webhooks/1234567890/Abcd-eFgh-iJklmNo_pqr",
Timeout: utl.NewDuration(10 * time.Second),
},
Gotify: &model.NotifGotify{
Endpoint: "http://gotify.foo.com",
Token: "Token123456",

View File

@@ -13,6 +13,9 @@ notif:
username: guest
password: guest
queue: queue
discord:
webhookURL: https://discordapp.com/api/webhooks/1234567890/Abcd-eFgh-iJklmNo_pqr
timeout: 10s
gotify:
endpoint: http://gotify.foo.com
token: Token123456

View File

@@ -13,6 +13,9 @@ notif:
username: guest
password: guest
queue: queue
discord:
webhookURL: https://discordapp.com/api/webhooks/1234567890/Abcd-eFgh-iJklmNo_pqr
timeout: 10s
gotify:
endpoint: http://gotify.foo.com
token: Token123456

View File

@@ -15,6 +15,7 @@ type NotifEntry struct {
// Notif holds data necessary for notification configuration
type Notif struct {
Amqp *NotifAmqp `yaml:"amqp,omitempty" json:"amqp,omitempty"`
Discord *NotifDiscord `yaml:"discord,omitempty" json:"discord,omitempty"`
Gotify *NotifGotify `yaml:"gotify,omitempty" json:"gotify,omitempty"`
Mail *NotifMail `yaml:"mail,omitempty" json:"mail,omitempty"`
RocketChat *NotifRocketChat `yaml:"rocketchat,omitempty" json:"rocketchat,omitempty"`

View File

@@ -0,0 +1,25 @@
package model
import (
"time"
"github.com/crazy-max/diun/v4/pkg/utl"
)
// NotifDiscord holds Discord notification configuration details
type NotifDiscord struct {
WebhookURL string `yaml:"webhookURL,omitempty" json:"webhookURL,omitempty" validate:"required"`
Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"`
}
// GetDefaults gets the default values
func (s *NotifDiscord) GetDefaults() *NotifDiscord {
n := &NotifDiscord{}
n.SetDefaults()
return n
}
// SetDefaults sets the default values
func (s *NotifDiscord) SetDefaults() {
s.Timeout = utl.NewDuration(10 * time.Second)
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/crazy-max/diun/v4/internal/model"
"github.com/crazy-max/diun/v4/internal/notif/amqp"
"github.com/crazy-max/diun/v4/internal/notif/discord"
"github.com/crazy-max/diun/v4/internal/notif/gotify"
"github.com/crazy-max/diun/v4/internal/notif/mail"
"github.com/crazy-max/diun/v4/internal/notif/notifier"
@@ -41,6 +42,9 @@ func New(config *model.Notif, meta model.Meta) (*Client, error) {
if config.Amqp != nil {
c.notifiers = append(c.notifiers, amqp.New(config.Amqp, meta))
}
if config.Discord != nil {
c.notifiers = append(c.notifiers, discord.New(config.Discord, meta))
}
if config.Gotify != nil {
c.notifiers = append(c.notifiers, gotify.New(config.Gotify, meta))
}

View File

@@ -0,0 +1,143 @@
package discord
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"text/template"
"time"
"github.com/crazy-max/diun/v4/internal/model"
"github.com/crazy-max/diun/v4/internal/notif/notifier"
)
// Client represents an active discord notification object
type Client struct {
*notifier.Notifier
cfg *model.NotifDiscord
meta model.Meta
}
// New creates a new discord notification instance
func New(config *model.NotifDiscord, meta model.Meta) notifier.Notifier {
return notifier.Notifier{
Handler: &Client{
cfg: config,
meta: meta,
},
}
}
// Name returns notifier's name
func (c *Client) Name() string {
return "discord"
}
// Send creates and sends a discord notification with an entry
// https://discord.com/developers/docs/resources/webhook#execute-webhook
func (c *Client) Send(entry model.NotifEntry) error {
hc := http.Client{
Timeout: *c.cfg.Timeout,
}
content := fmt.Sprintf("@here Image update for %s", entry.Image.String())
if entry.Status == model.ImageStatusNew {
content = fmt.Sprintf("@here New image %s has been added", entry.Image.String())
}
tagTpl := "**{{ .Entry.Image.Domain }}/{{ .Entry.Image.Path }}:{{ .Entry.Image.Tag }}**"
if len(entry.Image.HubLink) > 0 {
tagTpl = "[**{{ .Entry.Image.Domain }}/{{ .Entry.Image.Path }}:{{ .Entry.Image.Tag }}**]({{ .Entry.Image.HubLink }})"
}
var textBuf bytes.Buffer
textTpl := template.Must(template.New("discord").Parse(fmt.Sprintf(`Docker tag %s which you subscribed to through **{{ .Entry.Provider }}** provider has been {{ if (eq .Entry.Status "new") }}newly added{{ else }}updated{{ end }} on **myserver**.`, tagTpl)))
if err := textTpl.Execute(&textBuf, struct {
Meta model.Meta
Entry model.NotifEntry
}{
Meta: c.meta,
Entry: entry,
}); err != nil {
return err
}
fields := []EmbedField{
{
Name: "Hostname",
Value: c.meta.Hostname,
},
{
Name: "Provider",
Value: entry.Provider,
},
{
Name: "Created",
Value: entry.Manifest.Created.Format("Jan 02, 2006 15:04:05 UTC"),
},
{
Name: "Digest",
Value: entry.Manifest.Digest.String(),
},
{
Name: "Platform",
Value: entry.Manifest.Platform,
},
}
if len(entry.Image.HubLink) > 0 {
fields = append(fields, EmbedField{
Name: "HubLink",
Value: entry.Image.HubLink,
})
}
dataBuf := new(bytes.Buffer)
if err := json.NewEncoder(dataBuf).Encode(Message{
Content: content,
Username: c.meta.Name,
AvatarURL: c.meta.Logo,
Embeds: []Embed{
{
Description: textBuf.String(),
Footer: EmbedFooter{
Text: fmt.Sprintf("%s © %d %s %s", c.meta.Author, time.Now().Year(), c.meta.Name, c.meta.Version),
IconURL: c.meta.Logo,
},
Author: EmbedAuthor{
Name: c.meta.Name,
URL: c.meta.URL,
IconURL: c.meta.Logo,
},
Fields: fields,
},
},
}); err != nil {
return err
}
u, err := url.Parse(c.cfg.WebhookURL)
if err != nil {
return err
}
req, err := http.NewRequest("POST", u.String(), dataBuf)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", c.meta.UserAgent)
resp, err := hc.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("unexpected HTTP status %d: %s", resp.StatusCode, resp.Body)
}
return nil
}

View File

@@ -0,0 +1,52 @@
package discord
// Message contains all the information for a message
type Message struct {
Content string `json:"content"`
Username string `json:"username"`
AvatarURL string `json:"avatar_url"`
Embeds []Embed `json:"embeds"`
}
// Embed contains all the information for an embed object
type Embed struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
URL string `json:"url,omitempty"`
Color int `json:"color,omitempty"`
Footer EmbedFooter `json:"footer,omitempty"`
Image EmbedImage `json:"image,omitempty"`
Thumbnail EmbedThumbnail `json:"thumbnail,omitempty"`
Author EmbedAuthor `json:"author,omitempty"`
Fields []EmbedField `json:"fields,omitempty"`
}
// EmbedFooter contains all the information for an embed footer object
type EmbedFooter struct {
Text string `json:"text"`
IconURL string `json:"icon_url"`
}
// EmbedImage contains all the information for an embed image object
type EmbedImage struct {
URL string `json:"url"`
}
// EmbedThumbnail contains all the information for an embed thumbnail object
type EmbedThumbnail struct {
URL string `json:"url"`
}
// EmbedAuthor contains all the information for an embed author object
type EmbedAuthor struct {
Name string `json:"name"`
URL string `json:"url"`
IconURL string `json:"icon_url"`
}
// EmbedField contains all the information for an embed field object
type EmbedField struct {
Name string `json:"name"`
Value string `json:"value"`
Inline bool `json:"inline,omitempty"`
}

View File

@@ -66,6 +66,7 @@ nav:
- .providers: config/providers.md
- Notifications:
- Amqp: notif/amqp.md
- Discord: notif/discord.md
- Gotify: notif/gotify.md
- Mail: notif/mail.md
- Rocket.Chat: notif/rocketchat.md