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
|
birthday-notifier
|
||||||
|
config.yaml
|
||||||
|
|
107
README.md
107
README.md
|
@ -17,30 +17,97 @@ Hosted somewhere it's always running and configured properly and birthday notifi
|
||||||
```console
|
```console
|
||||||
# birthday-notifier --help
|
# birthday-notifier --help
|
||||||
Usage of birthday-notifier:
|
Usage of birthday-notifier:
|
||||||
--fetch-interval duration How often to fetch birthdays from CardDAV (default 1h0m0s)
|
-c, --config string Configuration file path (default "config.yaml")
|
||||||
--log-level string Log level (debug, info, warn, error, fatal) (default "info")
|
--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
|
--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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
# Specify your own template for the notification text. The default is
|
||||||
- **`pushover`** - Send notification via [Pushover](https://pushover.net)
|
# shown below and whould yield something like this:
|
||||||
- `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
|
# Ava has their birthday on Wed, 13 Mar. They are turning 27.
|
||||||
- `PUSHOVER_SOUND` - (Optional) Specify a sound to use
|
template: >-
|
||||||
- **`slack`** - Send notification through Slack(-compatible) webhook
|
{{ .contact | getName }} has their birthday
|
||||||
- `SLACK_WEBHOOK` - Webhook URL (i.e. `https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX` or `https://discord.com/api/webhooks/00000/XXXXX/slack`)
|
{{ if .when | isToday -}} today {{- else -}}
|
||||||
- `SLACK_CHANNEL` - (Optional) Specify the channel to send to
|
on {{ (.when | projectToNext).Format "Mon, 02 Jan" }} {{- end }}.
|
||||||
- `SLACK_ICON_EMOJI` - (Optional) Emoji to use as user icon
|
{{ if gt .when.Year 1 -}}They are turning {{ .when | getAge }}.{{- end }}
|
||||||
- `SLACK_USERNAME` - (Optional) Overwrite the hooks username
|
|
||||||
|
# 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"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/config"
|
||||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/dateutil"
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/dateutil"
|
||||||
"github.com/emersion/go-vcard"
|
"github.com/emersion/go-vcard"
|
||||||
"github.com/emersion/go-webdav"
|
"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(
|
client, err := carddav.NewClient(
|
||||||
webdav.HTTPClientWithBasicAuth(http.DefaultClient, cfg.WebdavUser, cfg.WebdavPass),
|
webdav.HTTPClientWithBasicAuth(http.DefaultClient, webdavConfig.User, webdavConfig.Pass),
|
||||||
cfg.WebdavBaseURL,
|
webdavConfig.BaseURL,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating carddav client: %w", err)
|
return nil, fmt.Errorf("creating carddav client: %w", err)
|
||||||
|
@ -31,7 +32,7 @@ func fetchBirthdays() (birthdays []birthdayEntry, err error) {
|
||||||
|
|
||||||
homeSet, err := client.FindAddressBookHomeSet(
|
homeSet, err := client.FindAddressBookHomeSet(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
fmt.Sprintf(cfg.WebdavPrincipal, cfg.WebdavUser),
|
fmt.Sprintf(webdavConfig.Principal, webdavConfig.User),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting addressbook-home-set: %w", err)
|
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
|
go 1.22.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Luzifer/go_helpers/v2 v2.23.0
|
||||||
github.com/Luzifer/rconfig/v2 v2.5.0
|
github.com/Luzifer/rconfig/v2 v2.5.0
|
||||||
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
|
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
|
||||||
github.com/emersion/go-webdav v0.5.0
|
github.com/emersion/go-webdav v0.5.0
|
||||||
|
@ -11,13 +12,13 @@ require (
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // 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/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 h1:zx5lfQbNX3za4VegID97IeY+M+BmfgHxWJTYA94sxok=
|
||||||
github.com/Luzifer/rconfig/v2 v2.5.0/go.mod h1:eGWUPQeCPv/Pr/p0hjmwFgI20uqvwi/Szen69hUzGzU=
|
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=
|
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/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 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/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.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
|
||||||
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
|
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.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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
|
||||||
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
|
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
82
main.go
82
main.go
|
@ -6,26 +6,23 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/config"
|
||||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/dateutil"
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/dateutil"
|
||||||
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/formatter"
|
||||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/notifier"
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/notifier"
|
||||||
"github.com/emersion/go-vcard"
|
"github.com/emersion/go-vcard"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/Luzifer/rconfig/v2"
|
"github.com/Luzifer/rconfig/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfg = struct {
|
cfg = struct {
|
||||||
FetchInterval time.Duration `flag:"fetch-interval" default:"1h" description:"How often to fetch birthdays from CardDAV"`
|
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)"`
|
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"`
|
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
|
@ -61,33 +58,40 @@ func main() {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var notifiers []notifier.Notifier
|
configFile, err := config.LoadFromFile(cfg.Config)
|
||||||
for _, nv := range cfg.NotifyVia {
|
if err != nil {
|
||||||
notify := getNotifierByName(nv)
|
logrus.WithError(err).Fatal("loading configuration file")
|
||||||
if notify == nil {
|
|
||||||
logrus.Fatal("unknown notifier specified")
|
|
||||||
}
|
|
||||||
notifiers = append(notifiers, notify)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
logrus.WithError(err).Fatal("initially fetching birthdays")
|
||||||
}
|
}
|
||||||
|
|
||||||
crontab := cron.New()
|
crontab := cron.New()
|
||||||
|
|
||||||
// Periodically update birthdays
|
// 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")
|
logrus.WithError(err).Fatal("adding update-cron")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send notifications at midnight
|
// 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.WithError(err).Fatal("adding update-cron")
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"advance": cfg.NotifyDaysInAdvance,
|
"advance": configFile.NotifyDaysInAdvance,
|
||||||
"version": version,
|
"version": version,
|
||||||
}).Info("birthday-notifier started")
|
}).Info("birthday-notifier started")
|
||||||
crontab.Start()
|
crontab.Start()
|
||||||
|
@ -97,36 +101,45 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cronFetchBirthdays() {
|
func cronFetchBirthdays(webdavConfig config.WebdavConfig) func() {
|
||||||
|
return func() {
|
||||||
birthdaysLock.Lock()
|
birthdaysLock.Lock()
|
||||||
defer birthdaysLock.Unlock()
|
defer birthdaysLock.Unlock()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if birthdays, err = fetchBirthdays(); err != nil {
|
if birthdays, err = fetchBirthdays(webdavConfig); err != nil {
|
||||||
logrus.WithError(err).Error("updating birthdays")
|
logrus.WithError(err).Error("updating birthdays")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func cronSendNotifications(notifiers []notifier.Notifier) func() {
|
func cronSendNotifications(configFile config.File) func() {
|
||||||
return func() {
|
return func() {
|
||||||
birthdaysLock.Lock()
|
birthdaysLock.Lock()
|
||||||
defer birthdaysLock.Unlock()
|
defer birthdaysLock.Unlock()
|
||||||
|
|
||||||
for _, b := range birthdays {
|
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)) {
|
if !dateutil.IsToday(notifyDate(dateutil.ProjectToNextBirthday(b.birthday), advanceDays)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range notifiers {
|
for i := range configFile.Notifiers {
|
||||||
go func(n notifier.Notifier, contact vcard.Card, when time.Time) {
|
notifyInstance := getNotifierByName(configFile.Notifiers[i].Type)
|
||||||
if err := n.SendNotification(contact, when); err != nil {
|
|
||||||
|
go func(
|
||||||
|
n notifier.Notifier,
|
||||||
|
settings *fieldcollection.FieldCollection,
|
||||||
|
contact vcard.Card,
|
||||||
|
when time.Time,
|
||||||
|
) {
|
||||||
|
if err := n.SendNotification(settings, contact, when); err != nil {
|
||||||
logrus.
|
logrus.
|
||||||
WithError(err).
|
WithError(err).
|
||||||
WithField("name", contact.Get(vcard.FieldFormattedName).Value).
|
WithField("name", contact.Get(vcard.FieldFormattedName).Value).
|
||||||
Error("sending notification")
|
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 {
|
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)
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
@ -18,7 +17,9 @@ import (
|
||||||
const timeDay = 24 * time.Hour
|
const timeDay = 24 * time.Hour
|
||||||
|
|
||||||
var (
|
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 }}.
|
{{ .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 }}
|
{{ if gt .when.Year 1 -}}They are turning {{ .when | getAge }}.{{- end }}
|
||||||
`, "\n", " ")), " ")
|
`, "\n", " ")), " ")
|
||||||
|
@ -26,24 +27,6 @@ var (
|
||||||
notifyTpl *template.Template
|
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
|
// FormatNotificationText takes the notification template and renders
|
||||||
// the contact / birthday date into a text to submit in the notification
|
// the contact / birthday date into a text to submit in the notification
|
||||||
func FormatNotificationText(contact vcard.Card, when time.Time) (text string, err error) {
|
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
|
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 {
|
func getAge(t time.Time) int {
|
||||||
return dateutil.ProjectToNextBirthday(t).Year() - t.Year()
|
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) {
|
func TestFormatNotificationText(t *testing.T) {
|
||||||
|
require.NoError(t, SetTemplate(DefaultTemplate))
|
||||||
|
|
||||||
card := getTestVCard(t, `BEGIN:VCARD
|
card := getTestVCard(t, `BEGIN:VCARD
|
||||||
VERSION:4.0
|
VERSION:4.0
|
||||||
N:Bloggs;Joe;;;
|
N:Bloggs;Joe;;;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/formatter"
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/formatter"
|
||||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/notifier"
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/notifier"
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/emersion/go-vcard"
|
"github.com/emersion/go-vcard"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -19,7 +20,7 @@ type (
|
||||||
var _ notifier.Notifier = Notifier{}
|
var _ notifier.Notifier = Notifier{}
|
||||||
|
|
||||||
// SendNotification implements the Notifier interface
|
// 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 {
|
if contact.Name() == nil {
|
||||||
return fmt.Errorf("contact has no name")
|
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)
|
logrus.WithField("name", contact.Name().GivenName).Info(text)
|
||||||
return nil
|
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 (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/emersion/go-vcard"
|
"github.com/emersion/go-vcard"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,7 +14,13 @@ type (
|
||||||
// SendNotification will be called with the contact and the
|
// SendNotification will be called with the contact and the
|
||||||
// time when the birthday actually is. The method is therefore
|
// time when the birthday actually is. The method is therefore
|
||||||
// also called when a notification in advance is configured and
|
// also called when a notification in advance is configured and
|
||||||
// needs to properly format the notification for that.
|
// needs to properly format the notification for that. The settings
|
||||||
SendNotification(contact vcard.Card, when time.Time) error
|
// 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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/formatter"
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/formatter"
|
||||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/notifier"
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/notifier"
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/emersion/go-vcard"
|
"github.com/emersion/go-vcard"
|
||||||
"github.com/gregdel/pushover"
|
"github.com/gregdel/pushover"
|
||||||
)
|
)
|
||||||
|
@ -18,24 +18,16 @@ type (
|
||||||
Notifier struct{}
|
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
apiToken = os.Getenv("PUSHOVER_API_TOKEN")
|
ptrStrEmpty = func(v string) *string { return &v }("")
|
||||||
userKey = os.Getenv("PUSHOVER_USER_KEY")
|
|
||||||
|
_ notifier.Notifier = Notifier{}
|
||||||
)
|
)
|
||||||
|
|
||||||
if apiToken == "" {
|
// SendNotification implements the Notifier interface
|
||||||
return fmt.Errorf("missing PUSHOVER_API_TOKEN env variable")
|
func (Notifier) SendNotification(settings *fieldcollection.FieldCollection, contact vcard.Card, when time.Time) error {
|
||||||
}
|
if contact.Name() == nil {
|
||||||
if userKey == "" {
|
return fmt.Errorf("contact has no name")
|
||||||
return fmt.Errorf("missing PUSHOVER_USER_KEY env variable")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
text, err := formatter.FormatNotificationText(contact, when)
|
text, err := formatter.FormatNotificationText(contact, when)
|
||||||
|
@ -46,13 +38,26 @@ func (Notifier) SendNotification(contact vcard.Card, when time.Time) error {
|
||||||
message := &pushover.Message{
|
message := &pushover.Message{
|
||||||
Message: text,
|
Message: text,
|
||||||
Title: formatter.FormatNotificationTitle(contact),
|
Title: formatter.FormatNotificationTitle(contact),
|
||||||
Sound: os.Getenv("PUSHOVER_SOUND"),
|
Sound: settings.MustString("sound", ptrStrEmpty),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = pushover.New(apiToken).
|
if _, err = pushover.New(settings.MustString("apiToken", nil)).
|
||||||
SendMessage(message, pushover.NewRecipient(userKey)); err != nil {
|
SendMessage(message, pushover.NewRecipient(settings.MustString("userKey", nil))); err != nil {
|
||||||
return fmt.Errorf("sending notification: %w", err)
|
return fmt.Errorf("sending notification: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/formatter"
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/formatter"
|
||||||
"git.luzifer.io/luzifer/birthday-notifier/pkg/notifier"
|
"git.luzifer.io/luzifer/birthday-notifier/pkg/notifier"
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
"github.com/emersion/go-vcard"
|
"github.com/emersion/go-vcard"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -24,20 +24,18 @@ type (
|
||||||
Notifier struct{}
|
Notifier struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ notifier.Notifier = Notifier{}
|
var (
|
||||||
|
ptrStrEmpty = func(v string) *string { return &v }("")
|
||||||
|
|
||||||
|
_ notifier.Notifier = Notifier{}
|
||||||
|
)
|
||||||
|
|
||||||
// SendNotification implements the Notifier interface
|
// 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 {
|
if contact.Name() == nil {
|
||||||
return fmt.Errorf("contact has no name")
|
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)
|
text, err := formatter.FormatNotificationText(contact, when)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("rendering text: %w", err)
|
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"`
|
Text string `json:"text"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
}{
|
}{
|
||||||
Channel: os.Getenv("SLACK_CHANNEL"),
|
Channel: settings.MustString("channel", ptrStrEmpty),
|
||||||
IconEmoji: os.Getenv("SLACK_ICON_EMOJI"),
|
IconEmoji: settings.MustString("iconEmoji", ptrStrEmpty),
|
||||||
Text: text,
|
Text: text,
|
||||||
Username: os.Getenv("SLACK_USERNAME"),
|
Username: settings.MustString("username", ptrStrEmpty),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("encoding hook payload: %w", err)
|
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)
|
ctx, cancel := context.WithTimeout(context.Background(), webhookPostTimeout)
|
||||||
defer cancel()
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("creating request: %w", err)
|
return fmt.Errorf("creating request: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -83,3 +81,12 @@ func (Notifier) SendNotification(contact vcard.Card, when time.Time) error {
|
||||||
|
|
||||||
return nil
|
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