Skip to content

Commit

Permalink
Merge branch 'main' of into latest
Browse files Browse the repository at this point in the history
  • Loading branch information
piksel committed Jul 19, 2022
2 parents 84afd59 + 357c766 commit 666b520
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 35 deletions.
16 changes: 14 additions & 2 deletions docs/services/generic.md
@@ -1,9 +1,21 @@
# Generic
The Generic service can be used for any target that is not explicitly supported by Shoutrrr, as long as it
supports recieving the message via a POST request.
Usually, this requires customization on the recieving end to interpret the payload that it recives, and might
supports receiving the message via a POST request.
Usually, this requires customization on the receiving end to interpret the payload that it receives, and might
not be a viable approach.

## JSON template
By using the built in `JSON` template (`template=json`) you can create a generic JSON payload. The keys used for `title` and `message` can be overriden
by supplying the params/query values `titleKey` and `messageKey`.

!!! example
```json
{
"title": "Oh no!",
"message": "The thing happened and now there is stuff all over the area!"
}
```

## Shortcut URL
You can just add `generic+` as a prefix to your target URL to use it with the generic service, so
```
Expand Down
78 changes: 58 additions & 20 deletions pkg/services/generic/generic.go
@@ -1,11 +1,15 @@
package generic

import (
"bytes"
"fmt"
"encoding/json"
"io/ioutil"

"github.com/containrrr/shoutrrr/pkg/format"
"github.com/containrrr/shoutrrr/pkg/services/standard"
"github.com/containrrr/shoutrrr/pkg/types"

"bytes"
"fmt"
"io"
"net/http"
"net/url"
Expand All @@ -20,14 +24,23 @@ type Service struct {
}

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

if err := service.pkr.UpdateConfigFromParams(config, params); err != nil {
var params types.Params
if paramsPtr == nil {
params = types.Params{}
} else {
params = *paramsPtr
}

if err := service.pkr.UpdateConfigFromParams(&config, &params); err != nil {
service.Logf("Failed to update params: %v", err)
}

if err := service.doSend(config, message, params); err != nil {
updateParams(&config, params, message)

if err := service.doSend(&config, params); err != nil {
return fmt.Errorf("an error occurred while sending notification to generic webhook: %s", err.Error())
}

Expand Down Expand Up @@ -57,35 +70,60 @@ func (*Service) GetConfigURLFromCustom(customURL *url.URL) (serviceURL *url.URL,
return config.getURL(&pkr), nil
}

func (service *Service) doSend(config *Config, message string, params *types.Params) error {
func (service *Service) doSend(config *Config, params types.Params) error {
postURL := config.WebhookURL().String()
payload, err := service.getPayload(config.Template, message, params)
payload, err := service.getPayload(config, params)
if err != nil {
return err
}

res, err := http.Post(postURL, config.ContentType, payload)
if err == nil && res.StatusCode != http.StatusOK {
err = fmt.Errorf("server returned response status code %s", res.Status)
req, err := http.NewRequest(config.RequestMethod, postURL, payload)
if err == nil {
req.Header.Set("Content-Type", config.ContentType)
req.Header.Set("Accept", config.ContentType)
var res *http.Response
res, err = http.DefaultClient.Do(req)
if res != nil && res.Body != nil {
defer res.Body.Close()
if body, errRead := ioutil.ReadAll(res.Body); errRead == nil {
service.Log("Server response: ", string(body))
}
}
if err == nil && res.StatusCode >= http.StatusMultipleChoices {
err = fmt.Errorf("server returned response status code %s", res.Status)
}
}

return err
}

func (service *Service) getPayload(template string, message string, params *types.Params) (io.Reader, error) {
if template == "" {
return bytes.NewBufferString(message), nil
func (service *Service) getPayload(config *Config, params types.Params) (io.Reader, error) {
switch config.Template {
case "":
return bytes.NewBufferString(params[config.MessageKey]), nil
case "json", "JSON":
jsonBytes, err := json.Marshal(params)
if err != nil {
return nil, err
}
return bytes.NewBuffer(jsonBytes), nil
}
tpl, found := service.GetTemplate(template)
tpl, found := service.GetTemplate(config.Template)
if !found {
return nil, fmt.Errorf("template %q has not been loaded", template)
return nil, fmt.Errorf("template %q has not been loaded", config.Template)
}

if params == nil {
params = &types.Params{}
}
params.SetMessage(message)
bb := &bytes.Buffer{}
err := tpl.Execute(bb, params)
return bb, err
}

func updateParams(config *Config, params types.Params, message string) {
if title, found := params.Title(); found {
if config.TitleKey != "title" {
delete(params, "title")
params[config.TitleKey] = title
}
}
params[config.MessageKey] = message
}
16 changes: 10 additions & 6 deletions pkg/services/generic/generic_config.go
@@ -1,20 +1,24 @@
package generic

import (
"net/url"

"github.com/containrrr/shoutrrr/pkg/format"
"github.com/containrrr/shoutrrr/pkg/services/standard"
t "github.com/containrrr/shoutrrr/pkg/types"
"net/url"
)

// Config for use within the generic service
type Config struct {
standard.EnumlessConfig
webhookURL *url.URL
ContentType string `key:"contenttype" default:"application/json" desc:"The value of the Content-Type header"`
DisableTLS bool `key:"disabletls" default:"No"`
Template string `key:"template" optional:""`
Title string `key:"title" default:""`
webhookURL *url.URL
ContentType string `key:"contenttype" default:"application/json" desc:"The value of the Content-Type header"`
DisableTLS bool `key:"disabletls" default:"No"`
Template string `key:"template" optional:"" desc:"The template used for creating the request payload"`
Title string `key:"title" default:""`
TitleKey string `key:"titlekey" default:"title" desc:"The key that will be used for the title value"`
MessageKey string `key:"messagekey" default:"message" desc:"The key that will be used for the message value"`
RequestMethod string `key:"method" default:"POST"`
}

// DefaultConfig creates a PropKeyResolver and uses it to populate the default values of a new Config, returning both
Expand Down
68 changes: 61 additions & 7 deletions pkg/services/generic/generic_test.go
Expand Up @@ -2,14 +2,15 @@ package generic

import (
"errors"
"github.com/containrrr/shoutrrr/pkg/format"
"github.com/containrrr/shoutrrr/pkg/types"
"github.com/jarcoal/httpmock"
"io/ioutil"
"log"
"net/url"
"testing"

"github.com/containrrr/shoutrrr/pkg/format"
"github.com/containrrr/shoutrrr/pkg/types"
"github.com/jarcoal/httpmock"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
Expand Down Expand Up @@ -115,6 +116,8 @@ var _ = Describe("the Generic service", func() {
Expect(err).NotTo(HaveOccurred(), "parsing")

config := &Config{}
pkr := format.NewPropKeyResolver(config)
Expect(pkr.SetDefaultProps(config)).To(Succeed())
err = config.SetURL(url)
Expect(err).NotTo(HaveOccurred(), "verifying")

Expand All @@ -127,25 +130,64 @@ var _ = Describe("the Generic service", func() {

Describe("building the payload", func() {
var service Service
var config Config
BeforeEach(func() {
service = Service{}
config = Config{
MessageKey: "message",
TitleKey: "title",
}
})
When("no template is specified", func() {
It("should use the message as payload", func() {
payload, err := service.getPayload("", "test message", nil)
payload, err := service.getPayload(&config, types.Params{"message": "test message"})
Expect(err).NotTo(HaveOccurred())
contents, err := ioutil.ReadAll(payload)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("test message"))
})
})
When("template is specified as `JSON`", func() {
It("should create a JSON object as the payload", func() {
config.Template = "JSON"
params := types.Params{"title": "test title"}
updateParams(&config, params, "test message")
payload, err := service.getPayload(&config, params)
Expect(err).NotTo(HaveOccurred())
contents, err := ioutil.ReadAll(payload)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(MatchJSON(`{
"title": "test title",
"message": "test message"
}`))
})
When("alternate keys are specified", func() {
It("should create a JSON object using the specified keys", func() {
config.Template = "JSON"
config.MessageKey = "body"
config.TitleKey = "header"
params := types.Params{"title": "test title"}
updateParams(&config, params, "test message")
payload, err := service.getPayload(&config, params)
Expect(err).NotTo(HaveOccurred())
contents, err := ioutil.ReadAll(payload)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(MatchJSON(`{
"header": "test title",
"body": "test message"
}`))
})
})
})
When("a valid template is specified", func() {
It("should apply the template to the message payload", func() {
err := service.SetTemplateString("news", `{{.title}} ==> {{.message}}`)
Expect(err).NotTo(HaveOccurred())
params := types.Params{}
params.SetTitle("BREAKING NEWS")
payload, err := service.getPayload("news", "it's today!", &params)
params.SetMessage("it's today!")
config.Template = "news"
payload, err := service.getPayload(&config, params)
Expect(err).NotTo(HaveOccurred())
contents, err := ioutil.ReadAll(payload)
Expect(err).NotTo(HaveOccurred())
Expand All @@ -155,7 +197,8 @@ var _ = Describe("the Generic service", func() {
It("should apply template with message data", func() {
err := service.SetTemplateString("arrows", `==> {{.message}} <==`)
Expect(err).NotTo(HaveOccurred())
payload, err := service.getPayload("arrows", "LOOK AT ME", nil)
config.Template = "arrows"
payload, err := service.getPayload(&config, types.Params{"message": "LOOK AT ME"})
Expect(err).NotTo(HaveOccurred())
contents, err := ioutil.ReadAll(payload)
Expect(err).NotTo(HaveOccurred())
Expand All @@ -165,7 +208,7 @@ var _ = Describe("the Generic service", func() {
})
When("an unknown template is specified", func() {
It("should return an error", func() {
_, err := service.getPayload("missing", "test message", nil)
_, err := service.getPayload(&Config{Template: "missing"}, nil)
Expect(err).To(HaveOccurred())
})
})
Expand Down Expand Up @@ -214,6 +257,17 @@ var _ = Describe("the Generic service", func() {
err = service.Send("Message", &types.Params{"unknown": "param"})
Expect(err).NotTo(HaveOccurred())
})
It("should use the configured HTTP method", func() {
serviceURL, _ := url.Parse("generic://host.tld/webhook?method=GET")
err = service.Initialize(serviceURL, logger)
Expect(err).NotTo(HaveOccurred())

targetURL := "https://host.tld/webhook"
httpmock.RegisterResponder("GET", targetURL, httpmock.NewStringResponder(200, ""))

err = service.Send("Message", nil)
Expect(err).NotTo(HaveOccurred())
})
})
})

Expand Down

0 comments on commit 666b520

Please sign in to comment.