From d2e6c034156e43514328b6f6ce24ea738fb2c1c2 Mon Sep 17 00:00:00 2001 From: Joseph Kavanagh Date: Tue, 17 May 2022 14:29:44 +0100 Subject: [PATCH 1/6] feat(mattermost): add support for icons --- docs/services/mattermost.md | 3 +- pkg/services/mattermost/mattermost.go | 12 +++++-- pkg/services/mattermost/mattermost_config.go | 26 ++++++++++++-- pkg/services/mattermost/mattermost_json.go | 27 +++++++++++++-- pkg/services/mattermost/mattermost_test.go | 36 ++++++++++++++++++++ 5 files changed, 96 insertions(+), 8 deletions(-) diff --git a/docs/services/mattermost.md b/docs/services/mattermost.md index 1e762c47..23d9c599 100644 --- a/docs/services/mattermost.md +++ b/docs/services/mattermost.md @@ -3,7 +3,7 @@ ## URL Format !!! info "" - mattermost://[__`username`__@]__`mattermost-host`__/__`token`__[/__`channel`__] + mattermost://[__`username`__@]__`mattermost-host`__/__`token`__[/__`channel`__][?icon=__`smiley`__] --8<-- "docs/services/mattermost/config.md" @@ -59,6 +59,7 @@ params := (*types.Params)( &map[string]string{ "username": "overwriteUserName", "channel": "overwriteChannel", + "icon": "overwriteIcon", }, ) diff --git a/pkg/services/mattermost/mattermost.go b/pkg/services/mattermost/mattermost.go index f56931f0..092456ef 100644 --- a/pkg/services/mattermost/mattermost.go +++ b/pkg/services/mattermost/mattermost.go @@ -3,10 +3,12 @@ package mattermost import ( "bytes" "fmt" - "github.com/containrrr/shoutrrr/pkg/services/standard" "net/http" "net/url" + "github.com/containrrr/shoutrrr/pkg/format" + "github.com/containrrr/shoutrrr/pkg/services/standard" + "github.com/containrrr/shoutrrr/pkg/types" ) @@ -14,13 +16,15 @@ import ( 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 types.StdLogger) 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 } @@ -31,6 +35,10 @@ func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) e func (service *Service) Send(message string, params *types.Params) error { config := service.config apiURL := buildURL(config) + + if err := service.pkr.UpdateConfigFromParams(config, params); err != nil { + return err + } json, _ := CreateJSONPayload(config, message, params) res, err := http.Post(apiURL, "application/json", bytes.NewReader(json)) if err != nil { diff --git a/pkg/services/mattermost/mattermost_config.go b/pkg/services/mattermost/mattermost_config.go index 53008bab..a38a020a 100644 --- a/pkg/services/mattermost/mattermost_config.go +++ b/pkg/services/mattermost/mattermost_config.go @@ -2,15 +2,19 @@ package mattermost import ( "errors" - "github.com/containrrr/shoutrrr/pkg/services/standard" "net/url" "strings" + + "github.com/containrrr/shoutrrr/pkg/format" + "github.com/containrrr/shoutrrr/pkg/services/standard" + "github.com/containrrr/shoutrrr/pkg/types" ) //Config object holding all information type Config struct { standard.EnumlessConfig UserName string `url:"user" optional:"" desc:"Override webhook user"` + Icon string `key:"icon,icon_emoji,icon_url" default:"" optional:"" desc:"Use emoji or URL as icon (based on presence of http(s):// prefix)"` Channel string `url:"path2" optional:"" desc:"Override webhook channel"` Host string `url:"host,port" desc:"Mattermost server host"` Token string `url:"path1" desc:"Webhook token"` @@ -36,7 +40,12 @@ 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(url *url.URL) error { + resolver := format.NewPropKeyResolver(config) + return config.setURL(&resolver, url) +} + +func (config *Config) setURL(resolver types.ConfigQueryResolver, serviceURL *url.URL) error { config.Host = serviceURL.Hostname() if serviceURL.Path == "" || serviceURL.Path == "/" { @@ -45,6 +54,12 @@ func (config *Config) SetURL(serviceURL *url.URL) error { config.UserName = serviceURL.User.Username() path := strings.Split(serviceURL.Path[1:], "/") + for key, vals := range serviceURL.Query() { + if err := resolver.Set(key, vals[0]); err != nil { + return err + } + } + if len(path) < 1 { return errors.New(string(NotEnoughArguments)) } @@ -68,3 +83,10 @@ const ( // NotEnoughArguments provided in the service URL NotEnoughArguments ErrorMessage = "the apiURL does not include enough arguments, either provide 1 or 3 arguments (they may be empty)" ) + +// CreateConfigFromURL to use within the mattermost service +func CreateConfigFromURL(serviceURL *url.URL) (*Config, error) { + config := Config{} + err := config.SetURL(serviceURL) + return &config, err +} diff --git a/pkg/services/mattermost/mattermost_json.go b/pkg/services/mattermost/mattermost_json.go index 9233980d..1deb3fa7 100644 --- a/pkg/services/mattermost/mattermost_json.go +++ b/pkg/services/mattermost/mattermost_json.go @@ -2,15 +2,34 @@ package mattermost import ( "encoding/json" + "regexp" "github.com/containrrr/shoutrrr/pkg/types" ) // JSON payload for mattermost notifications type JSON struct { - Text string `json:"text"` - UserName string `json:"username,omitempty"` - Channel string `json:"channel,omitempty"` + Text string `json:"text"` + UserName string `json:"username,omitempty"` + Channel string `json:"channel,omitempty"` + IconEmoji string `json:"icon_emoji,omitempty"` + IconURL string `json:"icon_url,omitempty"` +} + +var iconURLPattern = regexp.MustCompile(`https?://`) + +// SetIcon sets the appropriate icon field in the payload based on whether the input is a URL or not +func (j *JSON) SetIcon(icon string) { + j.IconURL = "" + j.IconEmoji = "" + + if icon != "" { + if iconURLPattern.MatchString(icon) { + j.IconURL = icon + } else { + j.IconEmoji = icon + } + } } // CreateJSONPayload for usage with the mattermost service @@ -29,5 +48,7 @@ func CreateJSONPayload(config *Config, message string, params *types.Params) ([] payload.Channel = value } } + payload.SetIcon(config.Icon) + return json.Marshal(payload) } diff --git a/pkg/services/mattermost/mattermost_test.go b/pkg/services/mattermost/mattermost_test.go index 693caa61..8a13d07c 100644 --- a/pkg/services/mattermost/mattermost_test.go +++ b/pkg/services/mattermost/mattermost_test.go @@ -111,6 +111,42 @@ var _ = Describe("the mattermost service", func() { }) }) }) + When("generating a config object", func() { + It("should not set icon", func() { + slackURL, _ := url.Parse("mattermost://AAAAAAAAA/BBBBBBBBB") + config, configError := CreateConfigFromURL(slackURL) + + Expect(configError).NotTo(HaveOccurred()) + Expect(config.Icon).To(BeEmpty()) + }) + It("should set icon", func() { + slackURL, _ := url.Parse("mattermost://AAAAAAAAA/BBBBBBBBB?icon=test") + config, configError := CreateConfigFromURL(slackURL) + + Expect(configError).NotTo(HaveOccurred()) + Expect(config.Icon).To(BeIdenticalTo("test")) + }) + }) + Describe("creating the payload", func() { + Describe("the icon fields", func() { + payload := JSON{} + It("should set IconURL when the configured icon looks like an URL", func() { + payload.SetIcon("https://example.com/logo.png") + Expect(payload.IconURL).To(Equal("https://example.com/logo.png")) + Expect(payload.IconEmoji).To(BeEmpty()) + }) + It("should set IconEmoji when the configured icon does not look like an URL", func() { + payload.SetIcon("tanabata_tree") + Expect(payload.IconEmoji).To(Equal("tanabata_tree")) + Expect(payload.IconURL).To(BeEmpty()) + }) + It("should clear both fields when icon is empty", func() { + payload.SetIcon("") + Expect(payload.IconEmoji).To(BeEmpty()) + Expect(payload.IconURL).To(BeEmpty()) + }) + }) + }) Describe("Sending messages", func() { When("sending a message completely without parameters", func() { mattermostURL, _ := url.Parse("mattermost://mattermost.my-domain.com/thisshouldbeanapitoken") From 9fdf10097ab5de2fa028f0c1a408aa2db42456cb Mon Sep 17 00:00:00 2001 From: Joseph Kavanagh Date: Tue, 17 May 2022 16:07:24 +0100 Subject: [PATCH 2/6] acknowledge `title` key, but don't use it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: nils måsén --- pkg/services/mattermost/mattermost_config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/services/mattermost/mattermost_config.go b/pkg/services/mattermost/mattermost_config.go index a38a020a..d7de0d9a 100644 --- a/pkg/services/mattermost/mattermost_config.go +++ b/pkg/services/mattermost/mattermost_config.go @@ -15,6 +15,7 @@ type Config struct { standard.EnumlessConfig UserName string `url:"user" optional:"" desc:"Override webhook user"` Icon string `key:"icon,icon_emoji,icon_url" default:"" optional:"" desc:"Use emoji or URL as icon (based on presence of http(s):// prefix)"` + Title string `key:"title" default:"" desc:"Notification title, optionally set by the sender (not used)"` Channel string `url:"path2" optional:"" desc:"Override webhook channel"` Host string `url:"host,port" desc:"Mattermost server host"` Token string `url:"path1" desc:"Webhook token"` From 11f5d925bd57958dbcc549f89c09993fa47186d6 Mon Sep 17 00:00:00 2001 From: Joseph Kavanagh Date: Fri, 20 May 2022 10:42:33 +0100 Subject: [PATCH 3/6] Add generic service compliance tests --- pkg/services/mattermost/mattermost_test.go | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pkg/services/mattermost/mattermost_test.go b/pkg/services/mattermost/mattermost_test.go index 8a13d07c..dadc0353 100644 --- a/pkg/services/mattermost/mattermost_test.go +++ b/pkg/services/mattermost/mattermost_test.go @@ -220,4 +220,31 @@ var _ = Describe("the mattermost service", func() { }) }) + + Describe("the basic service API", func() { + Describe("the service config", func() { + It("should implement basic service config API methods correctly", func() { + testutils.TestConfigGetInvalidQueryValue(&Config{}) + testutils.TestConfigSetInvalidQueryValue(&Config{}, "bark://:mock-device@host/?foo=bar") + + testutils.TestConfigSetDefaultValues(&Config{}) + + testutils.TestConfigGetEnumsCount(&Config{}, 0) + testutils.TestConfigGetFieldsCount(&Config{}, 9) + }) + }) + Describe("the service instance", func() { + BeforeEach(func() { + httpmock.Activate() + }) + AfterEach(func() { + httpmock.DeactivateAndReset() + }) + It("should implement basic service API methods correctly", func() { + serviceURL := testutils.URLMust("bark://:devicekey@hostname") + Expect(service.Initialize(serviceURL, logger)).To(Succeed()) + testutils.TestServiceSetInvalidParamValue(service, "foo", "bar") + }) + }) + }) }) From a33d53475f85fb25a79fd8c58094e6e0604a253a Mon Sep 17 00:00:00 2001 From: Joseph Kavanagh Date: Fri, 20 May 2022 15:10:35 +0100 Subject: [PATCH 4/6] fix `GetURL` and tests --- pkg/services/mattermost/mattermost_config.go | 2 + pkg/services/mattermost/mattermost_test.go | 56 +++++++++++++++++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/pkg/services/mattermost/mattermost_config.go b/pkg/services/mattermost/mattermost_config.go index d7de0d9a..140dd0fa 100644 --- a/pkg/services/mattermost/mattermost_config.go +++ b/pkg/services/mattermost/mattermost_config.go @@ -31,12 +31,14 @@ func (config *Config) GetURL() *url.URL { if config.UserName != "" { user = url.User(config.UserName) } + resolver := format.NewPropKeyResolver(config) return &url.URL{ User: user, Host: config.Host, Path: strings.Join(paths, "/"), Scheme: Scheme, ForceQuery: false, + RawQuery: format.BuildQuery(&resolver), } } diff --git a/pkg/services/mattermost/mattermost_test.go b/pkg/services/mattermost/mattermost_test.go index dadc0353..5c52ddf2 100644 --- a/pkg/services/mattermost/mattermost_test.go +++ b/pkg/services/mattermost/mattermost_test.go @@ -1,6 +1,7 @@ package mattermost import ( + "fmt" "net/url" "os" "testing" @@ -195,6 +196,51 @@ var _ = Describe("the mattermost service", func() { }) }) + Describe("creating configurations", func() { + When("given a url with channel field", func() { + It("should not throw an error", func() { + serviceURL := testutils.URLMust(`mattermost://user@mockserver/atoken/achannel`) + Expect((&Config{}).SetURL(serviceURL)).To(Succeed()) + }) + }) + When("given a url with title prop", func() { + It("should not throw an error", func() { + serviceURL := testutils.URLMust(`mattermost://user@mockserver/atoken?icon=https%3A%2F%2Fexample%2Fsomething.png`) + Expect((&Config{}).SetURL(serviceURL)).To(Succeed()) + }) + }) + When("given a url with all fields and props", func() { + It("should not throw an error", func() { + serviceURL := testutils.URLMust(`mattermost://user@mockserver/atoken/achannel?icon=https%3A%2F%2Fexample%2Fsomething.png`) + Expect((&Config{}).SetURL(serviceURL)).To(Succeed()) + }) + }) + When("given a url with invalid props", func() { + It("should return an error", func() { + serviceURL := testutils.URLMust(`matrix://user@mockserver/atoken?foo=bar`) + Expect((&Config{}).SetURL(serviceURL)).To(HaveOccurred()) + }) + }) + When("parsing the configuration URL", func() { + It("should be identical after de-/serialization", func() { + testURL := "mattermost://user@mockserver/atoken/achannel?icon=something" + + url, err := url.Parse(testURL) + Expect(err).NotTo(HaveOccurred(), "parsing") + + config := &Config{} + err = config.SetURL(url) + Expect(err).NotTo(HaveOccurred(), "verifying") + + outputURL := config.GetURL() + fmt.Println(outputURL.String(), testURL) + + Expect(outputURL.String()).To(Equal(testURL)) + + }) + }) + }) + Describe("sending the payload", func() { var err error BeforeEach(func() { @@ -218,19 +264,17 @@ var _ = Describe("the mattermost service", func() { err = service.Send("Message", nil) Expect(err).NotTo(HaveOccurred()) }) - }) - + Describe("the basic service API", func() { Describe("the service config", func() { It("should implement basic service config API methods correctly", func() { testutils.TestConfigGetInvalidQueryValue(&Config{}) - testutils.TestConfigSetInvalidQueryValue(&Config{}, "bark://:mock-device@host/?foo=bar") testutils.TestConfigSetDefaultValues(&Config{}) testutils.TestConfigGetEnumsCount(&Config{}, 0) - testutils.TestConfigGetFieldsCount(&Config{}, 9) + testutils.TestConfigGetFieldsCount(&Config{}, 4) }) }) Describe("the service instance", func() { @@ -241,8 +285,8 @@ var _ = Describe("the mattermost service", func() { httpmock.DeactivateAndReset() }) It("should implement basic service API methods correctly", func() { - serviceURL := testutils.URLMust("bark://:devicekey@hostname") - Expect(service.Initialize(serviceURL, logger)).To(Succeed()) + serviceURL := testutils.URLMust("bark://mockhost/mocktoken") + Expect(service.Initialize(serviceURL, testutils.TestLogger())).To(Succeed()) testutils.TestServiceSetInvalidParamValue(service, "foo", "bar") }) }) From c8bcce327e05f1fa2465e9af356d08314db9da03 Mon Sep 17 00:00:00 2001 From: Joseph Kavanagh Date: Fri, 20 May 2022 16:33:42 +0100 Subject: [PATCH 5/6] remove `redundant if ...; err != nil check` --- pkg/services/mattermost/mattermost.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/services/mattermost/mattermost.go b/pkg/services/mattermost/mattermost.go index 092456ef..d638f049 100644 --- a/pkg/services/mattermost/mattermost.go +++ b/pkg/services/mattermost/mattermost.go @@ -24,11 +24,7 @@ func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) e service.Logger.SetLogger(logger) service.config = &Config{} service.pkr = format.NewPropKeyResolver(service.config) - if err := service.config.setURL(&service.pkr, configURL); err != nil { - return err - } - - return nil + return service.config.setURL(&service.pkr, configURL) } // Send a notification message to Mattermost From 4a9e2315c745ef847eb764072ce7c6cabe208916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 21 May 2022 10:34:07 +0200 Subject: [PATCH 6/6] fix copy paste typo --- pkg/services/mattermost/mattermost_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/mattermost/mattermost_test.go b/pkg/services/mattermost/mattermost_test.go index 5c52ddf2..6048067b 100644 --- a/pkg/services/mattermost/mattermost_test.go +++ b/pkg/services/mattermost/mattermost_test.go @@ -285,7 +285,7 @@ var _ = Describe("the mattermost service", func() { httpmock.DeactivateAndReset() }) It("should implement basic service API methods correctly", func() { - serviceURL := testutils.URLMust("bark://mockhost/mocktoken") + serviceURL := testutils.URLMust("mattermost://mockhost/mocktoken") Expect(service.Initialize(serviceURL, testutils.TestLogger())).To(Succeed()) testutils.TestServiceSetInvalidParamValue(service, "foo", "bar") })