From 8ba67fe7f6c3f7b8076931c42fe803da671882dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Tue, 10 Aug 2021 14:30:28 +0200 Subject: [PATCH 1/4] add static version meta info --- cli/main.go | 6 ++++-- internal/meta/version.go | 7 +++++++ pkg/util/docs.go | 11 +++++++++++ shoutrrr.go | 6 ++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 internal/meta/version.go create mode 100644 pkg/util/docs.go diff --git a/cli/main.go b/cli/main.go index 22c28c09..cb544b1b 100644 --- a/cli/main.go +++ b/cli/main.go @@ -6,14 +6,16 @@ import ( "github.com/containrrr/shoutrrr/cli/cmd/generate" "github.com/containrrr/shoutrrr/cli/cmd/send" "github.com/containrrr/shoutrrr/cli/cmd/verify" + "github.com/containrrr/shoutrrr/internal/meta" "github.com/spf13/cobra" "github.com/spf13/viper" "os" ) var cmd = &cobra.Command{ - Use: "shoutrrr", - Short: "Notification library for gophers and their furry friends", + Use: "shoutrrr", + Version: meta.Version, + Short: "Shoutrrr CLI", } func init() { diff --git a/internal/meta/version.go b/internal/meta/version.go new file mode 100644 index 00000000..b60e41aa --- /dev/null +++ b/internal/meta/version.go @@ -0,0 +1,7 @@ +package meta + +// Version of Shoutrrr +const Version = `0.5-dev` + +// DocsVersion is prepended to documentation URLs and usually equals MAJOR.MINOR of Version +const DocsVersion = `dev` diff --git a/pkg/util/docs.go b/pkg/util/docs.go new file mode 100644 index 00000000..c4e956bc --- /dev/null +++ b/pkg/util/docs.go @@ -0,0 +1,11 @@ +package util + +import ( + "fmt" + + "github.com/containrrr/shoutrrr/internal/meta" +) + +func DocsURL(path string) string { + return fmt.Sprintf("https://containrrr.dev/shoutrrr/%s/%s", meta.DocsVersion, path) +} diff --git a/shoutrrr.go b/shoutrrr.go index 8e005409..30dd897b 100644 --- a/shoutrrr.go +++ b/shoutrrr.go @@ -1,6 +1,7 @@ package shoutrrr import ( + "github.com/containrrr/shoutrrr/internal/meta" "github.com/containrrr/shoutrrr/pkg/router" "github.com/containrrr/shoutrrr/pkg/types" ) @@ -32,3 +33,8 @@ func CreateSender(rawURLs ...string) (*router.ServiceRouter, error) { func NewSender(logger types.StdLogger, serviceURLs ...string) (*router.ServiceRouter, error) { return router.New(logger, serviceURLs...) } + +// Version returns the shoutrrr version +func Version() string { + return meta.Version +} From 0626bfc4d14adc1d4b3c39ebc2bb749e99cf3d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Tue, 10 Aug 2021 14:34:48 +0200 Subject: [PATCH 2/4] fix(teams): use correct path in webhook URL --- pkg/services/teams/teams.go | 12 +++++--- pkg/services/teams/teams_config.go | 29 +++++++++++++----- pkg/services/teams/teams_test.go | 48 ++++++++++++++++++++---------- 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/pkg/services/teams/teams.go b/pkg/services/teams/teams.go index 151b1aad..0c59ba67 100644 --- a/pkg/services/teams/teams.go +++ b/pkg/services/teams/teams.go @@ -4,13 +4,14 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/containrrr/shoutrrr/pkg/format" "net/http" "net/url" "strings" + "github.com/containrrr/shoutrrr/pkg/format" "github.com/containrrr/shoutrrr/pkg/services/standard" "github.com/containrrr/shoutrrr/pkg/types" + "github.com/containrrr/shoutrrr/pkg/util" ) // Service providing teams as a notification service @@ -35,7 +36,7 @@ func (service *Service) Send(message string, params *types.Params) error { func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error { service.Logger.SetLogger(logger) service.config = &Config{ - Host: DefaultHost, + Host: LegacyHost, } service.pkr = format.NewPropKeyResolver(service.config) @@ -93,9 +94,12 @@ func (service *Service) doSend(config *Config, message string) error { host := config.Host if host == "" { - host = DefaultHost + host = LegacyHost + // Emit a warning to the log for now. + // TODO(v0.6): Remove legacy support as it should be fully deprecated now + service.Logf(`Warning: No host specified, update your Teams URL: %s`, util.DocsURL(`services/teams`)) } - postURL := buildWebhookURL(host, config.webhookParts()) + postURL := buildWebhookURL(host, config.Group, config.Tenant, config.AltID, config.GroupOwner) res, err := http.Post(postURL, "application/json", bytes.NewBuffer(payload)) if err == nil && res.StatusCode != http.StatusOK { diff --git a/pkg/services/teams/teams_config.go b/pkg/services/teams/teams_config.go index c0e5e2c7..c47a420b 100644 --- a/pkg/services/teams/teams_config.go +++ b/pkg/services/teams/teams_config.go @@ -114,14 +114,21 @@ func (config *Config) setFromWebhookParts(parts [4]string) { config.GroupOwner = parts[3] } -func buildWebhookURL(host string, parts [4]string) string { +func buildWebhookURL(host, group, tenant, altID, groupOwner string) string { + // config.Group, config.Tenant, config.AltID, config.GroupOwner + path := Path + if host == LegacyHost { + path = LegacyPath + } return fmt.Sprintf( - "https://%s/webhook/%s@%s/IncomingWebhook/%s/%s", + "https://%s/%s/%s@%s/%s/%s/%s", host, - parts[0], - parts[1], - parts[2], - parts[3]) + path, + group, + tenant, + ProviderName, + altID, + groupOwner) } func parseAndVerifyWebhookURL(webhookURL string) (parts [4]string, err error) { @@ -142,6 +149,12 @@ func parseAndVerifyWebhookURL(webhookURL string) (parts [4]string, err error) { const ( // Scheme is the identifying part of this service's configuration URL Scheme = "teams" - // DefaultHost is the default host for the webhook request - DefaultHost = "outlook.office.com" + // LegacyHost is the default host for legacy webhook requests + LegacyHost = "outlook.office.com" + // LegacyPath is the initial path of the webhook URL for legacy webhook requests + LegacyPath = "webhook" + // Path is the initial path of the webhook URL for domain-scoped webhook requests + Path = "webhookb2" + // ProviderName is the name of the Teams integration provider + ProviderName = "IncomingWebhook" ) diff --git a/pkg/services/teams/teams_test.go b/pkg/services/teams/teams_test.go index 48241e51..a3e3d22b 100644 --- a/pkg/services/teams/teams_test.go +++ b/pkg/services/teams/teams_test.go @@ -12,9 +12,11 @@ import ( ) const ( - testWebhookURL = "https://outlook.office.com/webhook/11111111-4444-4444-8444-cccccccccccc@22222222-4444-4444-8444-cccccccccccc/IncomingWebhook/33333333012222222222333333333344/44444444-4444-4444-8444-cccccccccccc" - customURL = "teams+https://publicservice.info/webhook/11111111-4444-4444-8444-cccccccccccc@22222222-4444-4444-8444-cccccccccccc/IncomingWebhook/33333333012222222222333333333344/44444444-4444-4444-8444-cccccccccccc" - testURLBase = "teams://11111111-4444-4444-8444-cccccccccccc@22222222-4444-4444-8444-cccccccccccc/33333333012222222222333333333344/44444444-4444-4444-8444-cccccccccccc" + legacyWebhookURL = "https://outlook.office.com/webhook/11111111-4444-4444-8444-cccccccccccc@22222222-4444-4444-8444-cccccccccccc/IncomingWebhook/33333333012222222222333333333344/44444444-4444-4444-8444-cccccccccccc" + scopedWebhookURL = "https://test.webhook.office.com/webhookb2/11111111-4444-4444-8444-cccccccccccc@22222222-4444-4444-8444-cccccccccccc/IncomingWebhook/33333333012222222222333333333344/44444444-4444-4444-8444-cccccccccccc" + scopedDomainHost = "test.webhook.office.com" + testURLBase = "teams://11111111-4444-4444-8444-cccccccccccc@22222222-4444-4444-8444-cccccccccccc/33333333012222222222333333333344/44444444-4444-4444-8444-cccccccccccc" + scopedURLBase = testURLBase + `?host=` + scopedDomainHost ) var logger = log.New(GinkgoWriter, "Test", log.LstdFlags) @@ -24,9 +26,9 @@ func TestTeams(t *testing.T) { RunSpecs(t, "Shoutrrr Teams Suite") } -var _ = Describe("the teams plugin", func() { +var _ = Describe("the teams service", func() { When("creating the webhook URL", func() { - It("should match the expected output", func() { + It("should match the expected output for legacy URLs", func() { config := Config{} config.setFromWebhookParts([4]string{ "11111111-4444-4444-8444-cccccccccccc", @@ -34,8 +36,23 @@ var _ = Describe("the teams plugin", func() { "33333333012222222222333333333344", "44444444-4444-4444-8444-cccccccccccc", }) - apiURL := buildWebhookURL(DefaultHost, config.webhookParts()) - Expect(apiURL).To(Equal(testWebhookURL)) + apiURL := buildWebhookURL(LegacyHost, config.Group, config.Tenant, config.AltID, config.GroupOwner) + Expect(apiURL).To(Equal(legacyWebhookURL)) + + parts, err := parseAndVerifyWebhookURL(apiURL) + Expect(err).ToNot(HaveOccurred()) + Expect(parts).To(Equal(config.webhookParts())) + }) + It("should match the expected output for custom URLs", func() { + config := Config{} + config.setFromWebhookParts([4]string{ + "11111111-4444-4444-8444-cccccccccccc", + "22222222-4444-4444-8444-cccccccccccc", + "33333333012222222222333333333344", + "44444444-4444-4444-8444-cccccccccccc", + }) + apiURL := buildWebhookURL(scopedDomainHost, config.Group, config.Tenant, config.AltID, config.GroupOwner) + Expect(apiURL).To(Equal(scopedWebhookURL)) parts, err := parseAndVerifyWebhookURL(apiURL) Expect(err).ToNot(HaveOccurred()) @@ -46,18 +63,17 @@ var _ = Describe("the teams plugin", func() { Describe("creating a config", func() { When("parsing the configuration URL", func() { It("should be identical after de-/serialization", func() { - testURL := testURLBase + "?color=aabbcc&host=notdefault.outlook.office.com&title=Test+title" + testURL := testURLBase + "?color=aabbcc&host=test.outlook.office.com&title=Test+title" url, err := url.Parse(testURL) Expect(err).NotTo(HaveOccurred(), "parsing") - config := &Config{Host: DefaultHost} + config := &Config{Host: LegacyHost} err = config.SetURL(url) Expect(err).NotTo(HaveOccurred(), "verifying") outputURL := config.GetURL() Expect(outputURL.String()).To(Equal(testURL)) - }) }) }) @@ -78,7 +94,7 @@ var _ = Describe("the teams plugin", func() { When("a valid custom URL is provided", func() { It("should set the host field from the custom URL", func() { service := Service{} - testURL := customURL + testURL := `teams+` + scopedWebhookURL customURL, err := url.Parse(testURL) Expect(err).NotTo(HaveOccurred(), "parsing") @@ -86,11 +102,11 @@ var _ = Describe("the teams plugin", func() { serviceURL, err := service.GetConfigURLFromCustom(customURL) Expect(err).NotTo(HaveOccurred(), "converting") - Expect(serviceURL.String()).To(Equal(testURLBase + "?host=publicservice.info")) + Expect(serviceURL.String()).To(Equal(scopedURLBase)) }) It("should preserve the query params in the generated service URL", func() { service := Service{} - testURL := "teams+" + testWebhookURL + "?color=f008c1&title=TheTitle" + testURL := "teams+" + legacyWebhookURL + "?color=f008c1&title=TheTitle" customURL, err := url.Parse(testURL) Expect(err).NotTo(HaveOccurred(), "parsing") @@ -113,11 +129,11 @@ var _ = Describe("the teams plugin", func() { httpmock.DeactivateAndReset() }) It("should not report an error if the server accepts the payload", func() { - serviceURL, _ := url.Parse(testURLBase) + serviceURL, _ := url.Parse(scopedURLBase) err = service.Initialize(serviceURL, logger) Expect(err).NotTo(HaveOccurred()) - httpmock.RegisterResponder("POST", testWebhookURL, httpmock.NewStringResponder(200, "")) + httpmock.RegisterResponder("POST", scopedWebhookURL, httpmock.NewStringResponder(200, "")) err = service.Send("Message", nil) Expect(err).NotTo(HaveOccurred()) @@ -127,7 +143,7 @@ var _ = Describe("the teams plugin", func() { err = service.Initialize(serviceURL, logger) Expect(err).NotTo(HaveOccurred()) - httpmock.RegisterResponder("POST", testWebhookURL, httpmock.NewErrorResponder(errors.New("dummy error"))) + httpmock.RegisterResponder("POST", legacyWebhookURL, httpmock.NewErrorResponder(errors.New("dummy error"))) err = service.Send("Message", nil) Expect(err).To(HaveOccurred()) From 305f97b27ae23a0ea059a47469edbb120f4925b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Tue, 10 Aug 2021 14:50:52 +0200 Subject: [PATCH 3/4] add missing docs --- pkg/util/docs.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/util/docs.go b/pkg/util/docs.go index c4e956bc..2d137e1a 100644 --- a/pkg/util/docs.go +++ b/pkg/util/docs.go @@ -6,6 +6,12 @@ import ( "github.com/containrrr/shoutrrr/internal/meta" ) +// DocsURL returns a full documentation URL for the current version of Shoutrrr with the path appended. +// If the path contains a leading slash, it is stripped. func DocsURL(path string) string { + // strip leading slash if present + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } return fmt.Sprintf("https://containrrr.dev/shoutrrr/%s/%s", meta.DocsVersion, path) } From 51fe0c3afc9ff21b64c867afeac7ab7dd8570d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Tue, 10 Aug 2021 15:16:28 +0200 Subject: [PATCH 4/4] add tests --- pkg/util/partition_message_test.go | 4 ---- pkg/util/util_test.go | 13 +++++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/util/partition_message_test.go b/pkg/util/partition_message_test.go index fb0ace3a..e3201616 100644 --- a/pkg/util/partition_message_test.go +++ b/pkg/util/partition_message_test.go @@ -118,14 +118,10 @@ func testPartitionMessage(hundreds int, limits types.MessageLimit, distance int) items, omitted = PartitionMessage(builder.String(), limits, distance) - println(hundreds, len(items), omitted) - contentSize := Min(hundreds*100, limits.TotalChunkSize) expectedChunkCount := CeilDiv(contentSize, limits.ChunkSize-1) expectedOmitted := Max(0, (hundreds*100)-contentSize) - println(contentSize, expectedChunkCount, expectedOmitted) - Expect(omitted).To(Equal(expectedOmitted)) Expect(len(items)).To(Equal(expectedChunkCount)) diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index f789a913..34f58b4c 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -1,12 +1,14 @@ package util_test import ( + "fmt" "reflect" "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/containrrr/shoutrrr/internal/meta" . "github.com/containrrr/shoutrrr/pkg/util" ) @@ -91,4 +93,15 @@ var _ = Describe("the util package", func() { Expect(IsNumeric(reflect.TypeOf("3").Kind())).To(BeFalse()) }) }) + + When("calling function DocsURL", func() { + It("should return the expected URL", func() { + expectedBase := fmt.Sprintf(`https://containrrr.dev/shoutrrr/%s/`, meta.DocsVersion) + Expect(DocsURL(``)).To(Equal(expectedBase)) + Expect(DocsURL(`services/logger`)).To(Equal(expectedBase + `services/logger`)) + }) + It("should strip the leading slash from the path", func() { + Expect(DocsURL(`/foo`)).To(Equal(DocsURL(`foo`))) + }) + }) })