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..d638f049 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,23 +16,25 @@ 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 { - return err - } - - return nil + service.pkr = format.NewPropKeyResolver(service.config) + return service.config.setURL(&service.pkr, configURL) } // Send a notification message to Mattermost 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..140dd0fa 100644 --- a/pkg/services/mattermost/mattermost_config.go +++ b/pkg/services/mattermost/mattermost_config.go @@ -2,15 +2,20 @@ 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)"` + 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"` @@ -26,17 +31,24 @@ 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), } } // 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 +57,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 +86,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..6048067b 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" @@ -111,6 +112,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") @@ -159,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() { @@ -182,6 +264,31 @@ 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.TestConfigSetDefaultValues(&Config{}) + testutils.TestConfigGetEnumsCount(&Config{}, 0) + testutils.TestConfigGetFieldsCount(&Config{}, 4) + }) + }) + 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("mattermost://mockhost/mocktoken") + Expect(service.Initialize(serviceURL, testutils.TestLogger())).To(Succeed()) + testutils.TestServiceSetInvalidParamValue(service, "foo", "bar") + }) + }) }) })