From 8af4ff08a3721f9ebd9a3878c30baf5305400cc4 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Wed, 13 Mar 2024 11:41:15 +0100 Subject: [PATCH] Add Slack-Compatible webhook notification target Signed-off-by: Knut Ahlers --- README.md | 7 ++- main.go | 2 +- notifier.go | 4 ++ pkg/formatter/formatter.go | 16 ++++++ pkg/notifier/pushover/pushover.go | 13 +---- pkg/notifier/slack/slack.go | 85 +++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 pkg/notifier/slack/slack.go diff --git a/README.md b/README.md index cdd3591..012f82d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Usage of birthday-notifier: --fetch-interval duration How often to fetch birthdays from CardDAV (default 1h0m0s) --log-level string Log level (debug, info, warn, error, fatal) (default "info") --notify-days-in-advance ints Send notification X days before birthday (default [1]) - --notify-via string How to send the notification (one of: log, pushover) (default "log") + --notify-via strings How to send the notification (log, pushover, slack) (default [log]) --version Prints current version and exits --webdav-base-url string Webdav server to connect to --webdav-pass string Password for the Webdav user @@ -39,3 +39,8 @@ To adjust the notification text see the template in [`pkg/formatter/formatter.go - `PUSHOVER_API_TOKEN` - Token for the App you've created in the Pushover Dashboard - `PUSHOVER_USER_KEY` - Token for the User to send the notification to - `PUSHOVER_SOUND` - (Optional) Specify a sound to use +- **`slack`** - Send notification through Slack(-compatible) webhook + - `SLACK_WEBHOOK` - Webhook URL (i.e. `https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX` or `https://discord.com/api/webhooks/00000/XXXXX/slack`) + - `SLACK_CHANNEL` - (Optional) Specify the channel to send to + - `SLACK_ICON_EMOJI` - (Optional) Emoji to use as user icon + - `SLACK_USERNAME` - (Optional) Overwrite the hooks username diff --git a/main.go b/main.go index 6b11933..6e6ab00 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,7 @@ var ( FetchInterval time.Duration `flag:"fetch-interval" default:"1h" description:"How often to fetch birthdays from CardDAV"` LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` NotifyDaysInAdvance []int `flag:"notify-days-in-advance" default:"1" description:"Send notification X days before birthday"` - NotifyVia []string `flag:"notify-via" default:"log" description:"How to send the notification (one of: log, pushover)"` + NotifyVia []string `flag:"notify-via" default:"log" description:"How to send the notification (log, pushover, slack)"` WebdavBaseURL string `flag:"webdav-base-url" default:"" description:"Webdav server to connect to"` WebdavPass string `flag:"webdav-pass" default:"" description:"Password for the Webdav user"` WebdavPrincipal string `flag:"webdav-principal" default:"principals/users/%s" description:"Principal format to fetch the addressbooks for (%s will be replaced with the webdav-user)"` diff --git a/notifier.go b/notifier.go index ee825a6..41e402c 100644 --- a/notifier.go +++ b/notifier.go @@ -4,6 +4,7 @@ import ( "git.luzifer.io/luzifer/birthday-notifier/pkg/notifier" "git.luzifer.io/luzifer/birthday-notifier/pkg/notifier/log" "git.luzifer.io/luzifer/birthday-notifier/pkg/notifier/pushover" + "git.luzifer.io/luzifer/birthday-notifier/pkg/notifier/slack" ) func getNotifierByName(name string) notifier.Notifier { @@ -14,6 +15,9 @@ func getNotifierByName(name string) notifier.Notifier { case "pushover": return pushover.Notifier{} + case "slack": + return slack.Notifier{} + default: return nil } diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index 75bd44d..e731b8a 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -59,6 +59,22 @@ func FormatNotificationText(contact vcard.Card, when time.Time) (text string, er return buf.String(), nil } +// FormatNotificationTitle provides a title from the contacts formatted +// name or from given and family name +func FormatNotificationTitle(contact vcard.Card) (title string) { + for _, fn := range contact.FormattedNames() { + if fn.Value != "" { + title = fmt.Sprintf("%s (Birthday)", fn.Value) + } + } + + if title == "" { + title = fmt.Sprintf("%s %s (Birthday)", contact.Name().GivenName, contact.Name().FamilyName) + } + + return title +} + func getAge(t time.Time) int { return dateutil.ProjectToNextBirthday(t).Year() - t.Year() } diff --git a/pkg/notifier/pushover/pushover.go b/pkg/notifier/pushover/pushover.go index d70684e..02752fd 100644 --- a/pkg/notifier/pushover/pushover.go +++ b/pkg/notifier/pushover/pushover.go @@ -43,20 +43,9 @@ func (Notifier) SendNotification(contact vcard.Card, when time.Time) error { return fmt.Errorf("rendering text: %w", err) } - var title string - for _, fn := range contact.FormattedNames() { - if fn.Value != "" { - title = fmt.Sprintf("%s (Birthday)", fn.Value) - } - } - - if title == "" { - title = fmt.Sprintf("%s %s (Birthday)", contact.Name().GivenName, contact.Name().FamilyName) - } - message := &pushover.Message{ Message: text, - Title: title, + Title: formatter.FormatNotificationTitle(contact), Sound: os.Getenv("PUSHOVER_SOUND"), } diff --git a/pkg/notifier/slack/slack.go b/pkg/notifier/slack/slack.go new file mode 100644 index 0000000..04ea3d4 --- /dev/null +++ b/pkg/notifier/slack/slack.go @@ -0,0 +1,85 @@ +// Package slack provides a notifier to send birthday notifications +// through a Slack(-compatible) WebHook +package slack + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "time" + + "git.luzifer.io/luzifer/birthday-notifier/pkg/formatter" + "git.luzifer.io/luzifer/birthday-notifier/pkg/notifier" + "github.com/emersion/go-vcard" + "github.com/sirupsen/logrus" +) + +const webhookPostTimeout = 2 * time.Second + +type ( + // Notifier implements the notifier interface + Notifier struct{} +) + +var _ notifier.Notifier = Notifier{} + +// SendNotification implements the Notifier interface +func (Notifier) SendNotification(contact vcard.Card, when time.Time) error { + if contact.Name() == nil { + return fmt.Errorf("contact has no name") + } + + webhookURL := os.Getenv("SLACK_WEBHOOK") + + if webhookURL == "" { + return fmt.Errorf("missing SLACK_WEBHOOK env variable") + } + + text, err := formatter.FormatNotificationText(contact, when) + if err != nil { + return fmt.Errorf("rendering text: %w", err) + } + + payload := new(bytes.Buffer) + if err = json.NewEncoder(payload).Encode(struct { + Channel string `json:"channel,omitempty"` + IconEmoji string `json:"icon_emoji,omitempty"` + Text string `json:"text"` + Username string `json:"username,omitempty"` + }{ + Channel: os.Getenv("SLACK_CHANNEL"), + IconEmoji: os.Getenv("SLACK_ICON_EMOJI"), + Text: text, + Username: os.Getenv("SLACK_USERNAME"), + }); err != nil { + return fmt.Errorf("encoding hook payload: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), webhookPostTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, webhookURL, payload) + if err != nil { + return fmt.Errorf("creating request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("executing request: %w", err) + } + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.WithError(err).Error("closing slack response body (leaked fd)") + } + }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status %d", resp.StatusCode) + } + + return nil +}