Switch to config-file, support multiple notifiers of same type
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
20b0969ca4
commit
bd35172bb4
13 changed files with 341 additions and 117 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
birthday-notifier
|
||||
config.yaml
|
||||
|
|
111
README.md
111
README.md
|
@ -17,30 +17,97 @@ Hosted somewhere it's always running and configured properly and birthday notifi
|
|||
```console
|
||||
# birthday-notifier --help
|
||||
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 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
|
||||
--webdav-principal string Principal format to fetch the addressbooks for (%s will be replaced with the webdav-user) (default "principals/users/%s")
|
||||
--webdav-user string Username for Webdav login
|
||||
-c, --config string Configuration file path (default "config.yaml")
|
||||
--log-level string Log level (debug, info, warn, error, fatal) (default "info")
|
||||
--version Prints current version and exits
|
||||
```
|
||||
|
||||
For Nextcloud leave the principal format the default, for other systems you might need to adjust it.
|
||||
## Configuration
|
||||
|
||||
To adjust the notification text see the template in [`pkg/formatter/formatter.go`](./pkg/formatter/formatter.go) and provide your own as `NOTIFICATION_TEMPLATE` environment variable.
|
||||
```yaml
|
||||
# Specify days before the actual birthday to send advance notifications
|
||||
# i.e. to buy gifts or something. Default is to send only on the actual
|
||||
# birthday itself.
|
||||
notifyDaysInAdvance: [ 1 ]
|
||||
|
||||
### Notifier configuration
|
||||
# Configure how to notify you when there is a birthday pending / today.
|
||||
# Each entry consists of a type and the settings for that kind of
|
||||
# notifier. For settings and available types see below.
|
||||
notifiers:
|
||||
- type: slack
|
||||
settings:
|
||||
webhook: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
- **`log`** - Just sends the notification to the console logs, no configuration available
|
||||
- **`pushover`** - Send notification via [Pushover](https://pushover.net)
|
||||
- `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
|
||||
# Specify your own template for the notification text. The default is
|
||||
# shown below and whould yield something like this:
|
||||
#
|
||||
# Ava has their birthday on Wed, 13 Mar. They are turning 27.
|
||||
template: >-
|
||||
{{ .contact | getName }} has their birthday
|
||||
{{ if .when | isToday -}} today {{- else -}}
|
||||
on {{ (.when | projectToNext).Format "Mon, 02 Jan" }} {{- end }}.
|
||||
{{ if gt .when.Year 1 -}}They are turning {{ .when | getAge }}.{{- end }}
|
||||
|
||||
# Configure how to connect to the CardDAV addressbooks inside the
|
||||
# webdav server
|
||||
webdav:
|
||||
# Base-URL for the webdav server (example for Nextcloud)
|
||||
baseURL: https://my-nextcloud.example.com/remote.php/dav/
|
||||
# How often to fetch new birthdays (default: 1h)
|
||||
fetchInterval: 1h
|
||||
# Password for the user
|
||||
pass: 'my super secret password'
|
||||
# Principal format for the webdav server (default as below is valid
|
||||
# for Nextcloud instances): `%s` will be replaced with the value of
|
||||
# the user field below.
|
||||
principal: 'principals/users/%s'
|
||||
# Username for the login to the webdav server
|
||||
user: 'my.username'
|
||||
```
|
||||
|
||||
### Notifiers
|
||||
|
||||
#### `log`
|
||||
|
||||
Just sends the notification to the console logs
|
||||
|
||||
```yaml
|
||||
notifiers:
|
||||
- type: log
|
||||
# No settings for this one
|
||||
```
|
||||
|
||||
#### `pushover`
|
||||
|
||||
Send notification via [Pushover](https://pushover.net)
|
||||
|
||||
```yaml
|
||||
notifiers:
|
||||
- type: pushover
|
||||
settings:
|
||||
# Token for the App you've created in the Pushover Dashboard
|
||||
apiToken: '...'
|
||||
# Token for the User to send the notification to
|
||||
userKey: '...'
|
||||
# (Optional) Specify a sound to use
|
||||
sound: ''
|
||||
```
|
||||
|
||||
#### `slack`
|
||||
|
||||
Send notification through Slack(-compatible) webhook
|
||||
|
||||
```yaml
|
||||
notifiers:
|
||||
- type: slack
|
||||
settings:
|
||||
# Webhook URL (i.e. `https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX`
|
||||
# or `https://discord.com/api/webhooks/00000/XXXXX/slack`)
|
||||
webhook: 'https://...'
|
||||
# (Optional) Specify the channel to send to
|
||||
channel: ''
|
||||
# (Optional) Emoji to use as user icon
|
||||
iconEmoji: ''
|
||||
# (Optional) Overwrite the hooks username\
|
||||
username: ''
|
||||
```
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/config"
|
||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/dateutil"
|
||||
"github.com/emersion/go-vcard"
|
||||
"github.com/emersion/go-webdav"
|
||||
|
@ -20,10 +21,10 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func fetchBirthdays() (birthdays []birthdayEntry, err error) {
|
||||
func fetchBirthdays(webdavConfig config.WebdavConfig) (birthdays []birthdayEntry, err error) {
|
||||
client, err := carddav.NewClient(
|
||||
webdav.HTTPClientWithBasicAuth(http.DefaultClient, cfg.WebdavUser, cfg.WebdavPass),
|
||||
cfg.WebdavBaseURL,
|
||||
webdav.HTTPClientWithBasicAuth(http.DefaultClient, webdavConfig.User, webdavConfig.Pass),
|
||||
webdavConfig.BaseURL,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating carddav client: %w", err)
|
||||
|
@ -31,7 +32,7 @@ func fetchBirthdays() (birthdays []birthdayEntry, err error) {
|
|||
|
||||
homeSet, err := client.FindAddressBookHomeSet(
|
||||
context.Background(),
|
||||
fmt.Sprintf(cfg.WebdavPrincipal, cfg.WebdavUser),
|
||||
fmt.Sprintf(webdavConfig.Principal, webdavConfig.User),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting addressbook-home-set: %w", err)
|
||||
|
|
5
go.mod
5
go.mod
|
@ -3,6 +3,7 @@ module git.luzifer.io/luzifer/birthday-notifier
|
|||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/Luzifer/go_helpers/v2 v2.23.0
|
||||
github.com/Luzifer/rconfig/v2 v2.5.0
|
||||
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
|
||||
github.com/emersion/go-webdav v0.5.0
|
||||
|
@ -11,13 +12,13 @@ require (
|
|||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.8.4
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
11
go.sum
11
go.sum
|
@ -1,3 +1,5 @@
|
|||
github.com/Luzifer/go_helpers/v2 v2.23.0 h1:VowDwOCl6nOt+GVqKUX/do6a94pEeqNTRHb29MsoGX4=
|
||||
github.com/Luzifer/go_helpers/v2 v2.23.0/go.mod h1:BSGkJ/dxqs7AxsfZt8zjJb4R6YB5dONS+/ad7foLUrk=
|
||||
github.com/Luzifer/rconfig/v2 v2.5.0 h1:zx5lfQbNX3za4VegID97IeY+M+BmfgHxWJTYA94sxok=
|
||||
github.com/Luzifer/rconfig/v2 v2.5.0/go.mod h1:eGWUPQeCPv/Pr/p0hjmwFgI20uqvwi/Szen69hUzGzU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -12,8 +14,8 @@ github.com/gregdel/pushover v1.3.0 h1:CewbxqsThoN/1imgwkDKFkRkltaQMoyBV0K9IquQLt
|
|||
github.com/gregdel/pushover v1.3.0/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
@ -30,13 +32,16 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
|
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
|
||||
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
|
||||
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
94
main.go
94
main.go
|
@ -6,27 +6,24 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/config"
|
||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/dateutil"
|
||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/formatter"
|
||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/notifier"
|
||||
"github.com/emersion/go-vcard"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||
"github.com/Luzifer/rconfig/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg = struct {
|
||||
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 (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)"`
|
||||
WebdavUser string `flag:"webdav-user" default:"" description:"Username for Webdav login"`
|
||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||
Config string `flag:"config,c" default:"config.yaml" description:"Configuration file path"`
|
||||
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||
}{}
|
||||
|
||||
birthdays []birthdayEntry
|
||||
|
@ -61,33 +58,40 @@ func main() {
|
|||
os.Exit(0)
|
||||
}
|
||||
|
||||
var notifiers []notifier.Notifier
|
||||
for _, nv := range cfg.NotifyVia {
|
||||
notify := getNotifierByName(nv)
|
||||
if notify == nil {
|
||||
logrus.Fatal("unknown notifier specified")
|
||||
}
|
||||
notifiers = append(notifiers, notify)
|
||||
configFile, err := config.LoadFromFile(cfg.Config)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("loading configuration file")
|
||||
}
|
||||
|
||||
if birthdays, err = fetchBirthdays(); err != nil {
|
||||
if err = validateNotifierConfigs(configFile); err != nil {
|
||||
logrus.WithError(err).Fatal("validating configuration")
|
||||
}
|
||||
|
||||
if err = formatter.SetTemplate(configFile.Template); err != nil {
|
||||
logrus.WithError(err).Fatal("setting template")
|
||||
}
|
||||
|
||||
if birthdays, err = fetchBirthdays(configFile.Webdav); err != nil {
|
||||
logrus.WithError(err).Fatal("initially fetching birthdays")
|
||||
}
|
||||
|
||||
crontab := cron.New()
|
||||
|
||||
// Periodically update birthdays
|
||||
if _, err = crontab.AddFunc(fmt.Sprintf("@every %s", cfg.FetchInterval), cronFetchBirthdays); err != nil {
|
||||
if _, err = crontab.AddFunc(
|
||||
fmt.Sprintf("@every %s", configFile.Webdav.FetchInterval),
|
||||
cronFetchBirthdays(configFile.Webdav),
|
||||
); err != nil {
|
||||
logrus.WithError(err).Fatal("adding update-cron")
|
||||
}
|
||||
|
||||
// Send notifications at midnight
|
||||
if _, err = crontab.AddFunc("@midnight", cronSendNotifications(notifiers)); err != nil {
|
||||
if _, err = crontab.AddFunc("@midnight", cronSendNotifications(configFile)); err != nil {
|
||||
logrus.WithError(err).Fatal("adding update-cron")
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"advance": cfg.NotifyDaysInAdvance,
|
||||
"advance": configFile.NotifyDaysInAdvance,
|
||||
"version": version,
|
||||
}).Info("birthday-notifier started")
|
||||
crontab.Start()
|
||||
|
@ -97,36 +101,45 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
func cronFetchBirthdays() {
|
||||
birthdaysLock.Lock()
|
||||
defer birthdaysLock.Unlock()
|
||||
func cronFetchBirthdays(webdavConfig config.WebdavConfig) func() {
|
||||
return func() {
|
||||
birthdaysLock.Lock()
|
||||
defer birthdaysLock.Unlock()
|
||||
|
||||
var err error
|
||||
if birthdays, err = fetchBirthdays(); err != nil {
|
||||
logrus.WithError(err).Error("updating birthdays")
|
||||
var err error
|
||||
if birthdays, err = fetchBirthdays(webdavConfig); err != nil {
|
||||
logrus.WithError(err).Error("updating birthdays")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cronSendNotifications(notifiers []notifier.Notifier) func() {
|
||||
func cronSendNotifications(configFile config.File) func() {
|
||||
return func() {
|
||||
birthdaysLock.Lock()
|
||||
defer birthdaysLock.Unlock()
|
||||
|
||||
for _, b := range birthdays {
|
||||
for _, advanceDays := range append(cfg.NotifyDaysInAdvance, 0) {
|
||||
for _, advanceDays := range append(configFile.NotifyDaysInAdvance, 0) {
|
||||
if !dateutil.IsToday(notifyDate(dateutil.ProjectToNextBirthday(b.birthday), advanceDays)) {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range notifiers {
|
||||
go func(n notifier.Notifier, contact vcard.Card, when time.Time) {
|
||||
if err := n.SendNotification(contact, when); err != nil {
|
||||
for i := range configFile.Notifiers {
|
||||
notifyInstance := getNotifierByName(configFile.Notifiers[i].Type)
|
||||
|
||||
go func(
|
||||
n notifier.Notifier,
|
||||
settings *fieldcollection.FieldCollection,
|
||||
contact vcard.Card,
|
||||
when time.Time,
|
||||
) {
|
||||
if err := n.SendNotification(settings, contact, when); err != nil {
|
||||
logrus.
|
||||
WithError(err).
|
||||
WithField("name", contact.Get(vcard.FieldFormattedName).Value).
|
||||
Error("sending notification")
|
||||
}
|
||||
}(notifiers[i], b.contact, b.birthday)
|
||||
}(notifyInstance, configFile.Notifiers[i].Settings, b.contact, b.birthday)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -136,3 +149,20 @@ func cronSendNotifications(notifiers []notifier.Notifier) func() {
|
|||
func notifyDate(t time.Time, daysInAdvance int) time.Time {
|
||||
return time.Date(t.Year(), t.Month(), t.Day()-daysInAdvance, 0, 0, 0, 0, time.Local)
|
||||
}
|
||||
|
||||
func validateNotifierConfigs(configFile config.File) (err error) {
|
||||
for i := range configFile.Notifiers {
|
||||
notifierCfg := configFile.Notifiers[i]
|
||||
|
||||
n := getNotifierByName(notifierCfg.Type)
|
||||
if n == nil {
|
||||
return fmt.Errorf("notifier %q does not exist", notifierCfg.Type)
|
||||
}
|
||||
|
||||
if err = n.ValidateSettings(notifierCfg.Settings); err != nil {
|
||||
return fmt.Errorf("settings for %q are invalid: %w", notifierCfg.Type, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
90
pkg/config/config.go
Normal file
90
pkg/config/config.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
// Package config contains parser and structure for the configuration
|
||||
// of the tool
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/formatter"
|
||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// WebdavPrincipalNextcloud is the principal default used for config
|
||||
// files on parse and represents the principal format used by Nextcloud
|
||||
const WebdavPrincipalNextcloud = "principals/users/%s"
|
||||
|
||||
type (
|
||||
// File contains the structure of the YAML configuration file
|
||||
File struct {
|
||||
NotifyDaysInAdvance []int `yaml:"notifyDaysInAdvance"`
|
||||
Notifiers []NotifierConfig `yaml:"notifiers"`
|
||||
|
||||
Template string `yaml:"template"`
|
||||
|
||||
Webdav WebdavConfig `yaml:"webdav"`
|
||||
}
|
||||
|
||||
// NotifierConfig contains the type of the notifier and the settings
|
||||
// for it required to execute
|
||||
NotifierConfig struct {
|
||||
Type string `yaml:"type"`
|
||||
Settings *fieldcollection.FieldCollection `yaml:"settings"`
|
||||
}
|
||||
|
||||
// WebdavConfig defines how to interact with the Webdav server
|
||||
WebdavConfig struct {
|
||||
BaseURL string `yaml:"baseURL"`
|
||||
FetchInterval time.Duration `yaml:"fetchInterval"`
|
||||
Pass string `yaml:"pass"`
|
||||
Principal string `yaml:"principal"`
|
||||
User string `yaml:"user"`
|
||||
}
|
||||
)
|
||||
|
||||
// Load parses the given reader over a default configuration replacing
|
||||
// the fields specified in the reader
|
||||
func Load(r io.Reader) (f File, err error) {
|
||||
f = defaultConfig()
|
||||
dec := yaml.NewDecoder(r)
|
||||
|
||||
dec.KnownFields(true)
|
||||
if err = dec.Decode(&f); err != nil {
|
||||
return f, fmt.Errorf("decoding yaml: %w", err)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// LoadFromFile is a convenience wrapper around Load to read the config
|
||||
// from file system
|
||||
func LoadFromFile(filePath string) (f File, err error) {
|
||||
inFile, err := os.Open(filePath) //#nosec G304 -- Intended to load a given path
|
||||
if err != nil {
|
||||
return f, fmt.Errorf("opening file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := inFile.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing config file (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
return Load(inFile)
|
||||
}
|
||||
|
||||
func defaultConfig() File {
|
||||
return File{
|
||||
NotifyDaysInAdvance: nil,
|
||||
|
||||
Template: formatter.DefaultTemplate,
|
||||
|
||||
Webdav: WebdavConfig{
|
||||
FetchInterval: time.Hour,
|
||||
Principal: WebdavPrincipalNextcloud,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ package formatter
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
@ -18,7 +17,9 @@ import (
|
|||
const timeDay = 24 * time.Hour
|
||||
|
||||
var (
|
||||
defaultTemplate = regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(strings.ReplaceAll(`
|
||||
// DefaultTemplate contains the template used in testing and as a
|
||||
// default in the config package
|
||||
DefaultTemplate = regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(strings.ReplaceAll(`
|
||||
{{ .contact | getName }} has their birthday {{ if .when | isToday -}} today {{- else -}} on {{ (.when | projectToNext).Format "Mon, 02 Jan" }} {{- end }}.
|
||||
{{ if gt .when.Year 1 -}}They are turning {{ .when | getAge }}.{{- end }}
|
||||
`, "\n", " ")), " ")
|
||||
|
@ -26,24 +27,6 @@ var (
|
|||
notifyTpl *template.Template
|
||||
)
|
||||
|
||||
func init() {
|
||||
rawTpl := defaultTemplate
|
||||
if tpl := os.Getenv("NOTIFICATION_TEMPLATE"); tpl != "" {
|
||||
rawTpl = tpl
|
||||
}
|
||||
|
||||
var err error
|
||||
notifyTpl, err = template.New("notification").Funcs(template.FuncMap{
|
||||
"getAge": getAge,
|
||||
"getName": getContactName,
|
||||
"isToday": dateutil.IsToday,
|
||||
"projectToNext": dateutil.ProjectToNextBirthday,
|
||||
}).Parse(rawTpl)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("parsing notification template: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
// FormatNotificationText takes the notification template and renders
|
||||
// the contact / birthday date into a text to submit in the notification
|
||||
func FormatNotificationText(contact vcard.Card, when time.Time) (text string, err error) {
|
||||
|
@ -75,6 +58,24 @@ func FormatNotificationTitle(contact vcard.Card) (title string) {
|
|||
return title
|
||||
}
|
||||
|
||||
// SetTemplate initializes the template to use in the
|
||||
// FormatNotificationText function. This MUST be called before first
|
||||
// use of the FormatNotificationText function.
|
||||
func SetTemplate(rawTpl string) error {
|
||||
var err error
|
||||
notifyTpl, err = template.New("notification").Funcs(template.FuncMap{
|
||||
"getAge": getAge,
|
||||
"getName": getContactName,
|
||||
"isToday": dateutil.IsToday,
|
||||
"projectToNext": dateutil.ProjectToNextBirthday,
|
||||
}).Parse(rawTpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing notification template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAge(t time.Time) int {
|
||||
return dateutil.ProjectToNextBirthday(t).Year() - t.Year()
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ func getTestVCard(t *testing.T, content string) vcard.Card {
|
|||
}
|
||||
|
||||
func TestFormatNotificationText(t *testing.T) {
|
||||
require.NoError(t, SetTemplate(DefaultTemplate))
|
||||
|
||||
card := getTestVCard(t, `BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
N:Bloggs;Joe;;;
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/formatter"
|
||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/notifier"
|
||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||
"github.com/emersion/go-vcard"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -19,7 +20,7 @@ type (
|
|||
var _ notifier.Notifier = Notifier{}
|
||||
|
||||
// SendNotification implements the Notifier interface
|
||||
func (Notifier) SendNotification(contact vcard.Card, when time.Time) error {
|
||||
func (Notifier) SendNotification(_ *fieldcollection.FieldCollection, contact vcard.Card, when time.Time) error {
|
||||
if contact.Name() == nil {
|
||||
return fmt.Errorf("contact has no name")
|
||||
}
|
||||
|
@ -32,3 +33,9 @@ func (Notifier) SendNotification(contact vcard.Card, when time.Time) error {
|
|||
logrus.WithField("name", contact.Name().GivenName).Info(text)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSettings implements the Notifier interface
|
||||
func (Notifier) ValidateSettings(*fieldcollection.FieldCollection) error {
|
||||
// We don't take settings so everything is fine
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ package notifier
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||
"github.com/emersion/go-vcard"
|
||||
)
|
||||
|
||||
|
@ -13,7 +14,13 @@ type (
|
|||
// SendNotification will be called with the contact and the
|
||||
// time when the birthday actually is. The method is therefore
|
||||
// also called when a notification in advance is configured and
|
||||
// needs to properly format the notification for that.
|
||||
SendNotification(contact vcard.Card, when time.Time) error
|
||||
// needs to properly format the notification for that. The settings
|
||||
// passed through this call MUST NOT be stored.
|
||||
SendNotification(settings *fieldcollection.FieldCollection, contact vcard.Card, when time.Time) error
|
||||
|
||||
// ValidateSettings is called after configuration load to validate
|
||||
// the settings are suitable for the notifier and do not yield
|
||||
// surprising errors when doing the real notifications
|
||||
ValidateSettings(settings *fieldcollection.FieldCollection) error
|
||||
}
|
||||
)
|
||||
|
|
|
@ -4,11 +4,11 @@ package pushover
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/formatter"
|
||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/notifier"
|
||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||
"github.com/emersion/go-vcard"
|
||||
"github.com/gregdel/pushover"
|
||||
)
|
||||
|
@ -18,26 +18,18 @@ type (
|
|||
Notifier struct{}
|
||||
)
|
||||
|
||||
var _ notifier.Notifier = Notifier{}
|
||||
var (
|
||||
ptrStrEmpty = func(v string) *string { return &v }("")
|
||||
|
||||
_ notifier.Notifier = Notifier{}
|
||||
)
|
||||
|
||||
// SendNotification implements the Notifier interface
|
||||
func (Notifier) SendNotification(contact vcard.Card, when time.Time) error {
|
||||
func (Notifier) SendNotification(settings *fieldcollection.FieldCollection, contact vcard.Card, when time.Time) error {
|
||||
if contact.Name() == nil {
|
||||
return fmt.Errorf("contact has no name")
|
||||
}
|
||||
|
||||
var (
|
||||
apiToken = os.Getenv("PUSHOVER_API_TOKEN")
|
||||
userKey = os.Getenv("PUSHOVER_USER_KEY")
|
||||
)
|
||||
|
||||
if apiToken == "" {
|
||||
return fmt.Errorf("missing PUSHOVER_API_TOKEN env variable")
|
||||
}
|
||||
if userKey == "" {
|
||||
return fmt.Errorf("missing PUSHOVER_USER_KEY env variable")
|
||||
}
|
||||
|
||||
text, err := formatter.FormatNotificationText(contact, when)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rendering text: %w", err)
|
||||
|
@ -46,13 +38,26 @@ func (Notifier) SendNotification(contact vcard.Card, when time.Time) error {
|
|||
message := &pushover.Message{
|
||||
Message: text,
|
||||
Title: formatter.FormatNotificationTitle(contact),
|
||||
Sound: os.Getenv("PUSHOVER_SOUND"),
|
||||
Sound: settings.MustString("sound", ptrStrEmpty),
|
||||
}
|
||||
|
||||
if _, err = pushover.New(apiToken).
|
||||
SendMessage(message, pushover.NewRecipient(userKey)); err != nil {
|
||||
if _, err = pushover.New(settings.MustString("apiToken", nil)).
|
||||
SendMessage(message, pushover.NewRecipient(settings.MustString("userKey", nil))); err != nil {
|
||||
return fmt.Errorf("sending notification: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSettings implements the Notifier interface
|
||||
func (Notifier) ValidateSettings(settings *fieldcollection.FieldCollection) (err error) {
|
||||
if v, err := settings.String("apiToken"); err != nil || v == "" {
|
||||
return fmt.Errorf("apiToken is expected to be non-empty string")
|
||||
}
|
||||
|
||||
if v, err := settings.String("userKey"); err != nil || v == "" {
|
||||
return fmt.Errorf("userKey is expected to be non-empty string")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@ import (
|
|||
"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/Luzifer/go_helpers/v2/fieldcollection"
|
||||
"github.com/emersion/go-vcard"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -24,20 +24,18 @@ type (
|
|||
Notifier struct{}
|
||||
)
|
||||
|
||||
var _ notifier.Notifier = Notifier{}
|
||||
var (
|
||||
ptrStrEmpty = func(v string) *string { return &v }("")
|
||||
|
||||
_ notifier.Notifier = Notifier{}
|
||||
)
|
||||
|
||||
// SendNotification implements the Notifier interface
|
||||
func (Notifier) SendNotification(contact vcard.Card, when time.Time) error {
|
||||
func (Notifier) SendNotification(settings *fieldcollection.FieldCollection, 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)
|
||||
|
@ -50,10 +48,10 @@ func (Notifier) SendNotification(contact vcard.Card, when time.Time) error {
|
|||
Text string `json:"text"`
|
||||
Username string `json:"username,omitempty"`
|
||||
}{
|
||||
Channel: os.Getenv("SLACK_CHANNEL"),
|
||||
IconEmoji: os.Getenv("SLACK_ICON_EMOJI"),
|
||||
Channel: settings.MustString("channel", ptrStrEmpty),
|
||||
IconEmoji: settings.MustString("iconEmoji", ptrStrEmpty),
|
||||
Text: text,
|
||||
Username: os.Getenv("SLACK_USERNAME"),
|
||||
Username: settings.MustString("username", ptrStrEmpty),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("encoding hook payload: %w", err)
|
||||
}
|
||||
|
@ -61,7 +59,7 @@ func (Notifier) SendNotification(contact vcard.Card, when time.Time) error {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), webhookPostTimeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, webhookURL, payload)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, settings.MustString("webhook", nil), payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
@ -83,3 +81,12 @@ func (Notifier) SendNotification(contact vcard.Card, when time.Time) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSettings implements the Notifier interface
|
||||
func (Notifier) ValidateSettings(settings *fieldcollection.FieldCollection) (err error) {
|
||||
if v, err := settings.String("webhook"); err != nil || v == "" {
|
||||
return fmt.Errorf("webhook is expected to be non-empty string")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue