Skip to content

Commit

Permalink
fix(slack): use attachments and add title and color fields (#119)
Browse files Browse the repository at this point in the history
* fix(slack): use attachments and add title and color fields
* fix(slack): simplify init
* test(slack): add serialization test
* fix(slack): remove arbitrary max message length
  • Loading branch information
piksel committed Jan 31, 2021
1 parent c889ea9 commit 7e4a3bf
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 29 deletions.
28 changes: 15 additions & 13 deletions pkg/services/slack/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package slack

import (
"bytes"
"errors"
"fmt"
"github.com/containrrr/shoutrrr/pkg/format"
"log"
"net/http"
"net/url"
Expand All @@ -17,22 +17,23 @@ import (
type Service struct {
standard.Standard
config *Config
pkr format.PropKeyResolver
}

const (
apiURL = "https://hooks.slack.com/services"
maxlength = 1000
apiURL = "https://hooks.slack.com/services"
)

// Send a notification message to Slack
func (service *Service) Send(message string, params *types.Params) error {
config := service.config

if err := ValidateToken(config.Token); err != nil {
if err := service.pkr.UpdateConfigFromParams(config, params); err != nil {
return err
}
if len(message) > maxlength {
return errors.New("message exceeds max length")

if err := ValidateToken(config.Token); err != nil {
return err
}

return service.doSend(config, message)
Expand All @@ -42,17 +43,18 @@ func (service *Service) Send(message string, params *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 {
return err
}

return nil
service.pkr = format.NewPropKeyResolver(service.config)
return service.config.setURL(&service.pkr, configURL)
}

func (service *Service) doSend(config *Config, message string) error {
postURL := service.getURL(config)
payload, _ := CreateJSONPayload(config, message)
res, err := http.Post(postURL, "application/json", bytes.NewBuffer(payload))
payload, err := CreateJSONPayload(config, message)

var res *http.Response
if err == nil {
res, err = http.Post(postURL, "application/json", bytes.NewBuffer(payload))
}

if res == nil && err == nil {
err = fmt.Errorf("unknown error")
Expand Down
32 changes: 24 additions & 8 deletions pkg/services/slack/slack_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,48 @@ package slack

import (
"fmt"
"github.com/containrrr/shoutrrr/pkg/format"
"github.com/containrrr/shoutrrr/pkg/services/standard"
"github.com/containrrr/shoutrrr/pkg/types"
"net/url"
"strings"
)

// Config for the slack service
type Config struct {
standard.EnumlessConfig
BotName string `default:"Shoutrrr"`
BotName string `default:"" optional:""`
Token []string `description:"List of comma separated token parts"`
Color string `key:"color" optional:""`
Title string `key:"title" optional:""`
}

// 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.User(config.BotName),
Host: config.Token[0],
Path: fmt.Sprintf("/%s/%s", config.Token[1], config.Token[2]),
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(resolver types.ConfigQueryResolver, serviceURL *url.URL) error {

botName := serviceURL.User.Username()
if botName == "" {
botName = DefaultUser
}

host := serviceURL.Hostname()

Expand All @@ -49,12 +61,16 @@ func (config *Config) SetURL(serviceURL *url.URL) error {
return err
}

for key, vals := range serviceURL.Query() {
if err := resolver.Set(key, vals[0]); err != nil {
return err
}
}

return nil
}

const (
// DefaultUser for sending notifications to slack
DefaultUser = "Shoutrrr"
// Scheme is the identifying part of this service's configuration URL
Scheme = "slack"
)
Expand Down
51 changes: 46 additions & 5 deletions pkg/services/slack/slack_json.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,59 @@
package slack

import "encoding/json"
import (
"encoding/json"
"strings"
)

// JSON used within the Slack service
type JSON struct {
Text string `json:"text"`
BotName string `json:"username"`
Text string `json:"text"`
BotName string `json:"username,omitempty"`
Blocks []block `json:"blocks,omitempty"`
Attachments []attachment `json:"attachments,omitempty"`
}

type block struct {
Type string `json:"type"`
Text blockText `json:"text"`
}

type blockText struct {
Type string `json:"type"`
Text string `json:"text"`
}

type attachment struct {
Title string `json:"title,omitempty"`
Fallback string `json:"fallback,omitempty"`
Text string `json:"text"`
Color string `json:"color,omitempty"`
Fields []legacyField `json:"fields,omitempty"`
Footer string `json:"footer,omitempty"`
Time int `json:"ts,omitempty"`
}

type legacyField struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short,omitempty"`
}

// CreateJSONPayload compatible with the slack webhook api
func CreateJSONPayload(config *Config, message string) ([]byte, error) {

var atts []attachment
for _, line := range strings.Split(message, "\n") {
atts = append(atts, attachment{
Text: line,
Color: config.Color,
})
}

return json.Marshal(
JSON{
Text: message,
BotName: config.BotName,
Text: config.Title,
BotName: config.BotName,
Attachments: atts,
})
}
24 changes: 21 additions & 3 deletions pkg/services/slack/slack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ var _ = Describe("the slack service", func() {
}

serviceURL, _ := url.Parse(envSlackURL.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())
})
})
Expand Down Expand Up @@ -101,13 +103,29 @@ var _ = Describe("the slack service", func() {
})
})
Describe("the slack config", func() {
When("parsing the configuration URL", func() {
It("should be identical after de-/serialization", func() {
testURL := "slack://testbot@AAAAAAAAA/BBBBBBBBB/123456789123456789123456?color=3f00fe&title=Test title"

url, err := url.Parse(testURL)
Expect(err).NotTo(HaveOccurred(), "parsing")

config := &Config{}
err = config.SetURL(url)
Expect(err).NotTo(HaveOccurred(), "verifying")

outputURL := config.GetURL()
Expect(outputURL.String()).To(Equal(testURL))

})
})
When("generating a config object", func() {
It("should use the default botname if the argument list contains three strings", func() {
slackURL, _ := url.Parse("slack://AAAAAAAAA/BBBBBBBBB/123456789123456789123456")
config, configError := CreateConfigFromURL(slackURL)

Expect(config.BotName).To(Equal(DefaultUser))
Expect(configError).NotTo(HaveOccurred())
Expect(config.BotName).To(BeEmpty())
})
It("should set the botname if the argument list is three", func() {
slackURL, _ := url.Parse("slack://testbot@AAAAAAAAA/BBBBBBBBB/123456789123456789123456")
Expand Down
1 change: 1 addition & 0 deletions pkg/services/slack/slack_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
// Token is a three part string split into A, B and C
type Token []string

// ValidateToken checks that the token is in the expected format
func ValidateToken(token Token) error {
if err := tokenPartsAreNotEmpty(token); err != nil {
return err
Expand Down

0 comments on commit 7e4a3bf

Please sign in to comment.