Skip to content

Commit 6dbcb12

Browse files
authored
feat(mattermost): add support for icons (#237)
1 parent 5bca245 commit 6dbcb12

File tree

5 files changed

+170
-12
lines changed

5 files changed

+170
-12
lines changed

docs/services/mattermost.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## URL Format
44

55
!!! info ""
6-
mattermost://[__`username`__@]__`mattermost-host`__/__`token`__[/__`channel`__]
6+
mattermost://[__`username`__@]__`mattermost-host`__/__`token`__[/__`channel`__][?icon=__`smiley`__]
77

88
--8<-- "docs/services/mattermost/config.md"
99

@@ -59,6 +59,7 @@ params := (*types.Params)(
5959
&map[string]string{
6060
"username": "overwriteUserName",
6161
"channel": "overwriteChannel",
62+
"icon": "overwriteIcon",
6263
},
6364
)
6465

pkg/services/mattermost/mattermost.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,38 @@ package mattermost
33
import (
44
"bytes"
55
"fmt"
6-
"github.com/containrrr/shoutrrr/pkg/services/standard"
76
"net/http"
87
"net/url"
98

9+
"github.com/containrrr/shoutrrr/pkg/format"
10+
"github.com/containrrr/shoutrrr/pkg/services/standard"
11+
1012
"github.com/containrrr/shoutrrr/pkg/types"
1113
)
1214

1315
// Service sends notifications to a pre-configured channel or user
1416
type Service struct {
1517
standard.Standard
1618
config *Config
19+
pkr format.PropKeyResolver
1720
}
1821

1922
// Initialize loads ServiceConfig from configURL and sets logger for this Service
2023
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
2124
service.Logger.SetLogger(logger)
2225
service.config = &Config{}
23-
if err := service.config.SetURL(configURL); err != nil {
24-
return err
25-
}
26-
27-
return nil
26+
service.pkr = format.NewPropKeyResolver(service.config)
27+
return service.config.setURL(&service.pkr, configURL)
2828
}
2929

3030
// Send a notification message to Mattermost
3131
func (service *Service) Send(message string, params *types.Params) error {
3232
config := service.config
3333
apiURL := buildURL(config)
34+
35+
if err := service.pkr.UpdateConfigFromParams(config, params); err != nil {
36+
return err
37+
}
3438
json, _ := CreateJSONPayload(config, message, params)
3539
res, err := http.Post(apiURL, "application/json", bytes.NewReader(json))
3640
if err != nil {

pkg/services/mattermost/mattermost_config.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ package mattermost
22

33
import (
44
"errors"
5-
"github.com/containrrr/shoutrrr/pkg/services/standard"
65
"net/url"
76
"strings"
7+
8+
"github.com/containrrr/shoutrrr/pkg/format"
9+
"github.com/containrrr/shoutrrr/pkg/services/standard"
10+
"github.com/containrrr/shoutrrr/pkg/types"
811
)
912

1013
//Config object holding all information
1114
type Config struct {
1215
standard.EnumlessConfig
1316
UserName string `url:"user" optional:"" desc:"Override webhook user"`
17+
Icon string `key:"icon,icon_emoji,icon_url" default:"" optional:"" desc:"Use emoji or URL as icon (based on presence of http(s):// prefix)"`
18+
Title string `key:"title" default:"" desc:"Notification title, optionally set by the sender (not used)"`
1419
Channel string `url:"path2" optional:"" desc:"Override webhook channel"`
1520
Host string `url:"host,port" desc:"Mattermost server host"`
1621
Token string `url:"path1" desc:"Webhook token"`
@@ -26,17 +31,24 @@ func (config *Config) GetURL() *url.URL {
2631
if config.UserName != "" {
2732
user = url.User(config.UserName)
2833
}
34+
resolver := format.NewPropKeyResolver(config)
2935
return &url.URL{
3036
User: user,
3137
Host: config.Host,
3238
Path: strings.Join(paths, "/"),
3339
Scheme: Scheme,
3440
ForceQuery: false,
41+
RawQuery: format.BuildQuery(&resolver),
3542
}
3643
}
3744

3845
// SetURL updates a ServiceConfig from a URL representation of it's field values
39-
func (config *Config) SetURL(serviceURL *url.URL) error {
46+
func (config *Config) SetURL(url *url.URL) error {
47+
resolver := format.NewPropKeyResolver(config)
48+
return config.setURL(&resolver, url)
49+
}
50+
51+
func (config *Config) setURL(resolver types.ConfigQueryResolver, serviceURL *url.URL) error {
4052

4153
config.Host = serviceURL.Hostname()
4254
if serviceURL.Path == "" || serviceURL.Path == "/" {
@@ -45,6 +57,12 @@ func (config *Config) SetURL(serviceURL *url.URL) error {
4557
config.UserName = serviceURL.User.Username()
4658
path := strings.Split(serviceURL.Path[1:], "/")
4759

60+
for key, vals := range serviceURL.Query() {
61+
if err := resolver.Set(key, vals[0]); err != nil {
62+
return err
63+
}
64+
}
65+
4866
if len(path) < 1 {
4967
return errors.New(string(NotEnoughArguments))
5068
}
@@ -68,3 +86,10 @@ const (
6886
// NotEnoughArguments provided in the service URL
6987
NotEnoughArguments ErrorMessage = "the apiURL does not include enough arguments, either provide 1 or 3 arguments (they may be empty)"
7088
)
89+
90+
// CreateConfigFromURL to use within the mattermost service
91+
func CreateConfigFromURL(serviceURL *url.URL) (*Config, error) {
92+
config := Config{}
93+
err := config.SetURL(serviceURL)
94+
return &config, err
95+
}

pkg/services/mattermost/mattermost_json.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,34 @@ package mattermost
22

33
import (
44
"encoding/json"
5+
"regexp"
56

67
"github.com/containrrr/shoutrrr/pkg/types"
78
)
89

910
// JSON payload for mattermost notifications
1011
type JSON struct {
11-
Text string `json:"text"`
12-
UserName string `json:"username,omitempty"`
13-
Channel string `json:"channel,omitempty"`
12+
Text string `json:"text"`
13+
UserName string `json:"username,omitempty"`
14+
Channel string `json:"channel,omitempty"`
15+
IconEmoji string `json:"icon_emoji,omitempty"`
16+
IconURL string `json:"icon_url,omitempty"`
17+
}
18+
19+
var iconURLPattern = regexp.MustCompile(`https?://`)
20+
21+
// SetIcon sets the appropriate icon field in the payload based on whether the input is a URL or not
22+
func (j *JSON) SetIcon(icon string) {
23+
j.IconURL = ""
24+
j.IconEmoji = ""
25+
26+
if icon != "" {
27+
if iconURLPattern.MatchString(icon) {
28+
j.IconURL = icon
29+
} else {
30+
j.IconEmoji = icon
31+
}
32+
}
1433
}
1534

1635
// CreateJSONPayload for usage with the mattermost service
@@ -29,5 +48,7 @@ func CreateJSONPayload(config *Config, message string, params *types.Params) ([]
2948
payload.Channel = value
3049
}
3150
}
51+
payload.SetIcon(config.Icon)
52+
3253
return json.Marshal(payload)
3354
}

pkg/services/mattermost/mattermost_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mattermost
22

33
import (
4+
"fmt"
45
"net/url"
56
"os"
67
"testing"
@@ -111,6 +112,42 @@ var _ = Describe("the mattermost service", func() {
111112
})
112113
})
113114
})
115+
When("generating a config object", func() {
116+
It("should not set icon", func() {
117+
slackURL, _ := url.Parse("mattermost://AAAAAAAAA/BBBBBBBBB")
118+
config, configError := CreateConfigFromURL(slackURL)
119+
120+
Expect(configError).NotTo(HaveOccurred())
121+
Expect(config.Icon).To(BeEmpty())
122+
})
123+
It("should set icon", func() {
124+
slackURL, _ := url.Parse("mattermost://AAAAAAAAA/BBBBBBBBB?icon=test")
125+
config, configError := CreateConfigFromURL(slackURL)
126+
127+
Expect(configError).NotTo(HaveOccurred())
128+
Expect(config.Icon).To(BeIdenticalTo("test"))
129+
})
130+
})
131+
Describe("creating the payload", func() {
132+
Describe("the icon fields", func() {
133+
payload := JSON{}
134+
It("should set IconURL when the configured icon looks like an URL", func() {
135+
payload.SetIcon("https://example.com/logo.png")
136+
Expect(payload.IconURL).To(Equal("https://example.com/logo.png"))
137+
Expect(payload.IconEmoji).To(BeEmpty())
138+
})
139+
It("should set IconEmoji when the configured icon does not look like an URL", func() {
140+
payload.SetIcon("tanabata_tree")
141+
Expect(payload.IconEmoji).To(Equal("tanabata_tree"))
142+
Expect(payload.IconURL).To(BeEmpty())
143+
})
144+
It("should clear both fields when icon is empty", func() {
145+
payload.SetIcon("")
146+
Expect(payload.IconEmoji).To(BeEmpty())
147+
Expect(payload.IconURL).To(BeEmpty())
148+
})
149+
})
150+
})
114151
Describe("Sending messages", func() {
115152
When("sending a message completely without parameters", func() {
116153
mattermostURL, _ := url.Parse("mattermost://mattermost.my-domain.com/thisshouldbeanapitoken")
@@ -159,6 +196,51 @@ var _ = Describe("the mattermost service", func() {
159196
})
160197
})
161198

199+
Describe("creating configurations", func() {
200+
When("given a url with channel field", func() {
201+
It("should not throw an error", func() {
202+
serviceURL := testutils.URLMust(`mattermost://user@mockserver/atoken/achannel`)
203+
Expect((&Config{}).SetURL(serviceURL)).To(Succeed())
204+
})
205+
})
206+
When("given a url with title prop", func() {
207+
It("should not throw an error", func() {
208+
serviceURL := testutils.URLMust(`mattermost://user@mockserver/atoken?icon=https%3A%2F%2Fexample%2Fsomething.png`)
209+
Expect((&Config{}).SetURL(serviceURL)).To(Succeed())
210+
})
211+
})
212+
When("given a url with all fields and props", func() {
213+
It("should not throw an error", func() {
214+
serviceURL := testutils.URLMust(`mattermost://user@mockserver/atoken/achannel?icon=https%3A%2F%2Fexample%2Fsomething.png`)
215+
Expect((&Config{}).SetURL(serviceURL)).To(Succeed())
216+
})
217+
})
218+
When("given a url with invalid props", func() {
219+
It("should return an error", func() {
220+
serviceURL := testutils.URLMust(`matrix://user@mockserver/atoken?foo=bar`)
221+
Expect((&Config{}).SetURL(serviceURL)).To(HaveOccurred())
222+
})
223+
})
224+
When("parsing the configuration URL", func() {
225+
It("should be identical after de-/serialization", func() {
226+
testURL := "mattermost://user@mockserver/atoken/achannel?icon=something"
227+
228+
url, err := url.Parse(testURL)
229+
Expect(err).NotTo(HaveOccurred(), "parsing")
230+
231+
config := &Config{}
232+
err = config.SetURL(url)
233+
Expect(err).NotTo(HaveOccurred(), "verifying")
234+
235+
outputURL := config.GetURL()
236+
fmt.Println(outputURL.String(), testURL)
237+
238+
Expect(outputURL.String()).To(Equal(testURL))
239+
240+
})
241+
})
242+
})
243+
162244
Describe("sending the payload", func() {
163245
var err error
164246
BeforeEach(func() {
@@ -182,6 +264,31 @@ var _ = Describe("the mattermost service", func() {
182264
err = service.Send("Message", nil)
183265
Expect(err).NotTo(HaveOccurred())
184266
})
267+
})
268+
269+
Describe("the basic service API", func() {
270+
Describe("the service config", func() {
271+
It("should implement basic service config API methods correctly", func() {
272+
testutils.TestConfigGetInvalidQueryValue(&Config{})
273+
274+
testutils.TestConfigSetDefaultValues(&Config{})
185275

276+
testutils.TestConfigGetEnumsCount(&Config{}, 0)
277+
testutils.TestConfigGetFieldsCount(&Config{}, 4)
278+
})
279+
})
280+
Describe("the service instance", func() {
281+
BeforeEach(func() {
282+
httpmock.Activate()
283+
})
284+
AfterEach(func() {
285+
httpmock.DeactivateAndReset()
286+
})
287+
It("should implement basic service API methods correctly", func() {
288+
serviceURL := testutils.URLMust("mattermost://mockhost/mocktoken")
289+
Expect(service.Initialize(serviceURL, testutils.TestLogger())).To(Succeed())
290+
testutils.TestServiceSetInvalidParamValue(service, "foo", "bar")
291+
})
292+
})
186293
})
187294
})

0 commit comments

Comments
 (0)