Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mattermost): add support for icons #237

Merged
merged 6 commits into from May 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/services/mattermost.md
Expand Up @@ -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"

Expand Down Expand Up @@ -59,6 +59,7 @@ params := (*types.Params)(
&map[string]string{
"username": "overwriteUserName",
"channel": "overwriteChannel",
"icon": "overwriteIcon",
},
)

Expand Down
16 changes: 10 additions & 6 deletions pkg/services/mattermost/mattermost.go
Expand Up @@ -3,34 +3,38 @@ 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"
)

// Service sends notifications to a pre-configured channel or user
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 {
Expand Down
29 changes: 27 additions & 2 deletions pkg/services/mattermost/mattermost_config.go
Expand Up @@ -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)"`
JosephKav marked this conversation as resolved.
Show resolved Hide resolved
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"`
Expand All @@ -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 == "/" {
Expand All @@ -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))
}
Expand All @@ -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
}
27 changes: 24 additions & 3 deletions pkg/services/mattermost/mattermost_json.go
Expand Up @@ -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
Expand All @@ -29,5 +48,7 @@ func CreateJSONPayload(config *Config, message string, params *types.Params) ([]
payload.Channel = value
}
}
payload.SetIcon(config.Icon)

return json.Marshal(payload)
}
107 changes: 107 additions & 0 deletions pkg/services/mattermost/mattermost_test.go
@@ -1,6 +1,7 @@
package mattermost

import (
"fmt"
"net/url"
"os"
"testing"
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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() {
Expand All @@ -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")
})
})
})
})