Skip to content

Commit

Permalink
feat(service): add generic webhook service (#144)
Browse files Browse the repository at this point in the history
* feat(format): improve default props handling
this makes it possible to ignore errors setting
config props and defaults

* feat(service): add generic webhook service
* docs(generic): add some basic docs
* test(format): add and split additional tests
* docs: fix broken markdown formatting
  • Loading branch information
piksel committed May 14, 2021
1 parent 23170b8 commit f72cbdc
Show file tree
Hide file tree
Showing 13 changed files with 966 additions and 269 deletions.
9 changes: 8 additions & 1 deletion docs/services/overview.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Services overview

Click on the service for a more thorough explanation.
Click on the service for a more thorough explanation. <!-- @formatter:off -->

| Service | URL format |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
Expand All @@ -19,3 +19,10 @@ Click on the service for a more thorough explanation.
| [Teams](./teams.md) | *teams://__`token-a`__/__`token-b`__/__`token-c`__* |
| [Telegram](./telegram.md) | *telegram://__`token`__@telegram?channels=__`channel-1`__[,__`channel-2`__,...]* |
| [Zulip Chat](./zulip.md) | *zulip://__`bot-mail`__:__`bot-key`__@__`zulip-domain`__/?stream=__`name-or-id`__&topic=__`name`__* |

## Specialized services

| Service | Description |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| [Logger](./not-documented.md) | Writes notification to a configured go `log.Logger` |
| [Generic Webhook](./generic.md) | Sends notifications directly to a webhook |
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ nav:
- Teams: 'services/teams.md'
- Telegram: 'services/telegram.md'
- Zulip Chat: 'services/zulip.md'
- Generic Webhook: 'services/generic.md'
- Advanced usage:
- Proxy: 'proxy.md'

Expand Down
71 changes: 66 additions & 5 deletions pkg/format/format_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,91 @@ package format

import (
"net/url"
"strings"

"github.com/containrrr/shoutrrr/pkg/types"
t "github.com/containrrr/shoutrrr/pkg/types"
)

// BuildQuery converts the fields of a config object to a delimited query string
func BuildQuery(cqr types.ConfigQueryResolver) string {
query := url.Values{}
func BuildQuery(cqr t.ConfigQueryResolver) string {
return BuildQueryWithCustomFields(cqr, url.Values{}).Encode()
}

// BuildQueryWithCustomFields converts the fields of a config object to a delimited query string,
// escaping any custom fields that share the same key as a config prop using a "__" prefix
func BuildQueryWithCustomFields(cqr t.ConfigQueryResolver, query url.Values) url.Values {
fields := cqr.QueryFields()
skipEscape := len(query) < 1

pkr, isPkr := cqr.(*PropKeyResolver)

for _, key := range fields {
if !skipEscape {
// Escape any webhook query keys using the same name as service props
escValues := query[key]
if len(escValues) > 0 {
query.Del(key)
query[EscapeKey(key)] = escValues
}
}

if isPkr && !pkr.KeyIsPrimary(key) {
continue
}
value, err := cqr.Get(key)


if err != nil || isPkr && pkr.IsDefault(key, value) {
continue
}

query.Set(key, value)
}

return query.Encode()
return query
}

// SetConfigPropsFromQuery iterates over all the config prop keys and sets the config prop to the corresponding
// query value based on the key.
// SetConfigPropsFromQuery returns a non-nil url.Values query with all config prop keys removed, even if any of
// them could not be used to set a config field, and with any escaped keys unescaped.
// The error returned is the first error that occurred, subsequent errors are just discarded.
func SetConfigPropsFromQuery(cqr t.ConfigQueryResolver, query url.Values) (url.Values, error) {
var firstError error
if query == nil {
return url.Values{}, nil
}
for _, key := range cqr.QueryFields() {
// Retrieve the service-related prop value
values := query[key]
if len(values) > 0 {
if err := cqr.Set(key, values[0]); err != nil && firstError == nil {
firstError = err
}
}
// Remove it from the query Values
query.Del(key)

// If an escaped version of the key exist, unescape it
escKey := EscapeKey(key)
escValues := query[escKey]
if len(escValues) > 0 {
query.Del(escKey)
query[key] = escValues
}
}
return query, firstError
}

// EscapeKey adds the KeyPrefix to custom URL query keys that conflict with service config prop keys
func EscapeKey(key string) string {
return KeyPrefix + key
}

// UnescapeKey removes the KeyPrefix from custom URL query keys that conflict with service config prop keys
func UnescapeKey(key string) string {
return strings.TrimPrefix(key, KeyPrefix)
}

// KeyPrefix is the prefix prepended to custom URL query keys that conflict with service config prop keys,
// consisting of two underscore characters ("__")
const KeyPrefix = "__"
48 changes: 48 additions & 0 deletions pkg/format/format_query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package format

import (
"net/url"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Query Formatter", func() {
var pkr PropKeyResolver
BeforeEach(func() {
ts = &testStruct{}
pkr = NewPropKeyResolver(ts)
_ = pkr.SetDefaultProps(ts)
})
Describe("Creating a service URL query from a config", func() {
When("a config property has been changed from default", func() {
It("should be included in the query string", func() {
ts.Str = "test"
query := BuildQuery(&pkr)
// (pkr, )
Expect(query).To(Equal("str=test"))
})
})
When("a custom query key conflicts with a config property key", func() {
It("should include both values, with the custom escaped", func() {
ts.Str = "service"
customQuery := url.Values{"str": {"custom"}}
query := BuildQueryWithCustomFields(&pkr, customQuery)
Expect(query.Encode()).To(Equal("__str=custom&str=service"))
})
})
})
Describe("Setting prop values from query", func() {
When("a custom query key conflicts with a config property key", func() {
It("should set the config prop from the regular and return the custom one unescaped", func() {
ts.Str = "service"
serviceQuery := url.Values{"__str": {"custom"}, "str": {"service"}}
query, err := SetConfigPropsFromQuery(&pkr, serviceQuery)
Expect(err).NotTo(HaveOccurred())
Expect(ts.Str).To(Equal("service"))
Expect(query.Get("str")).To(Equal("custom"))
})
})
})

})
Loading

0 comments on commit f72cbdc

Please sign in to comment.