From 15960cf14cf42214e7fd9829bb9b65eebf41356d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 6 Dec 2020 18:39:57 +0100 Subject: [PATCH] feat(services): migrate services to prop key resolver --- cli/cmd/generate/generate.go | 15 ++- cli/cmd/verify/verify.go | 3 +- internal/testutils/config.go | 5 +- pkg/format/format_query.go | 6 +- pkg/format/formatter.go | 4 +- pkg/format/prop_key_resolver.go | 105 +++++++++++++++++++ pkg/services/discord/discord_config.go | 1 - pkg/services/gotify/gotify_config.go | 1 - pkg/services/hangouts/hangouts_config.go | 1 - pkg/services/ifttt/ifttt.go | 18 +++- pkg/services/ifttt/ifttt_config.go | 98 +++++------------ pkg/services/ifttt/ifttt_test.go | 2 +- pkg/services/join/join.go | 5 +- pkg/services/join/join_config.go | 63 +++-------- pkg/services/join/join_test.go | 21 ++-- pkg/services/logger/logger.go | 2 +- pkg/services/logger/logger_config.go | 3 +- pkg/services/mattermost/mattermost_config.go | 1 - pkg/services/pushbullet/pushbullet_config.go | 1 - pkg/services/pushover/pushover.go | 33 +++--- pkg/services/pushover/pushover_config.go | 62 ++--------- pkg/services/pushover/pushover_test.go | 20 ++-- pkg/services/rocketchat/rocketchat_config.go | 4 +- pkg/services/slack/slack.go | 10 +- pkg/services/slack/slack_config.go | 26 ++--- pkg/services/slack/slack_token.go | 31 ++---- pkg/services/smtp/smtp.go | 11 +- pkg/services/smtp/smtp_config.go | 30 ++++-- pkg/services/smtp/smtp_test.go | 8 +- pkg/services/standard/key_config.go | 89 ---------------- pkg/services/standard/queryless_config.go | 23 ---- pkg/services/standard/standard_test.go | 21 ---- pkg/services/teams/teams_config.go | 1 - pkg/services/telegram/telegram.go | 5 +- pkg/services/telegram/telegram_config.go | 47 +++------ pkg/services/zulip/zulip.go | 2 +- pkg/services/zulip/zulip_config.go | 11 +- pkg/services/zulip/zulip_test.go | 9 +- pkg/types/service_config.go | 9 +- pkg/xmpp/xmpp.go | 18 ++-- pkg/xmpp/xmpp_config.go | 66 ++++-------- 41 files changed, 360 insertions(+), 531 deletions(-) create mode 100644 pkg/format/prop_key_resolver.go delete mode 100644 pkg/services/standard/key_config.go delete mode 100644 pkg/services/standard/queryless_config.go diff --git a/cli/cmd/generate/generate.go b/cli/cmd/generate/generate.go index 61c0c67b..dc6e39f7 100644 --- a/cli/cmd/generate/generate.go +++ b/cli/cmd/generate/generate.go @@ -85,7 +85,20 @@ func Run(cmd *cobra.Command, _ []string) { os.Exit(1) } - generator, err := generators.NewGenerator(generatorName) + var generator types.Generator + + var generatorFlag = cmd.Flags().Lookup("generator") + + if !generatorFlag.Changed { + // try to use the service default generator if one exists + generator, _ = generators.NewGenerator(serviceSchema) + } + + if generator != nil { + generatorName = serviceSchema + } else { + generator, err = generators.NewGenerator(generatorName) + } if err != nil { fmt.Printf("Error: %s\n", err) diff --git a/cli/cmd/verify/verify.go b/cli/cmd/verify/verify.go index de63bfb2..77f24ed5 100644 --- a/cli/cmd/verify/verify.go +++ b/cli/cmd/verify/verify.go @@ -42,7 +42,8 @@ func Run(cmd *cobra.Command, _ []string) { } configMap, maxKeyLen := format.GetConfigMap(service) - for key, value := range configMap { + for key, _ := range configMap { + value := configMap[key] pad := strings.Repeat(" ", maxKeyLen-len(key)) _, _ = fmt.Fprintf(color.Output, "%s%s: %s\n", pad, key, value) } diff --git a/internal/testutils/config.go b/internal/testutils/config.go index 02252627..64bff12d 100644 --- a/internal/testutils/config.go +++ b/internal/testutils/config.go @@ -1,6 +1,7 @@ package testutils import ( + "github.com/containrrr/shoutrrr/pkg/format" "net/url" Ω "github.com/onsi/gomega" @@ -10,7 +11,7 @@ import ( // TestConfigGetInvalidQueryValue tests whether the config returns an error when an invalid query value is requested func TestConfigGetInvalidQueryValue(config types.ServiceConfig) { - value, err := config.Get("invalid query var") + value, err := format.GetConfigQueryResolver(config).Get("invalid query var") Ω.ExpectWithOffset(1, value).To(Ω.BeEmpty()) Ω.ExpectWithOffset(1, err).To(Ω.HaveOccurred()) } @@ -32,6 +33,6 @@ func TestConfigGetEnumsCount(config types.ServiceConfig, expectedCount int) { // TestConfigGetFieldsCount tests whether the config.QueryFields return the expected amount of fields func TestConfigGetFieldsCount(config types.ServiceConfig, expectedCount int) { - fields := config.QueryFields() + fields := format.GetConfigQueryResolver(config).QueryFields() Ω.ExpectWithOffset(1, fields).To(Ω.HaveLen(expectedCount)) } diff --git a/pkg/format/format_query.go b/pkg/format/format_query.go index 8ebe3c83..cae270d0 100644 --- a/pkg/format/format_query.go +++ b/pkg/format/format_query.go @@ -6,12 +6,12 @@ import ( ) // BuildQuery converts the fields of a config object to a delimited query string -func BuildQuery(c types.ServiceConfig) string { +func BuildQuery(cqr types.ConfigQueryResolver) string { query := "" - fields := c.QueryFields() format := "%s=%s" + fields := cqr.QueryFields() for index, key := range fields { - value, _ := c.Get(key) + value, _ := cqr.Get(key) if index == 1 { format = "&%s=%s" } diff --git a/pkg/format/formatter.go b/pkg/format/formatter.go index 39124efb..8b3047af 100644 --- a/pkg/format/formatter.go +++ b/pkg/format/formatter.go @@ -23,7 +23,7 @@ func GetConfigMap(service types.Service) (map[string]string, int) { formatter := formatter{ EnumFormatters: config.Enums(), - MaxDepth: 2, + MaxDepth: 10, } return formatter.formatStructMap(configType, config, 0) } @@ -95,7 +95,7 @@ func (fmtr *formatter) formatStructMap(structType reflect.Type, structItem inter fmtr.Errors = append(fmtr.Errors, err) } } else if nextDepth < fmtr.MaxDepth { - value, valueLen = fmtr.getFieldValueString(values.Field(i), nextDepth) + value, valueLen = fmtr.getFieldValueString(values.FieldByName(field.Name), nextDepth) } } else { // Since no values was supplied, let's substitute the value with the type diff --git a/pkg/format/prop_key_resolver.go b/pkg/format/prop_key_resolver.go new file mode 100644 index 00000000..80a501c5 --- /dev/null +++ b/pkg/format/prop_key_resolver.go @@ -0,0 +1,105 @@ +package format + +import ( + "errors" + "fmt" + "github.com/containrrr/shoutrrr/pkg/types" + "reflect" + "sort" + "strings" +) + +// KeyPropConfig implements the ServiceConfig interface for services that uses key tags for query props +type PropKeyResolver struct { + confValue reflect.Value + keyFields map[string]FieldInfo + keys []string +} + +// BindKeys is called to map config fields to it's tagged query keys +func NewPropKeyResolver(config types.ServiceConfig) PropKeyResolver { + + _, fields := GetConfigFormat(config) + keyFields := make(map[string]FieldInfo, len(fields)) + keys := make([]string, 0, len(fields)) + for _, field := range fields { + key := strings.ToLower(field.Key) + if key != "" { + keys = append(keys, key) + keyFields[key] = field + } + } + + sort.Strings(keys) + + confValue := reflect.ValueOf(config) + if confValue.Kind() == reflect.Ptr { + confValue = confValue.Elem() + } + + return PropKeyResolver{ + keyFields: keyFields, + confValue: confValue, + keys: keys, + } +} + +// QueryFields returns a list of tagged keys +func (pkr *PropKeyResolver) QueryFields() []string { + return pkr.keys +} + +// Get returns the value of a config property tagged with the corresponding key +func (pkr *PropKeyResolver) Get(key string) (string, error) { + if field, found := pkr.keyFields[strings.ToLower(key)]; found { + return GetConfigFieldString(pkr.confValue, field) + } + + return "", fmt.Errorf("%v is not a valid config key", key) +} + +// Set sets the value of it's bound struct's property, tagged with the corresponding key +func (pkr *PropKeyResolver) Set(key string, value string) error { + return pkr.set(pkr.confValue, key, value) +} + +// set sets the value of a target struct tagged with the corresponding key +func (c *PropKeyResolver) set(target reflect.Value, key string, value string) error { + if field, found := c.keyFields[strings.ToLower(key)]; found { + valid, err := SetConfigField(target, field, value) + if !valid && err == nil { + return errors.New("invalid value for type") + } + return err + } + + return fmt.Errorf("%v is not a valid config key %v", key, c.keys) +} + +// UpdateConfigFromParams mutates the provided config, updating the values from it's corresponding params +func (pkr *PropKeyResolver) UpdateConfigFromParams(config types.ServiceConfig, params *types.Params) error { + if params != nil { + for key, val := range *params { + if err := pkr.set(reflect.ValueOf(config), key, val); err != nil { + return err + } + } + } + return nil +} + +func (pkr *PropKeyResolver) Bind(config types.ServiceConfig) PropKeyResolver { + bound := *pkr + bound.confValue = reflect.ValueOf(config) + return bound +} + +func GetConfigQueryResolver(config types.ServiceConfig) types.ConfigQueryResolver { + var resolver types.ConfigQueryResolver + var ok bool + if resolver, ok = config.(types.ConfigQueryResolver); !ok { + pkr := NewPropKeyResolver(config) + resolver = &pkr + } + return resolver +} diff --git a/pkg/services/discord/discord_config.go b/pkg/services/discord/discord_config.go index c8d6d8e8..bd5ebbf9 100644 --- a/pkg/services/discord/discord_config.go +++ b/pkg/services/discord/discord_config.go @@ -9,7 +9,6 @@ import ( // Config is the configuration needed to send discord notifications type Config struct { - standard.QuerylessConfig standard.EnumlessConfig Channel string Token string diff --git a/pkg/services/gotify/gotify_config.go b/pkg/services/gotify/gotify_config.go index ba210e2f..ca0b696d 100644 --- a/pkg/services/gotify/gotify_config.go +++ b/pkg/services/gotify/gotify_config.go @@ -8,7 +8,6 @@ import ( // Config for use within the gotify plugin type Config struct { - standard.QuerylessConfig standard.EnumlessConfig Token string Host string diff --git a/pkg/services/hangouts/hangouts_config.go b/pkg/services/hangouts/hangouts_config.go index c80e1101..d8ea6a66 100644 --- a/pkg/services/hangouts/hangouts_config.go +++ b/pkg/services/hangouts/hangouts_config.go @@ -8,7 +8,6 @@ import ( // Config for use within the Hangouts Chat plugin. type Config struct { - standard.QuerylessConfig standard.EnumlessConfig URL *url.URL } diff --git a/pkg/services/ifttt/ifttt.go b/pkg/services/ifttt/ifttt.go index 77c4509a..942cbe4f 100644 --- a/pkg/services/ifttt/ifttt.go +++ b/pkg/services/ifttt/ifttt.go @@ -3,6 +3,7 @@ package ifttt import ( "bytes" "fmt" + "github.com/containrrr/shoutrrr/pkg/format" "log" "net/http" "net/url" @@ -19,13 +20,17 @@ const ( type Service struct { standard.Standard config *Config + pkr format.PropKeyResolver } // Initialize loads ServiceConfig from configURL and sets logger for this Service func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error { service.Logger.SetLogger(logger) - service.config = &Config{} - if err := service.config.SetURL(configURL); err != nil { + service.config = &Config{ + UseMessageAsValue: 2, + } + service.pkr = format.NewPropKeyResolver(service.config) + if err := service.config.setURL(&service.pkr, configURL); err != nil { return err } @@ -34,12 +39,17 @@ func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error // Send a notification message to a IFTTT webhook func (service *Service) Send(message string, params *types.Params) error { - payload, err := createJSONToSend(service.config, message, params) + config := service.config + if err := service.pkr.UpdateConfigFromParams(config, params); err != nil { + return err + } + + payload, err := createJSONToSend(config, message, params) fmt.Printf("%+v", payload) if err != nil { return err } - for _, event := range service.config.Events { + for _, event := range config.Events { apiURL := service.createAPIURLForEvent(event) err := doSend(payload, apiURL) if err != nil { diff --git a/pkg/services/ifttt/ifttt_config.go b/pkg/services/ifttt/ifttt_config.go index dbf52ef4..10549510 100644 --- a/pkg/services/ifttt/ifttt_config.go +++ b/pkg/services/ifttt/ifttt_config.go @@ -2,13 +2,10 @@ package ifttt import ( "errors" - "fmt" - "net/url" - "strconv" - "strings" - "github.com/containrrr/shoutrrr/pkg/format" "github.com/containrrr/shoutrrr/pkg/services/standard" + "github.com/containrrr/shoutrrr/pkg/types" + "net/url" ) const ( @@ -20,36 +17,51 @@ const ( type Config struct { standard.EnumlessConfig WebHookID string - Events []string - Value1 string - Value2 string - Value3 string - UseMessageAsValue uint8 `desc:"" default:"2"` + Events []string `key:"events"` + Value1 string `key:"value1"` + Value2 string `key:"value2"` + Value3 string `key:"value3"` + UseMessageAsValue uint8 `key:"messagevalue" desc:"" default:"2"` } // GetURL returns a URL representation of it's current field values func (config *Config) GetURL() *url.URL { + resolver := format.NewPropKeyResolver(config) + return config.getURL(&resolver) +} + +// SetURL updates a ServiceConfig from a URL representation of it's field values +func (config *Config) SetURL(url *url.URL) error { + resolver := format.NewPropKeyResolver(config) + return config.setURL(&resolver, url) +} + +func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL { return &url.URL{ Host: config.WebHookID, Path: "/", Scheme: Scheme, - RawQuery: format.BuildQuery(config), + RawQuery: format.BuildQuery(resolver), } - } -// SetURL updates a ServiceConfig from a URL representation of it's field values -func (config *Config) SetURL(url *url.URL) error { - +func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error { + if config.UseMessageAsValue == 0 { + config.UseMessageAsValue = 2 + } config.WebHookID = url.Hostname() for key, vals := range url.Query() { - if err := config.Set(key, vals[0]); err != nil { + if err := resolver.Set(key, vals[0]); err != nil { return err } } + if config.UseMessageAsValue > 3 || config.UseMessageAsValue < 1 { + return errors.New("invalid value for messagevalue: only values 1-3 are supported") + } + if len(config.Events) < 1 { return errors.New("events missing from config URL") } @@ -60,57 +72,3 @@ func (config *Config) SetURL(url *url.URL) error { return nil } - -// QueryFields returns the fields that are part of the Query of the service URL -func (config *Config) QueryFields() []string { - return []string{ - "events", - "value1", - "value2", - "value3", - "messagevalue", - } -} - -// Get returns the value of a Query field -func (config *Config) Get(key string) (string, error) { - switch key { - case "events": - return strings.Join(config.Events, ","), nil - case "value1": - return config.Value1, nil - case "value2": - return config.Value2, nil - case "value3": - return config.Value3, nil - case "messagevalue": - return fmt.Sprintf("%d", config.UseMessageAsValue), nil - } - return "", fmt.Errorf("invalid query key \"%s\"", key) -} - -// Set updates the value of a Query field -func (config *Config) Set(key string, value string) error { - switch key { - case "events": - config.Events = strings.Split(value, ",") - case "value1": - config.Value1 = value - case "value2": - config.Value2 = value - case "value3": - config.Value3 = value - case "messagevalue": - val64, err := strconv.ParseUint(value, 10, 8) - if err == nil && val64 > 3 { - err = errors.New("only values 1-3 are supported") - } - if err != nil { - return fmt.Errorf("invalid value \"%s\" for \"messagevalue\": %s", value, err) - } - config.UseMessageAsValue = uint8(val64) - default: - return fmt.Errorf("invalid query key \"%s\"", key) - } - return nil -} diff --git a/pkg/services/ifttt/ifttt_test.go b/pkg/services/ifttt/ifttt_test.go index eecd0659..6dcc60d3 100644 --- a/pkg/services/ifttt/ifttt_test.go +++ b/pkg/services/ifttt/ifttt_test.go @@ -102,7 +102,7 @@ var _ = Describe("the ifttt package", func() { When("serializing a config to URL", func() { When("given multiple events", func() { It("should return an URL with all the events comma-separated", func() { - expectedURL := "ifttt://dummyID/?events=foo,bar,baz&value1=&value2=&value3=&messagevalue=0" + expectedURL := "ifttt://dummyID/?events=foo,bar,baz&messagevalue=0&value1=&value2=&value3=" config := Config{ Events: []string{"foo", "bar", "baz"}, WebHookID: "dummyID", diff --git a/pkg/services/join/join.go b/pkg/services/join/join.go index c6598065..75d3eac6 100644 --- a/pkg/services/join/join.go +++ b/pkg/services/join/join.go @@ -2,6 +2,7 @@ package join import ( "fmt" + "github.com/containrrr/shoutrrr/pkg/format" "log" "net/http" "net/url" @@ -20,6 +21,7 @@ const ( type Service struct { standard.Standard config *Config + pkr format.PropKeyResolver } // Send a notification message to Pushover @@ -87,7 +89,8 @@ func (service *Service) sendToDevices(devices string, message string, title stri func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error { service.Logger.SetLogger(logger) service.config = &Config{} - if err := service.config.SetURL(configURL); err != nil { + service.pkr = format.NewPropKeyResolver(service.config) + if err := service.config.setURL(&service.pkr, configURL); err != nil { return err } diff --git a/pkg/services/join/join_config.go b/pkg/services/join/join_config.go index 6bf54c06..d01cebb1 100644 --- a/pkg/services/join/join_config.go +++ b/pkg/services/join/join_config.go @@ -2,28 +2,17 @@ package join import ( "errors" - "fmt" "github.com/containrrr/shoutrrr/pkg/format" "github.com/containrrr/shoutrrr/pkg/types" "net/url" - "strings" ) // Config for the Pushover notification service service type Config struct { APIKey string - Devices []string - Title string - Icon string -} - -// QueryFields returns the fields that are part of the Query of the service URL -func (config *Config) QueryFields() []string { - return []string{ - "devices", - "title", - "icon", - } + Devices []string `key:"devices"` + Title string `key:"title"` + Icon string `key:"icon"` } // Enums returns the fields that should use a corresponding EnumFormatter to Print/Parse their values @@ -31,57 +20,35 @@ func (config *Config) Enums() map[string]types.EnumFormatter { return map[string]types.EnumFormatter{} } -// Get returns the value of a Query field -func (config *Config) Get(key string) (string, error) { - switch key { - case "devices": - return strings.Join(config.Devices, ","), nil - case "title": - return config.Title, nil - case "icon": - return config.Title, nil - } - - return "", fmt.Errorf("invalid query key \"%s\"", key) +// GetURL returns a URL representation of it's current field values +func (config *Config) GetURL() *url.URL { + resolver := format.NewPropKeyResolver(config) + return config.getURL(&resolver) } -// Set updates the value of a Query field -func (config *Config) Set(key string, value string) error { - switch key { - case "devices": - config.Devices = strings.Split(value, ",") - case "title": - config.Title = value - case "icon": - config.Icon = value - default: - return fmt.Errorf("invalid query key \"%s\"", key) - } - return nil +// SetURL updates a ServiceConfig from a URL representation of it's field values +func (config *Config) SetURL(url *url.URL) error { + resolver := format.NewPropKeyResolver(config) + return config.setURL(&resolver, url) } -// GetURL returns a URL representation of it's current field values -func (config *Config) GetURL() *url.URL { - +func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL { return &url.URL{ User: url.UserPassword("Token", config.APIKey), Host: "join", Scheme: Scheme, ForceQuery: true, - RawQuery: format.BuildQuery(config), + RawQuery: format.BuildQuery(resolver), } - } -// SetURL updates a ServiceConfig from a URL representation of it's field values -func (config *Config) SetURL(url *url.URL) error { - +func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error { password, _ := url.User.Password() config.APIKey = password for key, vals := range url.Query() { - if err := config.Set(key, vals[0]); err != nil { + if err := resolver.Set(key, vals[0]); err != nil { return err } } diff --git a/pkg/services/join/join_test.go b/pkg/services/join/join_test.go index a205b064..c7732a03 100644 --- a/pkg/services/join/join_test.go +++ b/pkg/services/join/join_test.go @@ -1,6 +1,7 @@ package join_test import ( + "github.com/containrrr/shoutrrr/pkg/format" "github.com/containrrr/shoutrrr/pkg/services/join" "github.com/containrrr/shoutrrr/pkg/util" @@ -20,6 +21,7 @@ func TestJoin(t *testing.T) { var ( service *join.Service config *join.Config + pkr format.PropKeyResolver envJoinURL *url.URL ) var _ = Describe("the join service", func() { @@ -44,6 +46,7 @@ var _ = Describe("the join service", func() { var _ = Describe("the join config", func() { BeforeEach(func() { config = &join.Config{} + pkr = format.NewPropKeyResolver(config) }) When("updating it using an url", func() { It("should update the API key using the password part of the url", func() { @@ -69,42 +72,42 @@ var _ = Describe("the join config", func() { }) When("setting a config key", func() { It("should split it by commas if the key is devices", func() { - err := config.Set("devices", "a,b,c,d") + err := pkr.Set("devices", "a,b,c,d") Expect(err).NotTo(HaveOccurred()) Expect(config.Devices).To(Equal([]string{"a", "b", "c", "d"})) }) It("should update icon when an icon is supplied", func() { - err := config.Set("icon", "https://example.com/icon.png") + err := pkr.Set("icon", "https://example.com/icon.png") Expect(err).NotTo(HaveOccurred()) Expect(config.Icon).To(Equal("https://example.com/icon.png")) }) It("should update the title when it is supplied", func() { - err := config.Set("title", "new title") + err := pkr.Set("title", "new title") Expect(err).NotTo(HaveOccurred()) Expect(config.Title).To(Equal("new title")) }) It("should return an error if the key is not recognized", func() { - err := config.Set("devicey", "a,b,c,d") + err := pkr.Set("devicey", "a,b,c,d") Expect(err).To(HaveOccurred()) }) }) When("getting a config key", func() { It("should join it with commas if the key is devices", func() { config.Devices = []string{"a", "b", "c"} - value, err := config.Get("devices") + value, err := pkr.Get("devices") Expect(err).NotTo(HaveOccurred()) Expect(value).To(Equal("a,b,c")) }) It("should return an error if the key is not recognized", func() { - _, err := config.Get("devicey") + _, err := pkr.Get("devicey") Expect(err).To(HaveOccurred()) }) }) When("listing the query fields", func() { - It("should return the keys \"devices\", \"title\", \"icon\"", func() { - fields := config.QueryFields() - Expect(fields).To(Equal([]string{"devices", "title", "icon"})) + It("should return the keys \"devices\", \"icon\", \"title\" in alphabetical order", func() { + fields := pkr.QueryFields() + Expect(fields).To(Equal([]string{"devices", "icon", "title"})) }) }) }) diff --git a/pkg/services/logger/logger.go b/pkg/services/logger/logger.go index f6d3359c..547ad8dc 100644 --- a/pkg/services/logger/logger.go +++ b/pkg/services/logger/logger.go @@ -39,7 +39,7 @@ func (service *Service) doSend(params *types.Params) error { } // Initialize loads ServiceConfig from configURL and sets logger for this Service -func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error { +func (service *Service) Initialize(_ *url.URL, logger *log.Logger) error { service.Logger.SetLogger(logger) service.config = &Config{} return nil diff --git a/pkg/services/logger/logger_config.go b/pkg/services/logger/logger_config.go index fdb2f408..beef482c 100644 --- a/pkg/services/logger/logger_config.go +++ b/pkg/services/logger/logger_config.go @@ -8,7 +8,6 @@ import ( // Config is the configuration object for the Logger Service type Config struct { - standard.QuerylessConfig standard.EnumlessConfig } @@ -20,7 +19,7 @@ func (config *Config) GetURL() *url.URL { } // SetURL updates a ServiceConfig from a URL representation of it's field values -func (config *Config) SetURL(url *url.URL) error { +func (config *Config) SetURL(_ *url.URL) error { return nil } diff --git a/pkg/services/mattermost/mattermost_config.go b/pkg/services/mattermost/mattermost_config.go index 790f28a5..e9d9a090 100644 --- a/pkg/services/mattermost/mattermost_config.go +++ b/pkg/services/mattermost/mattermost_config.go @@ -10,7 +10,6 @@ import ( //Config object holding all information type Config struct { - standard.QuerylessConfig standard.EnumlessConfig UserName string Channel string diff --git a/pkg/services/pushbullet/pushbullet_config.go b/pkg/services/pushbullet/pushbullet_config.go index 0ab5a60a..7b07eb72 100644 --- a/pkg/services/pushbullet/pushbullet_config.go +++ b/pkg/services/pushbullet/pushbullet_config.go @@ -11,7 +11,6 @@ import ( // Config ... type Config struct { - standard.QuerylessConfig standard.EnumlessConfig Targets []string Token string diff --git a/pkg/services/pushover/pushover.go b/pkg/services/pushover/pushover.go index df4b829a..aac627a6 100644 --- a/pkg/services/pushover/pushover.go +++ b/pkg/services/pushover/pushover.go @@ -2,6 +2,7 @@ package pushover import ( "fmt" + "github.com/containrrr/shoutrrr/pkg/format" "log" "net/http" "net/url" @@ -21,28 +22,20 @@ const ( type Service struct { standard.Standard config *Config + pkr format.PropKeyResolver } // Send a notification message to Pushover func (service *Service) Send(message string, params *types.Params) error { config := service.config - if params == nil { - params = &types.Params{} - } - errors := make([]error, 0) - - title, found := (*params)["subject"] - if !found { - title = config.Title + if err := service.pkr.UpdateConfigFromParams(config, params); err != nil { + return err } - priority, found := (*params)["priority"] - if !found { - priority = strconv.FormatInt(int64(config.Priority), 10) - } + errors := make([]error, 0) for _, device := range config.Devices { - if err := service.sendToDevice(device, message, title, priority); err != nil { + if err := service.sendToDevice(device, message, config); err != nil { errors = append(errors, err) } } @@ -54,8 +47,7 @@ func (service *Service) Send(message string, params *types.Params) error { return nil } -func (service *Service) sendToDevice(device string, message string, title string, priority string) error { - config := service.config +func (service *Service) sendToDevice(device string, message string, config *Config) error { data := url.Values{} data.Set("device", device) @@ -63,12 +55,12 @@ func (service *Service) sendToDevice(device string, message string, title string data.Set("token", config.Token) data.Set("message", message) - if len(title) > 0 { - data.Set("title", title) + if len(config.Title) > 0 { + data.Set("title", config.Title) } - if len(priority) > 0 { - data.Set("priority", priority) + if config.Priority > 0 { + data.Set("priority", strconv.FormatInt(int64(config.Priority), 10)) } res, err := http.Post( @@ -91,7 +83,8 @@ func (service *Service) sendToDevice(device string, message string, title string func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error { service.Logger.SetLogger(logger) service.config = &Config{} - if err := service.config.SetURL(configURL); err != nil { + service.pkr = format.NewPropKeyResolver(service.config) + if err := service.config.setURL(&service.pkr, configURL); err != nil { return err } diff --git a/pkg/services/pushover/pushover_config.go b/pkg/services/pushover/pushover_config.go index f01907e7..2eb61da9 100644 --- a/pkg/services/pushover/pushover_config.go +++ b/pkg/services/pushover/pushover_config.go @@ -2,30 +2,18 @@ package pushover import ( "errors" - "fmt" "github.com/containrrr/shoutrrr/pkg/format" "github.com/containrrr/shoutrrr/pkg/types" "net/url" - "strconv" - "strings" ) // Config for the Pushover notification service service type Config struct { Token string User string - Devices []string - Priority int8 - Title string -} - -// QueryFields returns the fields that are part of the Query of the service URL -func (config *Config) QueryFields() []string { - return []string{ - "devices", - "priority", - "title", - } + Devices []string `key:"devices"` + Priority int8 `key:"priority"` + Title string `key:"title" role:"title"` } // Enums returns the fields that should use a corresponding EnumFormatter to Print/Parse their values @@ -33,62 +21,32 @@ func (config *Config) Enums() map[string]types.EnumFormatter { return map[string]types.EnumFormatter{} } -// Get returns the value of a Query field -func (config *Config) Get(key string) (string, error) { - switch key { - case "devices": - return strings.Join(config.Devices, ","), nil - case "priority": - return strconv.FormatInt(int64(config.Priority), 10), nil - case "title": - return config.Title, nil - } - - return "", fmt.Errorf("invalid query key \"%s\"", key) -} - -// Set updates the value of a Query field -func (config *Config) Set(key string, value string) error { - switch key { - case "devices": - config.Devices = strings.Split(value, ",") - case "priority": - priority, err := strconv.ParseInt(value, 10, 8) - if err == nil { - config.Priority = int8(priority) - } - return err - case "title": - config.Title = value - default: - return fmt.Errorf("invalid query key \"%s\"", key) - } - return nil -} - // GetURL returns a URL representation of it's current field values func (config *Config) GetURL() *url.URL { - + resolver := format.NewPropKeyResolver(config) return &url.URL{ User: url.UserPassword("Token", config.Token), Host: config.User, Scheme: Scheme, ForceQuery: true, - RawQuery: format.BuildQuery(config), + RawQuery: format.BuildQuery(&resolver), } - } // SetURL updates a ServiceConfig from a URL representation of it's field values func (config *Config) SetURL(url *url.URL) error { + resolver := format.NewPropKeyResolver(config) + return config.setURL(&resolver, url) +} +func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error { password, _ := url.User.Password() config.User = url.Host config.Token = password for key, vals := range url.Query() { - if err := config.Set(key, vals[0]); err != nil { + if err := resolver.Set(key, vals[0]); err != nil { return err } } diff --git a/pkg/services/pushover/pushover_test.go b/pkg/services/pushover/pushover_test.go index d4270a6c..7ee4cb80 100644 --- a/pkg/services/pushover/pushover_test.go +++ b/pkg/services/pushover/pushover_test.go @@ -1,9 +1,9 @@ package pushover_test import ( + "github.com/containrrr/shoutrrr/pkg/format" "github.com/containrrr/shoutrrr/pkg/services/pushover" "github.com/containrrr/shoutrrr/pkg/util" - "net/url" "os" "testing" @@ -20,6 +20,7 @@ func TestPushover(t *testing.T) { var ( service *pushover.Service config *pushover.Config + keyResolver format.PropKeyResolver envPushoverURL *url.URL ) var _ = Describe("the pushover service", func() { @@ -44,6 +45,7 @@ var _ = Describe("the pushover service", func() { var _ = Describe("the pushover config", func() { BeforeEach(func() { config = &pushover.Config{} + keyResolver = format.NewPropKeyResolver(config) }) When("updating it using an url", func() { It("should update the username using the host part of the url", func() { @@ -81,45 +83,45 @@ var _ = Describe("the pushover config", func() { }) When("setting a config key", func() { It("should split it by commas if the key is devices", func() { - err := config.Set("devices", "a,b,c,d") + err := keyResolver.Set("devices", "a,b,c,d") Expect(err).NotTo(HaveOccurred()) Expect(config.Devices).To(Equal([]string{"a", "b", "c", "d"})) }) It("should update priority when a valid number is supplied", func() { - err := config.Set("priority", "1") + err := keyResolver.Set("priority", "1") Expect(err).NotTo(HaveOccurred()) Expect(config.Priority).To(Equal(int8(1))) }) It("should update the title when it is supplied", func() { - err := config.Set("title", "new title") + err := keyResolver.Set("title", "new title") Expect(err).NotTo(HaveOccurred()) Expect(config.Title).To(Equal("new title")) }) It("should return an error if priority is not a number", func() { - err := config.Set("priority", "super-duper") + err := keyResolver.Set("priority", "super-duper") Expect(err).To(HaveOccurred()) }) It("should return an error if the key is not recognized", func() { - err := config.Set("devicey", "a,b,c,d") + err := keyResolver.Set("devicey", "a,b,c,d") Expect(err).To(HaveOccurred()) }) }) When("getting a config key", func() { It("should join it with commas if the key is devices", func() { config.Devices = []string{"a", "b", "c"} - value, err := config.Get("devices") + value, err := keyResolver.Get("devices") Expect(err).NotTo(HaveOccurred()) Expect(value).To(Equal("a,b,c")) }) It("should return an error if the key is not recognized", func() { - _, err := config.Get("devicey") + _, err := keyResolver.Get("devicey") Expect(err).To(HaveOccurred()) }) }) When("listing the query fields", func() { It("should return the keys \"devices\",\"priority\",\"title\"", func() { - fields := config.QueryFields() + fields := keyResolver.QueryFields() Expect(fields).To(Equal([]string{"devices", "priority", "title"})) }) }) diff --git a/pkg/services/rocketchat/rocketchat_config.go b/pkg/services/rocketchat/rocketchat_config.go index d3d8dd8d..569218f0 100644 --- a/pkg/services/rocketchat/rocketchat_config.go +++ b/pkg/services/rocketchat/rocketchat_config.go @@ -3,6 +3,7 @@ package rocketchat import ( "errors" "fmt" + "github.com/containrrr/shoutrrr/pkg/types" "net/url" "strings" @@ -11,7 +12,6 @@ import ( // Config for the rocket.chat service type Config struct { - standard.QuerylessConfig standard.EnumlessConfig UserName string Host string @@ -68,7 +68,7 @@ const ( ) // CreateConfigFromURL to use within the rocket.chat service -func CreateConfigFromURL(serviceURL *url.URL) (*Config, error) { +func CreateConfigFromURL(_ types.ConfigQueryResolver, serviceURL *url.URL) (*Config, error) { config := Config{} err := config.SetURL(serviceURL) return &config, err diff --git a/pkg/services/slack/slack.go b/pkg/services/slack/slack.go index 5d4117af..a02f9284 100644 --- a/pkg/services/slack/slack.go +++ b/pkg/services/slack/slack.go @@ -7,6 +7,7 @@ import ( "log" "net/http" "net/url" + "strings" "github.com/containrrr/shoutrrr/pkg/services/standard" "github.com/containrrr/shoutrrr/pkg/types" @@ -27,7 +28,7 @@ const ( func (service *Service) Send(message string, params *types.Params) error { config := service.config - if err := validateToken(config.Token); err != nil { + if err := ValidateToken(config.Token); err != nil { return err } if len(message) > maxlength { @@ -60,10 +61,5 @@ func (service *Service) doSend(config *Config, message string) error { } func (service *Service) getURL(config *Config) string { - return fmt.Sprintf( - "%s/%s/%s/%s", - apiURL, - config.Token.A, - config.Token.B, - config.Token.C) + return fmt.Sprintf("%s/%s", apiURL, strings.Join(config.Token, "/")) } diff --git a/pkg/services/slack/slack_config.go b/pkg/services/slack/slack_config.go index 4a9ccb8e..f1a82c11 100644 --- a/pkg/services/slack/slack_config.go +++ b/pkg/services/slack/slack_config.go @@ -9,18 +9,17 @@ import ( // Config for the slack service type Config struct { - standard.QuerylessConfig standard.EnumlessConfig - BotName string - Token Token + BotName string `default:"Shoutrrr"` + Token []string `description:"List of comma separated token parts"` } // GetURL returns a URL representation of it's current field values func (config *Config) GetURL() *url.URL { return &url.URL{ - User: url.UserPassword(config.BotName, config.Token.String()), - Host: config.Token.A, - Path: fmt.Sprintf("/%s/%s", config.Token.B, config.Token.C), + User: url.User(config.BotName), + Host: config.Token[0], + Path: fmt.Sprintf("/%s/%s", config.Token[1], config.Token[2]), Scheme: Scheme, ForceQuery: false, } @@ -36,20 +35,17 @@ func (config *Config) SetURL(serviceURL *url.URL) error { host := serviceURL.Hostname() - path := strings.Split(serviceURL.Path, "/") + token := strings.Split(serviceURL.Path, "/") + token[0] = host - if len(path) < 2 { - path = []string{"", "", ""} + if len(token) < 2 { + token = []string{"", "", ""} } config.BotName = botName - config.Token = Token{ - A: host, - B: path[1], - C: path[2], - } + config.Token = token - if err := validateToken(config.Token); err != nil { + if err := ValidateToken(config.Token); err != nil { return err } diff --git a/pkg/services/slack/slack_token.go b/pkg/services/slack/slack_token.go index 534e5ce6..0e5e1698 100644 --- a/pkg/services/slack/slack_token.go +++ b/pkg/services/slack/slack_token.go @@ -2,19 +2,14 @@ package slack import ( "errors" - "fmt" "regexp" "strings" ) // Token is a three part string split into A, B and C -type Token struct { - A string - B string - C string -} +type Token []string -func validateToken(token Token) error { +func ValidateToken(token Token) error { if err := tokenPartsAreNotEmpty(token); err != nil { return err } else if err := tokenPartsAreValidFormat(token); err != nil { @@ -24,22 +19,22 @@ func validateToken(token Token) error { } func tokenPartsAreNotEmpty(token Token) error { - if token.A == "" { + if token[0] == "" { return errors.New(string(TokenAMissing)) - } else if token.B == "" { + } else if token[1] == "" { return errors.New(string(TokenBMissing)) - } else if token.C == "" { + } else if token[2] == "" { return errors.New(string(TokenCMissing)) } return nil } func tokenPartsAreValidFormat(token Token) error { - if !matchesPattern("[A-Z0-9]{9}", token.A) { + if !matchesPattern("[A-Z0-9]{9}", token[0]) { return errors.New(string(TokenAMalformed)) - } else if !matchesPattern("[A-Z0-9]{9}", token.B) { + } else if !matchesPattern("[A-Z0-9]{9}", token[1]) { return errors.New(string(TokenBMalformed)) - } else if !matchesPattern("[A-Za-z0-9]{24}", token.C) { + } else if !matchesPattern("[A-Za-z0-9]{24}", token[2]) { return errors.New(string(TokenCMalformed)) } return nil @@ -54,15 +49,11 @@ func matchesPattern(pattern string, part string) bool { } func (t Token) String() string { - return fmt.Sprintf("%s-%s-%s", t.A, t.B, t.C) + return strings.Join(t, "-") } // ParseToken creates a Token from a sting representation func ParseToken(s string) Token { - parts := strings.Split(s, "-") - return Token{ - A: parts[0], - B: parts[1], - C: parts[2], - } + token := strings.Split(s, "-") + return token } diff --git a/pkg/services/smtp/smtp.go b/pkg/services/smtp/smtp.go index c5c91f81..c0de20cd 100644 --- a/pkg/services/smtp/smtp.go +++ b/pkg/services/smtp/smtp.go @@ -3,6 +3,7 @@ package smtp import ( "crypto/tls" "fmt" + "github.com/containrrr/shoutrrr/pkg/format" "io" "log" "math/rand" @@ -20,6 +21,7 @@ type Service struct { standard.Templater config *Config multipartBoundary string + propKeyResolver format.PropKeyResolver } const ( @@ -41,9 +43,9 @@ func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error Encryption: encMethods.Auto, } - service.config.BindKeys(service.config) + pkr := format.NewPropKeyResolver(service.config) - if err := service.config.SetURL(configURL); err != nil { + if err := service.config.setURL(&pkr, configURL); err != nil { return err } @@ -55,6 +57,8 @@ func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error } } + service.propKeyResolver = pkr + return nil } @@ -65,7 +69,8 @@ func (service *Service) Send(message string, params *types.Params) error { return fail(FailGetSMTPClient, err) } - if config, err := GetSendConfig(*service.config, params); err != nil { + config := service.config.Clone() + if err := service.propKeyResolver.UpdateConfigFromParams(&config, params); err != nil { return fail(FailApplySendParams, err) } else { return service.doSend(client, message, &config) diff --git a/pkg/services/smtp/smtp_config.go b/pkg/services/smtp/smtp_config.go index f4a880be..cc252db6 100644 --- a/pkg/services/smtp/smtp_config.go +++ b/pkg/services/smtp/smtp_config.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "github.com/containrrr/shoutrrr/pkg/format" - "github.com/containrrr/shoutrrr/pkg/services/standard" "github.com/containrrr/shoutrrr/pkg/types" "net/url" "strconv" @@ -12,7 +11,6 @@ import ( // Config is the configuration needed to send e-mail notifications over SMTP type Config struct { - standard.KeyPropConfig Host string `desc:"SMTP server hostname or IP address"` Username string `desc:"authentication username"` Password string `desc:"authentication password or hash"` @@ -29,6 +27,17 @@ type Config struct { // GetURL returns a URL representation of it's current field values func (config *Config) GetURL() *url.URL { + resolver := format.NewPropKeyResolver(config) + return config.getURL(&resolver) +} + +// SetURL updates a ServiceConfig from a URL representation of it's field values +func (config *Config) SetURL(url *url.URL) error { + resolver := format.NewPropKeyResolver(config) + return config.setURL(&resolver, url) +} + +func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL { return &url.URL{ User: url.UserPassword(config.Username, config.Password), @@ -36,13 +45,12 @@ func (config *Config) GetURL() *url.URL { Path: "/", Scheme: Scheme, ForceQuery: true, - RawQuery: format.BuildQuery(config), + RawQuery: format.BuildQuery(resolver), } } -// SetURL updates a ServiceConfig from a URL representation of it's field values -func (config *Config) SetURL(url *url.URL) error { +func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error { password, _ := url.User.Password() @@ -55,7 +63,7 @@ func (config *Config) SetURL(url *url.URL) error { } for key, vals := range url.Query() { - if err := config.Set(key, vals[0]); err != nil { + if err := resolver.Set(key, vals[0]); err != nil { return err } } @@ -71,10 +79,12 @@ func (config *Config) SetURL(url *url.URL) error { return nil } -// GetSendConfig returns a copy of the config with overrides from params -func GetSendConfig(config Config, params *types.Params) (Config, error) { - err := config.KeyPropConfig.UpdateConfigFromParams(&config, params) - return config, err +// Clone returns a copy of the config +func (config *Config) Clone() Config { + clone := *config + clone.ToAddresses = make([]string, len(config.ToAddresses)) + copy(clone.ToAddresses, clone.ToAddresses) + return clone } // Enums returns the fields that should use a corresponding EnumFormatter to Print/Parse their values diff --git a/pkg/services/smtp/smtp_test.go b/pkg/services/smtp/smtp_test.go index 0849437c..f413bc08 100644 --- a/pkg/services/smtp/smtp_test.go +++ b/pkg/services/smtp/smtp_test.go @@ -2,6 +2,7 @@ package smtp import ( "fmt" + "github.com/containrrr/shoutrrr/pkg/format" "log" "net/smtp" "net/url" @@ -48,11 +49,11 @@ var _ = Describe("the SMTP service", func() { Expect(err).NotTo(HaveOccurred(), "parsing") config := &Config{} - config.BindKeys(config) + pkr := format.NewPropKeyResolver(config) err = config.SetURL(url) Expect(err).NotTo(HaveOccurred(), "verifying") - fmt.Printf("%v", config.QueryFields()) + fmt.Printf("%v", pkr.QueryFields()) outputURL := config.GetURL() fmt.Println(outputURL.String()) @@ -68,7 +69,6 @@ var _ = Describe("the SMTP service", func() { Expect(err).NotTo(HaveOccurred(), "parsing") config := &Config{} - config.BindKeys(config) err = config.SetURL(url) Expect(err).To(HaveOccurred(), "verifying") }) @@ -81,7 +81,6 @@ var _ = Describe("the SMTP service", func() { Expect(err).NotTo(HaveOccurred(), "parsing") config := &Config{} - config.BindKeys(config) err = config.SetURL(url) Expect(err).To(HaveOccurred(), "verifying") }) @@ -92,7 +91,6 @@ var _ = Describe("the SMTP service", func() { var config *Config BeforeEach(func() { config = &Config{} - config.BindKeys(config) }) It("should not allow getting invalid query values", func() { testutils.TestConfigGetInvalidQueryValue(config) diff --git a/pkg/services/standard/key_config.go b/pkg/services/standard/key_config.go deleted file mode 100644 index 5366d95a..00000000 --- a/pkg/services/standard/key_config.go +++ /dev/null @@ -1,89 +0,0 @@ -package standard - -import ( - "errors" - "fmt" - "github.com/containrrr/shoutrrr/pkg/format" - "github.com/containrrr/shoutrrr/pkg/types" - "log" - "reflect" - "sort" - "strings" -) - -// KeyPropConfig implements the ServiceConfig interface for services that uses key tags for query props -type KeyPropConfig struct { - confValue reflect.Value - keyFields map[string]format.FieldInfo - keys []string -} - -// BindKeys is called to map config fields to it's tagged query keys -func (c *KeyPropConfig) BindKeys(config types.ServiceConfig) { - _, fields := format.GetConfigFormat(config) - keyFields := make(map[string]format.FieldInfo, len(fields)) - keys := make([]string, 0, len(fields)) - for _, field := range fields { - key := strings.ToLower(field.Key) - if key != "" { - keys = append(keys, key) - keyFields[key] = field - } - } - c.keyFields = keyFields - c.confValue = reflect.ValueOf(config) - if c.confValue.Kind() == reflect.Ptr { - c.confValue = c.confValue.Elem() - } - sort.Strings(keys) - c.keys = keys -} - -// QueryFields returns a list of tagged keys -func (c *KeyPropConfig) QueryFields() []string { - if c.keys == nil { - log.Panic("KeyPropConfig.QueryFields called before BindKeys") - } - - return c.keys -} - -// Get returns the value of a config property tagged with the corresponding key -func (c *KeyPropConfig) Get(key string) (string, error) { - if c.keyFields == nil { - return "", errors.New("KeyPropConfig.Get called before BindKeys") - } - - if field, found := c.keyFields[strings.ToLower(key)]; found { - return format.GetConfigFieldString(c.confValue, field) - } - - return "", fmt.Errorf("%v is not a valid config key", key) -} - -// Set sets the value of a config property tagged with the corresponding key -func (c *KeyPropConfig) Set(key string, value string) error { - if c.keyFields == nil { - return errors.New("KeyPropConfig.Set called before BindKeys") - } - - if field, found := c.keyFields[strings.ToLower(key)]; found { - valid, err := format.SetConfigField(c.confValue, field, value) - if !valid && err == nil { - return errors.New("invalid value for type") - } - return err - } - - return fmt.Errorf("%v is not a valid config key", key) -} - -// UpdateConfigFromParams mutates the provided config, updating the values from it's corresponding params -func (c *KeyPropConfig) UpdateConfigFromParams(config types.ServiceConfig, params *types.Params) error { - for key, val := range *params { - if err := config.Set(key, val); err != nil { - return err - } - } - return nil -} diff --git a/pkg/services/standard/queryless_config.go b/pkg/services/standard/queryless_config.go deleted file mode 100644 index 8d975ab2..00000000 --- a/pkg/services/standard/queryless_config.go +++ /dev/null @@ -1,23 +0,0 @@ -package standard - -import ( - "errors" -) - -// QuerylessConfig implements the ServiceConfig interface for services that does not use Query fields -type QuerylessConfig struct{} - -// QueryFields returns an empty list of Query fields -func (qc *QuerylessConfig) QueryFields() []string { - return []string{} -} - -// Get is a dummy function that will return an error if called -func (qc *QuerylessConfig) Get(string) (string, error) { - return "", errors.New("service config does not support Get") -} - -// Set is a dummy function that will return an error if called -func (qc *QuerylessConfig) Set(string, string) error { - return errors.New("service config does not support Set") -} diff --git a/pkg/services/standard/standard_test.go b/pkg/services/standard/standard_test.go index ee935c73..4a400e6b 100644 --- a/pkg/services/standard/standard_test.go +++ b/pkg/services/standard/standard_test.go @@ -131,24 +131,3 @@ var _ = Describe("the standard enumless config implementation", func() { }) }) }) - -var _ = Describe("the standard queryless config implementation", func() { - When("it's queryfields method is called", func() { - It("should return an empty slice", func() { - Expect((&QuerylessConfig{}).QueryFields()).To(BeEmpty()) - }) - }) - When("it's get method is called", func() { - It("should return an error and no value", func() { - val, err := (&QuerylessConfig{}).Get("foo") - Expect(val).To(BeEmpty()) - Expect(err).To(HaveOccurred()) - }) - }) - When("it's set method is called", func() { - It("should return an error", func() { - err := (&QuerylessConfig{}).Set("foo", "bar") - Expect(err).To(HaveOccurred()) - }) - }) -}) diff --git a/pkg/services/teams/teams_config.go b/pkg/services/teams/teams_config.go index eff80503..0b96d443 100644 --- a/pkg/services/teams/teams_config.go +++ b/pkg/services/teams/teams_config.go @@ -8,7 +8,6 @@ import ( // Config for use within the teams plugin type Config struct { - standard.QuerylessConfig standard.EnumlessConfig Token Token } diff --git a/pkg/services/telegram/telegram.go b/pkg/services/telegram/telegram.go index 6233811a..89c66491 100644 --- a/pkg/services/telegram/telegram.go +++ b/pkg/services/telegram/telegram.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/containrrr/shoutrrr/pkg/format" "log" "net/http" "net/url" @@ -22,6 +23,7 @@ const ( type Service struct { standard.Standard config *Config + pkr format.PropKeyResolver } // Send notification to Telegram @@ -37,7 +39,8 @@ func (service *Service) Send(message string, _ *types.Params) error { func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error { service.Logger.SetLogger(logger) service.config = &Config{} - if err := service.config.SetURL(configURL); err != nil { + service.pkr = format.NewPropKeyResolver(service.config) + if err := service.config.setURL(&service.pkr, configURL); err != nil { return err } diff --git a/pkg/services/telegram/telegram_config.go b/pkg/services/telegram/telegram_config.go index e6774f48..f267dee2 100644 --- a/pkg/services/telegram/telegram_config.go +++ b/pkg/services/telegram/telegram_config.go @@ -3,24 +3,15 @@ package telegram import ( "errors" "fmt" - "net/url" - "strings" - "github.com/containrrr/shoutrrr/pkg/format" "github.com/containrrr/shoutrrr/pkg/types" + "net/url" ) // Config for use within the telegram plugin type Config struct { Token string - Channels []string -} - -// QueryFields returns the fields that are part of the Query of the service URL -func (config *Config) QueryFields() []string { - return []string{ - "channels", - } + Channels []string `key:"channels"` } // Enums returns the fields that should use a corresponding EnumFormatter to Print/Parse their values @@ -28,41 +19,31 @@ func (config *Config) Enums() map[string]types.EnumFormatter { return map[string]types.EnumFormatter{} } -// Get returns the value of a Query field -func (config *Config) Get(key string) (string, error) { - switch key { - case "channels": - return strings.Join(config.Channels, ","), nil - } - return "", fmt.Errorf("invalid query key \"%s\"", key) +// GetURL returns a URL representation of it's current field values +func (config *Config) GetURL() *url.URL { + resolver := format.NewPropKeyResolver(config) + return config.getURL(&resolver) } -// Set updates the value of a Query field -func (config *Config) Set(key string, value string) error { - switch key { - case "channels": - config.Channels = strings.Split(value, ",") - default: - return fmt.Errorf("invalid query key \"%s\"", key) - } - return nil +// SetURL updates a ServiceConfig from a URL representation of it's field values +func (config *Config) SetURL(url *url.URL) error { + resolver := format.NewPropKeyResolver(config) + return config.setURL(&resolver, url) } -// GetURL returns a URL representation of it's current field values -func (config *Config) GetURL() *url.URL { +func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL { return &url.URL{ User: url.UserPassword("Token", config.Token), Host: Scheme, Scheme: Scheme, ForceQuery: true, - RawQuery: format.BuildQuery(config), + RawQuery: format.BuildQuery(resolver), } } -// SetURL updates a ServiceConfig from a URL representation of it's field values -func (config *Config) SetURL(url *url.URL) error { +func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error { password, _ := url.User.Password() @@ -72,7 +53,7 @@ func (config *Config) SetURL(url *url.URL) error { } for key, vals := range url.Query() { - if err := config.Set(key, vals[0]); err != nil { + if err := resolver.Set(key, vals[0]); err != nil { return err } } diff --git a/pkg/services/zulip/zulip.go b/pkg/services/zulip/zulip.go index 6366138f..932617fd 100644 --- a/pkg/services/zulip/zulip.go +++ b/pkg/services/zulip/zulip.go @@ -59,7 +59,7 @@ func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error service.Logger.SetLogger(logger) service.config = &Config{} - if err := service.config.SetURL(configURL); err != nil { + if err := service.config.SetURL(nil, configURL); err != nil { return err } diff --git a/pkg/services/zulip/zulip_config.go b/pkg/services/zulip/zulip_config.go index ff9cb978..df187a1b 100644 --- a/pkg/services/zulip/zulip_config.go +++ b/pkg/services/zulip/zulip_config.go @@ -2,6 +2,7 @@ package zulip import ( "errors" + "github.com/containrrr/shoutrrr/pkg/types" "net/url" ) @@ -11,12 +12,12 @@ type Config struct { BotKey string Host string Path string - Stream string - Topic string + Stream string `key:"stream"` + Topic string `key:"topic"` } // GetURL returns a URL representation of it's current field values -func (config *Config) GetURL() *url.URL { +func (config *Config) GetURL(_ types.ConfigQueryResolver) *url.URL { query := &url.Values{} if config.Stream != "" { @@ -37,7 +38,7 @@ func (config *Config) GetURL() *url.URL { } // SetURL updates a ServiceConfig from a URL representation of it's field values -func (config *Config) SetURL(serviceURL *url.URL) error { +func (config *Config) SetURL(_ types.ConfigQueryResolver, serviceURL *url.URL) error { var ok bool config.BotMail = serviceURL.User.Username() @@ -85,7 +86,7 @@ const ( // CreateConfigFromURL to use within the zulip service func CreateConfigFromURL(serviceURL *url.URL) (*Config, error) { config := Config{} - err := config.SetURL(serviceURL) + err := config.SetURL(nil, serviceURL) return &config, err } diff --git a/pkg/services/zulip/zulip_test.go b/pkg/services/zulip/zulip_test.go index 65f6762d..086f2f6c 100644 --- a/pkg/services/zulip/zulip_test.go +++ b/pkg/services/zulip/zulip_test.go @@ -38,8 +38,9 @@ var _ = Describe("the zulip service", func() { } serviceURL, _ := url.Parse(envZulipURL.String()) - service.Initialize(serviceURL, util.TestLogger()) - err := service.Send("This is an integration test message", nil) + err := service.Initialize(serviceURL, util.TestLogger()) + Expect(err).NotTo(HaveOccurred()) + err = service.Send("This is an integration test message", nil) Expect(err).NotTo(HaveOccurred()) }) }) @@ -136,7 +137,7 @@ var _ = Describe("the zulip service", func() { Stream: "foo", Topic: "bar", } - url := config.GetURL() + url := config.GetURL(nil) Expect(url.String()).To(Equal("zulip://bot-name%40zulipchat.com:correcthorsebatterystable@example.zulipchat.com?stream=foo&topic=bar")) }) }) @@ -148,7 +149,7 @@ var _ = Describe("the zulip service", func() { Host: "example.zulipchat.com", Stream: "foo", } - url := config.GetURL() + url := config.GetURL(nil) Expect(url.String()).To(Equal("zulip://bot-name%40zulipchat.com:correcthorsebatterystable@example.zulipchat.com?stream=foo")) }) }) diff --git a/pkg/types/service_config.go b/pkg/types/service_config.go index 59efc3d5..60e3f6e2 100644 --- a/pkg/types/service_config.go +++ b/pkg/types/service_config.go @@ -4,10 +4,13 @@ import "net/url" // ServiceConfig is the common interface for all types of service configurations type ServiceConfig interface { - Get(string) (string, error) - Set(string, string) error - QueryFields() []string GetURL() *url.URL SetURL(*url.URL) error Enums() map[string]EnumFormatter } + +type ConfigQueryResolver interface { + Get(string) (string, error) + Set(string, string) error + QueryFields() []string +} diff --git a/pkg/xmpp/xmpp.go b/pkg/xmpp/xmpp.go index 389143eb..2e6ffb4f 100644 --- a/pkg/xmpp/xmpp.go +++ b/pkg/xmpp/xmpp.go @@ -1,6 +1,7 @@ package xmpp import ( + "github.com/containrrr/shoutrrr/pkg/format" "log" "net/url" @@ -14,6 +15,7 @@ import ( // Service sends notifications via XMPP type Service struct { standard.Standard + pkr format.PropKeyResolver client *xmpp.Client router *xmpp.Router config *Config @@ -27,7 +29,8 @@ func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error Subject: "Shoutrrr Notification", } - if err := service.config.SetURL(configURL); err != nil { + service.pkr = format.NewPropKeyResolver(service.config) + if err := service.config.setURL(&service.pkr, configURL); err != nil { return err } @@ -49,18 +52,13 @@ func (service *Service) Send(message string, params *types.Params) error { return err } - if params == nil { - params = &types.Params{} - } - - // TODO: Move param override to shared service API - subject, found := (*params)["subject"] - if !found { - subject = service.config.Subject + config := service.config + if err := service.pkr.UpdateConfigFromParams(config, params); err != nil { + return err } msg := stanza.Message{ - Subject: subject, + Subject: config.Subject, Body: message, Attrs: stanza.Attrs{ To: service.config.ToAddress, diff --git a/pkg/xmpp/xmpp_config.go b/pkg/xmpp/xmpp_config.go index 8f94bcf8..2141a9f6 100644 --- a/pkg/xmpp/xmpp_config.go +++ b/pkg/xmpp/xmpp_config.go @@ -3,11 +3,10 @@ package xmpp import ( "errors" "fmt" + "github.com/containrrr/shoutrrr/pkg/types" + "gosrc.io/xmpp" "net/url" "strconv" - "strings" - - "gosrc.io/xmpp" "github.com/containrrr/shoutrrr/pkg/format" "github.com/containrrr/shoutrrr/pkg/services/standard" @@ -25,26 +24,36 @@ type Config struct { Username string Password string Host string - ServerHost string - ToAddress string - Subject string + ServerHost string `key:"serverhost"` + ToAddress string `key:"toaddress"` + Subject string `key:"subject"` } // GetURL returns a URL representation of it's current field values func (config *Config) GetURL() *url.URL { + resolver := format.NewPropKeyResolver(config) + return config.getURL(&resolver) +} + +// SetURL updates a ServiceConfig from a URL representation of it's field values +func (config *Config) SetURL(url *url.URL) error { + resolver := format.NewPropKeyResolver(config) + return config.setURL(&resolver, url) +} + +func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL { return &url.URL{ User: url.UserPassword(config.Username, config.Password), Host: fmt.Sprintf("%s:%d", config.Host, config.Port), Path: "/", Scheme: Scheme, - RawQuery: format.BuildQuery(config), + RawQuery: format.BuildQuery(resolver), } } -// SetURL updates a ServiceConfig from a URL representation of it's field values -func (config *Config) SetURL(url *url.URL) error { +func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error { password, _ := url.User.Password() @@ -57,7 +66,7 @@ func (config *Config) SetURL(url *url.URL) error { } for key, vals := range url.Query() { - if err := config.Set(key, vals[0]); err != nil { + if err := resolver.Set(key, vals[0]); err != nil { return err } } @@ -69,43 +78,6 @@ func (config *Config) SetURL(url *url.URL) error { return nil } -// QueryFields returns the fields that are part of the Query of the service URL -func (config *Config) QueryFields() []string { - return []string{ - "toAddress", - "subject", - "serverHost", - } -} - -// Get returns the value of a Query field -func (config *Config) Get(key string) (string, error) { - switch strings.ToLower(key) { - case "toaddress": - return config.ToAddress, nil - case "subject": - return config.Subject, nil - case "serverhost": - return config.ServerHost, nil - } - return "", fmt.Errorf("invalid query key \"%s\"", key) -} - -// Set updates the value of a Query field -func (config *Config) Set(key string, value string) error { - switch strings.ToLower(key) { - case "toaddress": - config.ToAddress = value - case "subject": - config.Subject = value - case "serverhost": - config.ServerHost = value - default: - return fmt.Errorf("invalid query key \"%s\"", key) - } - return nil -} - func (config *Config) getClientConfig() *xmpp.Config { conf := xmpp.Config{ Jid: config.fromAddress(),