diff --git a/.gitignore b/.gitignore index a19bdb3..59bb039 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -.bitrise* -.gows.user.yml +.bitrise* \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 05641f7..0000000 --- a/Gopkg.lock +++ /dev/null @@ -1,33 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - branch = "master" - digest = "1:906b09e43bbd5141e38b265702d37cefbeeba9abd2e62b23b84c9a5539319043" - name = "github.com/bitrise-io/go-utils" - packages = [ - "colorstring", - "log", - "parseutil", - "pointers", - ] - pruneopts = "UT" - revision = "2a09aab8380d7842750328aebd5671bcccea89c8" - -[[projects]] - branch = "master" - digest = "1:dacd83ec550d6d5c2ac83e57f20d45a25a39d79c73be4451771f92572a1f6344" - name = "github.com/bitrise-tools/go-steputils" - packages = ["stepconf"] - pruneopts = "UT" - revision = "ec226b2359fcd806be64f0ad2217d8e741a1ff11" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - input-imports = [ - "github.com/bitrise-io/go-utils/log", - "github.com/bitrise-tools/go-steputils/stepconf", - ] - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index d5b9a2a..0000000 --- a/Gopkg.toml +++ /dev/null @@ -1,37 +0,0 @@ -# Gopkg.toml example -# -# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true - -[[constraint]] - name = "github.com/bitrise-io/go-utils" - branch = "master" - -[[constraint]] - name = "github.com/bitrise-tools/go-steputils" - branch = "master" - -[prune] - go-tests = true - unused-packages = true diff --git a/bitrise.yml b/bitrise.yml index 214f11c..62aea3d 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -6,10 +6,9 @@ app: # An example secret param, define it (MS_TEAMS_WEBHOOK_URL) in .bitrise.secrets.yml - MS_TEAMS_WEBHOOK_URL: $MS_TEAMS_WEBHOOK_URL # If you want to share this step into a StepLib - - BITRISE_STEP_ID: send-microsoft-teams-message - - BITRISE_STEP_VERSION: "0.1.0" - - BITRISE_STEP_GIT_CLONE_URL: https://github.com/maguhiro/bitrise-step-send-microsoft-teams-message.git - - MY_STEPLIB_REPO_FORK_GIT_URL: https://github.com/maguhiro/bitrise-steplib + - BITRISE_STEP_ID: microsoft-teams-adaptive-card + - BITRISE_STEP_VERSION: "1.0.0" + - BITRISE_STEP_GIT_CLONE_URL: https://github.com/HUK-COBURG/bitrise-step-microsoft-teams-adaptive-card.git workflows: success-test: diff --git a/config.go b/config.go new file mode 100644 index 0000000..c49243d --- /dev/null +++ b/config.go @@ -0,0 +1,58 @@ +/* +This file is: + +The MIT License (MIT) + +# Copyright (c) 2014 Bitrise + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +package main + +import ( + "github.com/bitrise-io/go-steputils/stepconf" + "os" +) + +// Buildstatus +var success = os.Getenv("BITRISE_BUILD_STATUS") == "0" + +// Config ... +type Config struct { + // Settings + Debug bool `env:"is_debug_mode,opt[yes,no]"` + WebhookURL stepconf.Secret `env:"webhook_url"` + // Message Main + CardStyle string `env:"card_style"` + CardStyleOnError string `env:"card_style_on_error"` + CardHeadline string `env:"card_headline"` + CardHeadlineOnError string `env:"card_headline_on_error"` + + Title string `env:"title"` + TitleOnError string `env:"title_on_error"` + // Message Git + AuthorName string `env:"author_name"` + Subject string `env:"subject"` + // Message Content + Fields string `env:"fields"` + Images string `env:"images"` + ImagesOnError string `env:"images_on_error"` + Buttons string `env:"buttons"` + ButtonsOnError string `env:"buttons_on_error"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2756e2f --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module bitrise-step-microsoft-teams-adaptive-card + +go 1.23.4 + +require ( + github.com/atc0005/go-teams-notify/v2 v2.13.0 + github.com/bitrise-io/go-steputils v0.0.0-20201016102104-03ae3a6ded35 + github.com/bitrise-io/go-utils v0.0.0-20201211082830-859032e9adf0 +) diff --git a/main.go b/main.go index 8570257..4b2ee0f 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,7 @@ This file is: The MIT License (MIT) -Copyright (c) 2014 Bitrise +# Copyright (c) 2014 Bitrise Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -29,38 +29,16 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "os" "strings" + "github.com/bitrise-io/go-steputils/stepconf" "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-tools/go-steputils/stepconf" -) - -// Config ... -type Config struct { - // Settings - Debug bool `env:"is_debug_mode,opt[yes,no]"` - WebhookURL stepconf.Secret `env:"webhook_url"` - // Message Main - ThemeColor string `env:"theme_color"` - ThemeColorOnError string `env:"theme_color_on_error"` - Title string `env:"title"` - TitleOnError string `env:"title_on_error"` - // Message Git - AuthorName string `env:"author_name"` - Subject string `env:"subject"` - // Message Content - Fields string `env:"fields"` - Images string `env:"images"` - ImagesOnError string `env:"images_on_error"` - Buttons string `env:"buttons"` - ButtonsOnError string `env:"buttons_on_error"` -} -// success is true if the build is successful, false otherwise. -var success = os.Getenv("BITRISE_BUILD_STATUS") == "0" + "github.com/atc0005/go-teams-notify/v2/adaptivecard" +) // selectValue chooses the right value based on the result of the build. func selectValue(ifSuccess, ifFailed string) string { @@ -70,32 +48,130 @@ func selectValue(ifSuccess, ifFailed string) string { return ifFailed } -// ensureNewlines replaces all \n substrings with newline characters. -func ensureNewlines(s string) string { - return strings.Replace(s, "\\n", "\n", -1) +func NewCard(c Config) adaptivecard.Card { + + card := adaptivecard.NewCard() + card.Type = "AdaptiveCard" + card.Schema = "http://adaptivecards.io/schemas/adaptive-card.json" + card.Version = "1.5" + + // Create style depending on build status + statusBanner := adaptivecard.NewContainer() + headline := adaptivecard.NewTextBlock("", false) + headline.Size = "large" + headline.Weight = "bolder" + headline.Style = "heading" + if success { + statusBanner.Style = c.CardStyle + headline.Color = c.CardStyle + headline.Text = c.CardHeadline + } else { + statusBanner.Style = c.CardStyleOnError + headline.Color = c.CardStyleOnError + headline.Text = c.CardHeadlineOnError + } + statusBanner.Spacing = "None" + statusBanner.Separator = true + statusBanner.Items = append(statusBanner.Items, headline) + card.Body = append(card.Body, adaptivecard.Element(statusBanner)) + + // Main Section + mainContainer := adaptivecard.NewContainer() + mainContainer.Style = "default" + mainContainer.Spacing = "medium" + if selectValue(c.Title, c.TitleOnError) != "" { + mainContainer.Items = append(mainContainer.Items, adaptivecard.NewTextBlock(selectValue(c.Title, c.TitleOnError), false)) + } + + if c.AuthorName != "" { + mainContainer.Items = append(mainContainer.Items, adaptivecard.NewTextBlock(c.AuthorName, false)) + } + + if c.Subject != "" { + mainContainer.Items = append(mainContainer.Items, adaptivecard.NewTextBlock(c.Subject, true)) + } + + factSet := adaptivecard.NewFactSet() + for _, fact := range parsesFacts(c.Fields) { + err := factSet.AddFact(fact) + if err != nil { + log.Errorf("Could not add fact to factset %v", err) + } + } + if len(factSet.Facts) > 0 { + mainContainer.Items = append(mainContainer.Items, adaptivecard.Element(factSet)) + } + + if len(mainContainer.Items) > 0 { + card.Body = append(card.Body, adaptivecard.Element(mainContainer)) + } + + // Images + imageContainer := parsesImages(selectValue(c.Images, c.ImagesOnError)) + + if len(imageContainer.Items) > 0 { + card.Body = append(card.Body, adaptivecard.Element(imageContainer)) + } + + // Actions + actions := parsesActions(selectValue(c.Buttons, c.ButtonsOnError)) + if len(actions.Actions) > 0 { + card.Body = append(card.Body, actions) + } + + return card +} + +func parsesFacts(s string) (fs []adaptivecard.Fact) { + for _, p := range pairs(s) { + fs = append(fs, adaptivecard.Fact{Title: p[0], Value: p[1]}) + } + return +} + +func parsesImages(s string) (container adaptivecard.Container) { + container = adaptivecard.NewContainer() + for _, p := range pairs(s) { + + image := adaptivecard.Element{ + URL: p[1], + Type: "Image", + Size: "large", + } + + err := container.AddElement(false, image) + if err != nil { + log.Errorf("Could not add image %v", err) + } + } + return container +} + +func parsesActions(s string) (as adaptivecard.Element) { + as = adaptivecard.NewActionSet() + for _, p := range pairs(s) { + action, _ := adaptivecard.NewActionOpenURL(p[1], p[0]) + as.Actions = append(as.Actions, action) + } + + return as } -func newMessage(c Config) Message { - msg := Message{ - Context: "https://schema.org/extension", - Type: "MessageCard", - ThemeColor: selectValue(c.ThemeColor, c.ThemeColorOnError), - Title: selectValue(c.Title, c.TitleOnError), - Summary: "Result of Bitrise", - Sections: []Section{{ - ActivityTitle: c.AuthorName, - ActivityText: ensureNewlines(c.Subject), - Facts: parsesFacts(c.Fields), - Images: parsesImages(selectValue(c.Images, c.ImagesOnError)), - Actions: parsesActions(selectValue(c.Buttons, c.ButtonsOnError)), - }}, - } - - return msg +// pairs slices every lines in s into two substrings separated by the first pipe +// character and returns a slice of those pairs. +func pairs(s string) [][2]string { + var ps [][2]string + for _, line := range strings.Split(s, "\n") { + a := strings.SplitN(line, "|", 2) + if len(a) == 2 && a[0] != "" && a[1] != "" { + ps = append(ps, [2]string{a[0], a[1]}) + } + } + return ps } -// postMessage sends a message. -func postMessage(conf Config, msg Message) error { +// PostCard sends the given adaptive card to configured webhook +func PostCard(conf Config, msg adaptivecard.Card) error { b, err := json.Marshal(msg) if err != nil { return err @@ -118,7 +194,7 @@ func postMessage(conf Config, msg Message) error { }() if resp.StatusCode != http.StatusOK { - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("server error: %s, failed to read response: %s", resp.Status, err) } @@ -137,8 +213,8 @@ func main() { stepconf.Print(conf) log.SetEnableDebugLog(conf.Debug) - msg := newMessage(conf) - if err := postMessage(conf, msg); err != nil { + msg := NewCard(conf) + if err := PostCard(conf, msg); err != nil { log.Errorf("Error: %s", err) os.Exit(1) } diff --git a/message.go b/message.go deleted file mode 100644 index 70ea0cb..0000000 --- a/message.go +++ /dev/null @@ -1,110 +0,0 @@ -/* -This file is: - -The MIT License (MIT) - -Copyright (c) 2014 Bitrise - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -package main - -import ( - "strings" -) - -// See also: https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#actions -type Message struct { - Context string `json:"@context"` - Type string `json:"@type"` - ThemeColor string `json:"themeColor,omitempty"` - Title string `json:"title,omitempty"` - Summary string `json:"summary,omitempty"` - Sections []Section `json:"sections,omitempty"` -} - -type Section struct { - ActivityTitle string `json:"activityTitle,omitempty"` - ActivityText string `json:"activityText,omitempty"` - Facts []Fact `json:"facts,omitempty"` - Images []Image `json:"images,omitempty"` - Actions []Action `json:"potentialAction,omitempty"` -} - -type Fact struct { - Name string `json:"name"` - Value string `json:"value"` -} - -func parsesFacts(s string) (fs []Fact) { - for _, p := range pairs(s) { - fs = append(fs, Fact{Name: p[0], Value: p[1]}) - } - return -} - -type Image struct { - URL string `json:"image"` - Title string `json:"title"` -} - -func parsesImages(s string) (is []Image) { - for _, p := range pairs(s) { - is = append(is, Image{Title: p[0], URL: p[1]}) - } - return -} - -type Action struct { - Type string `json:"@type"` - Name string `json:"name"` - Targets []Target `json:"targets,omitempty"` -} - -type Target struct { - OS string `json:"os"` - URI string `json:"uri"` -} - -func parsesActions(s string) (as []Action) { - for _, p := range pairs(s) { - as = append(as, Action{ - Type: "OpenUri", - Name: p[0], - Targets: []Target{{ - OS: "default", - URI: p[1], - }}, - }) - } - return -} - -// pairs slices every lines in s into two substrings separated by the first pipe -// character and returns a slice of those pairs. -func pairs(s string) [][2]string { - var ps [][2]string - for _, line := range strings.Split(s, "\n") { - a := strings.SplitN(line, "|", 2) - if len(a) == 2 && a[0] != "" && a[1] != "" { - ps = append(ps, [2]string{a[0], a[1]}) - } - } - return ps -} diff --git a/step.yml b/step.yml index 7a526e6..8280f8b 100644 --- a/step.yml +++ b/step.yml @@ -8,14 +8,14 @@ # - Bitrise CLI guides: http://devcenter.bitrise.io/bitrise-cli/ title: |- - Send Microsoft Teams message + Microsoft Teams Adaptive Card Integration summary: | Send Microsoft Teams message to a channel description: | - Send Microsoft Teams message to a channel -website: https://github.com/maguhiro/bitrise-step-send-microsoft-teams-message -source_code_url: https://github.com/maguhiro/bitrise-step-send-microsoft-teams-message -support_url: https://github.com/maguhiro/bitrise-step-send-microsoft-teams-message/issues + Send Microsoft Teams message as adaptive card +website: https://github.com/HUK-COBURG/bitrise-step-microsoft-teams-adaptive-card +source_code_url: https://github.com/HUK-COBURG/bitrise-step-microsoft-teams-adaptive-card +support_url: https://github.com/HUK-COBURG/bitrise-step-microsoft-teams-adaptive-card/issues host_os_tags: - osx-10.10 - ubuntu-16.04 @@ -28,7 +28,7 @@ is_skippable: true toolkit: go: - package_name: github.com/maguhiro/bitrise-step-send-microsoft-teams-message + package_name: github.com/HUK-COBURG/bitrise-step-microsoft-teams-adaptive-card inputs: @@ -50,20 +50,44 @@ inputs: is_required: true is_sensitive: true # Message Main Inputs - - theme_color: "10c289" + - card_headline: "Success" opts: - title: "Message card theme color" + title: "Adaptive Card Header Container text" description: | - Specifies a custom brand color for the card. - Can input any hex color code (eg. ff0000). - - [documentation of MS Teams](https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#card-fields). - - theme_color_on_error: "ff2158" + Text of the header container. + - card_headline_on_error: "Failed" + opts: + title: "Adaptive Card Header Container text" + description: | + Text of the header container. + category: If Build Failed + - card_style: "default" + opts: + title: "Adaptive Card Style" + description: | + Specifies the color of the header container. + For preview see: + [documentation of Adaptive Cards](https://adaptivecards.io/designer/). + value_options: + - "default" + - "good" + - "attention" + - "warning" + - "accent" + - "emphasis" + - card_style_on_error: "attention" opts: title: "Message card theme color if the build failed" description: | **This option will be used if the build failed.** category: If Build Failed + value_options: + - "default" + - "good" + - "attention" + - "warning" + - "accent" + - "emphasis" - title: "Build Succeeded!" opts: title: "Message card title" diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/.gitignore b/vendor/github.com/atc0005/go-teams-notify/v2/.gitignore new file mode 100644 index 0000000..f306348 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/.gitignore @@ -0,0 +1,30 @@ +# Copyright 2020 Enrico Hoffmann +# Copyright 2021 Adam Chalkley +# +# https://github.com/atc0005/go-teams-notify +# +# Licensed under the MIT License. See LICENSE file in the project root for +# full license information. + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# refs GH-6, GH-15 +# Allow vendored files to be included in this repo +#vendor + +# Ignore local scratch directory +/scratch + +# Ignore local Visual Studio Code settings +/.vscode diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/.golangci.yml b/vendor/github.com/atc0005/go-teams-notify/v2/.golangci.yml new file mode 100644 index 0000000..4bc9eaf --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/.golangci.yml @@ -0,0 +1,82 @@ +# Copyright 2020 Enrico Hoffmann +# Copyright 2021 Adam Chalkley +# +# https://github.com/atc0005/go-teams-notify +# +# Licensed under the MIT License. See LICENSE file in the project root for +# full license information. + +linters: + enable: + - dogsled + - dupl + - gocognit + - goconst + - gocritic + - gocyclo + - gofmt + - revive + - gosec + - nakedret + - prealloc + - exportloopref + - unconvert + - unparam + - whitespace + +linters-settings: + funlen: + lines: 60 + statements: 40 + + gocognit: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 10 + + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 15 + + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 2 + + unparam: + # Inspect exported functions, default is false. Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find external interfaces. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: true + + unused: + # treat code as a program (not a library) and report unused exported identifiers; default is false. + # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find funcs usages. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + + whitespace: + # Enforces newlines (or comments) after every multi-line if statement + multi-if: true + # Enforces newlines (or comments) after every multi-line function signature + multi-func: true + +issues: + # Not using default exclusions because we want to require comments on public + # functions and types. + exclude-use-default: false + +# options for analysis running +run: + # include test files or not, default is true + tests: false + + # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + modules-download-mode: vendor diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/.markdownlint.yml b/vendor/github.com/atc0005/go-teams-notify/v2/.markdownlint.yml new file mode 100644 index 0000000..cc002da --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/.markdownlint.yml @@ -0,0 +1,22 @@ +# Copyright 2021 Adam Chalkley +# +# https://github.com/atc0005/go-teams-notify +# +# Licensed under the MIT License. See LICENSE file in the project root for +# full license information. + +# https://github.com/igorshubovych/markdownlint-cli#configuration +# https://github.com/DavidAnson/markdownlint#optionsconfig + +# Setting the special default rule to true or false includes/excludes all +# rules by default. +"default": true + +# We know that line lengths will be long in the main README file, so don't +# report those cases. +"MD013": false + +# Don't complain if sub-heading names are duplicated since this is a common +# practice in CHANGELOG.md (e.g., "Fixed"). +"MD024": + "siblings_only": true diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/CHANGELOG.md b/vendor/github.com/atc0005/go-teams-notify/v2/CHANGELOG.md new file mode 100644 index 0000000..fd45b61 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/CHANGELOG.md @@ -0,0 +1,587 @@ +# Changelog + +## Overview + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a +Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +Please [open an issue](https://github.com/atc0005/go-teams-notify/issues) for any +deviations that you spot; I'm still learning!. + +## Types of changes + +The following types of changes will be recorded in this file: + +- `Added` for new features. +- `Changed` for changes in existing functionality. +- `Deprecated` for soon-to-be removed features. +- `Removed` for now removed features. +- `Fixed` for any bug fixes. +- `Security` in case of vulnerabilities. + +## [Unreleased] + +- placeholder + +## [v2.13.0] - 2024-09-08 + +### Added + +- (GH-293) Add MSTeams CodeBlock element + - credit: [@MichaelUrman](https://github.com/MichaelUrman) +- (GH-298) Update documentation for CodeBlock element + +## [v2.12.0] - 2024-08-16 + +### Added + +- (GH-291) Expose `TeamsMessage` interface to support mocking + +## [v2.11.0] - 2024-08-02 + +### Added + +- (GH-275) Add initial support for Workflow connectors + +### Changed + +#### Dependency Updates + +- (GH-259) Go Dependency: Bump github.com/stretchr/testify from 1.8.4 to 1.9.0 + +#### Other + +- (GH-272) Documentation refresh for O365 & Workflow connectors + +### Fixed + +- (GH-261) Remove inactive maligned linter +- (GH-274) Fix validation for `Action.Type` field +- (GH-283) Update CodeQL workflow to run on dev branch PRs + +## [v2.10.0] - 2024-02-22 + +### Added + +- (GH-255) Add `IsSublte` and `HorizontalAlignment` to `Element` + - credit: [@codello](https://github.com/codello) + +### Changed + +#### Dependency Updates + +- (GH-256) Update Dependabot PR prefixes + +## [v2.9.0] - 2024-01-25 + +### Added + +- (GH-241) Add proxy server examples +- (GH-251) Initial support for toggling visibility + +### Changed + +#### Dependency Updates + +- (GH-238) ghaw: bump actions/checkout from 3 to 4 +- (GH-248) ghaw: bump github/codeql-action from 2 to 3 +- (GH-236) Update Dependabot config to monitor both branches + +#### Other + +- (GH-244) Update Go Doc comment formatting + +## [v2.8.0] - 2023-07-21 + +### Added + +- `Adaptive Card` format + - (GH-205) Ability to create a Table in AdaptiveCard +- CI + - (GH-232) Add initial automated release notes config + - (GH-233) Add initial automated release build workflow + +### Changed + +- Dependencies + - `stretchr/testify` + - `v1.8.2` to `v1.8.4` +- CI + - (GH-226) Add `quick` Makefile recipe (alias) + - (GH-225) Update vuln analysis GHAW to remove on.push hook + - (GH-230) Disable unsupported build opts in monthly workflow + +### Fixed + +- CI + - (GH-229) Restore local CodeQL workflow + +## [v2.7.1] - 2023-06-09 + +### Changed + +- Dependencies + - `github.com/stretchr/testify` + - `v1.8.1` to `v1.8.2` +- CI + - (GH-198) Add Go Module Validation, Dependency Updates jobs + - (GH-200) Drop `Push Validation` workflow + - (GH-201) Rework workflow scheduling + - (GH-203) Remove `Push Validation` workflow status badge + - (GH-207) Update vuln analysis GHAW to use on.push hook +- `Adaptive Card` format + - (GH-206) Update `AdaptiveCardMaxVersion` to 1.5 + - (GH-216) Refactor `TopLevelCard.Validate` +- Other + - (GH-212) Update `InList`, `InListIfFieldValNotEmpty` validators + +### Fixed + +- (GH-208) Validation of `(adaptivecard.Attachment).Content` is missing + +## [v2.7.0] - 2022-12-12 + +### Added + +- (GH-134) Allow setting user agent, fallback to project-specific default + value +- (GH-135) Allow overriding default `http.Client` +- (GH-157) Add `Adaptive Card` message format support + - see also discussion from GH-127, including feedback from + [@ghokun](https://github.com/ghokun) +- (GH-169) Added YAML en(de)coding support to `MessageCard` + - credit: [@pcanilho](https://github.com/pcanilho) + +### Changed + +- Dependencies + - `github.com/stretchr/testify` + - `v1.7.0` to `v1.8.1` +- (GH-154) Deprecate API interface, expose underlying "Teams" client +- (GH-183) Update Makefile and GitHub Actions Workflows +- (GH-190) Refactor GitHub Actions workflows to import logic + +### Fixed + +- (GH-166) Update `lintinstall` Makefile recipe +- (GH-184) Apply Go 1.19 specific doc comments linting fixes +- (GH-176) `./send_test.go:238:8: second argument to errors.As should not be + *error` +- (GH-179) Wrong json key name for URL (uses uri instead) + - credit: [@janfonas](https://github.com/janfonas) + +## [v2.6.1] - 2022-02-25 + +### Changed + +- Dependencies + - `actions/setup-node` + - `v2.2.0` to `v3` + +- Linting + - (GH-131) Expand linting GitHub Actions Workflow to include `oldstable`, + `unstable` container images + - (GH-132) Switch Docker image source from Docker Hub to GitHub Container + Registry (GHCR) + +### Fixed + +- (GH-137) Missing doc comment for + `teamsClient.AddWebhookURLValidationPatterns()` +- (GH-138) Missing doc comment for `teamsClient.ValidateWebhook()` +- (GH-141) send.go:306:15: nilness: tautological condition: non-nil != nil + (govet) +- (GH-144) Incorrect field referenced in error message for + `MessageCardSection.AddFact()` + +## [v2.6.0] - 2021-07-09 + +### Added + +- Features + - Add support for PotentialActions (aka, "Actions") + - credit: [@nmaupu](https://github.com/nmaupu) + +- Documentation + - Add separate `examples` directory containing standalone example code for + most common use cases + +### Changed + +- Dependencies + - `actions/setup-node` + - `v2.1.5` to `v2.2.0` + - update `node-version` value to always use latest LTS version instead of + hard-coded version + +- Linting + - replace `golint`, `scopelint` linters, cleanup config + - note: this specifically applies to linting performed via Makefile + recipe, not (at present) the bulk of the CI linting checks + +- Documentation + - move examples from README to separate `examples` directory + - Remove example from doc.go file, direct reader to main README + - Update project status + - remove history as it is likely no longer relevant (original + project is discontinued at this point) + - remove future (for the same reason) + - Add explicit "Supported Releases" section to help make clear that + the v1 series is no longer maintained + - Remove explicit "used by" details, rely on dynamic listing provided + by pkg.go.dev instead + - Minor polish + +## [v2.5.0] - 2021-04-08 + +### Added + +- Features + - Validation of webhook URLs using custom validation patterns + - credit: [@nmaupu](https://github.com/nmaupu) + - Validation of `MessageCard` type using a custom validation function (to + override default validation behavior) + - credit: [@nmaupu](https://github.com/nmaupu) + +- Documentation + - Add list of projects using this library + - Update features list to include functionality added to this fork + - Configurable validation of webhook URLs + - Configurable validation of `MessageCard` type + - Configurable timeouts + - Configurable retry support + +### Changed + +- Dependencies + - `actions/setup-node` + - `v2.1.4` to `v2.1.5` + +### Fixed + +- Documentation + - Misc typos + - Grammatical tweaks + - Attempt to clarify project status + - i.e., not mothballed, just slow cadence + +## [v2.4.2] - 2021-01-28 + +### Changed + +- Apply regex pattern match for webhook URL validation instead of fixed + strings in order to support matching private org webhook URL subdomains + +### Fixed + +- Updating an exiting webhook connector in Microsoft Teams switches the URL to + unsupported `https://*.webhook.office.com/webhookb2/` format +- `SendWithRetry` method does not honor setting to disable webhook URL prefix + validation +- Support for disabling webhook URL validation limited to just disabling + validation of prefixes + +## [v2.4.1] - 2021-01-28 + +### Changed + +- (GH-59) Webhook URL API endpoint response validation now requires a `1` text + string as the response body + +### Fixed + +- (GH-59) Microsoft Teams Webhook Connector "200 OK" status insufficient + indication of success + +## [v2.4.0] - 2021-01-28 + +### Added + +- Add (optional) support for disabling webhook URL prefix validation + - credit: [@odise](https://github.com/odise) + +### Changed + +- Documentation + - Refresh "basic" example + - Add example for disabling webhook URL prefix validation + - Update "about this project" coverage + - Swap GoDoc badge for pkg.go.dev badge + +- Tests + - Extend test coverage + - Verbose test output by default (Makefile, GitHub Actions Workflow) + +- Dependencies + - `actions/setup-node` + - `v2.1.1` to `v2.1.4` + - `actions/checkout` + - `v2.3.2` to `v2.3.4` + - `stretchr/testify` + - `v1.6.1` to `v1.7.0` + +### Fixed + +- minor linting error for commented code +- Tests fail to assert that any errors which occur are expected, only the + types + +## [v2.3.0] - 2020-08-29 + +### Added + +- Add package-level logging for formatting functions + - as with other package-level logging, this is disabled by default + +- API + - add `SendWithRetry` method based on the `teams.SendMessage` function from + the `atc0005/send2teams` project + - actively working to move relevant content from that project to this one + +### Fixed + +- YYYY-MM-DD date formatting of changelog version entries + +## [v2.2.0] - 2020-08-28 + +### Added + +- Add package-level logger +- Extend API to allow request cancellation via context +- Add formatting functions useful for text conversion + - Convert Windows/Mac/Linux EOL to Markdown break statements + - used to provide equivalent Teams-compatible formatting + - Format text as code snippet + - this inserts leading and trailing ` character to provide Markdown string + formatting + - Format text as code block + - this inserts three leading and trailing ` characters to provide Markdown + code block formatting + - *`Try`* variants of code formatting functions + - return formatted string if no errors, otherwise return the original + string + +### Changed + +- Expose API response strings containing potential error messages +- README + - Explicitly note that this fork is now standalone until such time that the + upstream project resumes development/maintenance efforts + +### Fixed + +- CHANGELOG section link in previous release +- Invalid `RoundTripper` implementation used in `TestTeamsClientSend` test + function + - see `GH-46` and `GH-47`; thank you `@davecheney` for the fix! + +## [v2.1.1] - 2020-08-25 + +### Added + +- README + - Add badges for GitHub Actions Workflows + - Add release badge for latest project release +- Add CHANGELOG file +- Add GoDoc package-level documentation +- Extend webhook validation error handling +- Add Docker-based GitHub Actions Workflows +- Enable Dependabot updates +- Add Markdownlint config file + +### Changed + +- README + - Replace badge for latest tag with latest release + - Update GoDoc badge to reference this fork + - Update license badge to reference this fork + - Add new sections common to other projects that I maintain + - table of contents + - overview + - changelog + - references + - features +- Vendor dependencies +- Update license to add @atc0005 (new) in addition to @dasrick (existing) +- Update go.mod to replace upstream with this fork +- Rename golangci-lint config file to match officially supported name +- Remove files no longer used by this fork + - Travis CI configuration + - editorconfig file (and settings) +- Add license header to source files + - combined copyright statement for existing files + - single copyright statement for new files + +### Fixed + +- Add missing Facts assignment in MessageCardSection +- scopelint: Fix improper range loop var reference +- Fix misc linting issues with README +- Test failure from previous upstream pull request submissions + - `Object expected to be of type *url.Error, but was *errors.errorString` +- Misc linting issues with primary and test files + +## [v2.1.0] - 2020-04-08 + +### Added + +- `MessageCard` type includes additional fields + - `Type` and `Context` fields provide required JSON payload + fields + - preset to required static values via updated + `NewMessageCard()` constructor + - `Summary` + - required if `Text` field is not set, optional otherwise + - `Sections` slice + - `MessageCardSection` type + +- Additional nested types + - `MessageCardSection` + - `MessageCardSectionFact` + - `MessageCardSectionImage` + +- Additional methods for `MessageCard` and nested types + - `MessageCard.AddSection()` + - `MessageCardSection.AddFact()` + - `MessageCardSection.AddFactFromKeyValue()` + - `MessageCardSection.AddImage()` + - `MessageCardSection.AddHeroImageStr()` + - `MessageCardSection.AddHeroImage()` + +- Additional factory functions + - `NewMessageCardSection()` + - `NewMessageCardSectionFact()` + - `NewMessageCardSectionImage()` + +- `IsValidMessageCard()` added to check for minimum required + field values. + - This function has the potential to be extended + later with additional validation steps. + +- Wrapper `IsValidInput()` added to handle all validation + needs from one location. + - the intent was to both solve a CI error and provide + a location to easily extend validation checks in + the future (if needed) + +### Changed + +- `MessageCard` type includes additional fields +- `NewMessageCard` factory function sets fields needed for + required JSON payload fields + - `Type` + - `Context` + +- `teamsClient.Send()` method updated to apply `MessageCard` struct + validation alongside existing webhook URL validation + +- `isValidWebhookURL()` exported as `IsValidWebhookURL()` so that client + code can use the validation functionality instead of repeating the + code + - e.g., flag value validation for "fail early" behavior + +### Known Issues + +- No support in this set of changes for `potentialAction` types + - `ViewAction` + - `OpenUri` + - `HttpPOST` + - `ActionCard` + - `InvokeAddInCommand` + - Outlook specific based on what I read; likely not included + in a future release due to non-Teams specific usage + +## [v2.0.0] - 2020-03-29 + +### Breaking + +- `NewClient()` will NOT return multiple values +- remove provided mock + +### Changed + +- switch dependency/package management tool to from `dep` to `go mod` +- switch from `golint` to `golangci-lint` +- add more golang versions to pass via travis-ci + +## [v1.3.1] - 2020-03-29 + +### Fixed + +- fix redundant error logging +- fix redundant comment + +## [v1.3.0] - 2020-03-26 + +### Changed + +- feature: allow multiple valid webhook URL FQDNs (thx @atc0005) + +## [v1.2.0] - 2019-11-08 + +### Added + +- add mock + +### Changed + +- update deps +- `gosimple` (shorten two conditions) + +## [v1.1.1] - 2019-05-02 + +### Changed + +- rename client interface into API +- update deps + +### Fixed + +- fix typo in README + +## [v1.1.0] - 2019-04-30 + +### Added + +- add missing tests +- append documentation + +### Changed + +- add/change to client/interface + +## [v1.0.0] - 2019-04-29 + +### Added + +- add initial functionality of sending messages to MS Teams channel + +[Unreleased]: https://github.com/atc0005/go-teams-notify/compare/v2.13.0...HEAD +[v2.13.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.13.0 +[v2.12.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.12.0 +[v2.11.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.11.0 +[v2.10.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.10.0 +[v2.9.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.9.0 +[v2.8.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.8.0 +[v2.7.1]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.7.1 +[v2.7.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.7.0 +[v2.6.1]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.6.1 +[v2.6.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.6.0 +[v2.5.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.5.0 +[v2.4.2]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.4.2 +[v2.4.1]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.4.1 +[v2.4.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.4.0 +[v2.3.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.3.0 +[v2.2.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.2.0 +[v2.1.1]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.1.1 +[v2.1.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.1.0 +[v2.0.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v2.0.0 +[v1.3.1]: https://github.com/atc0005/go-teams-notify/releases/tag/v1.3.1 +[v1.3.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v1.3.0 +[v1.2.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v1.2.0 +[v1.1.1]: https://github.com/atc0005/go-teams-notify/releases/tag/v1.1.1 +[v1.1.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v1.1.0 +[v1.0.0]: https://github.com/atc0005/go-teams-notify/releases/tag/v1.0.0 diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/LICENSE b/vendor/github.com/atc0005/go-teams-notify/v2/LICENSE new file mode 100644 index 0000000..4d6d7cc --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020 Enrico Hoffmann +Copyright (c) 2020-Present Adam Chalkley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/Makefile b/vendor/github.com/atc0005/go-teams-notify/v2/Makefile new file mode 100644 index 0000000..66e5d3f --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/Makefile @@ -0,0 +1,116 @@ +# Copyright 2020 Enrico Hoffmann +# Copyright 2021 Adam Chalkley +# +# https://github.com/atc0005/go-teams-notify +# +# Licensed under the MIT License. See LICENSE file in the project root for +# full license information. + +# REFERENCES +# +# https://github.com/golangci/golangci-lint#install +# https://github.com/golangci/golangci-lint/releases/latest + +SHELL = /bin/bash + +BUILDCMD = go build -mod=vendor ./... +GOCLEANCMD = go clean -mod=vendor ./... +GITCLEANCMD = git clean -xfd +CHECKSUMCMD = sha256sum -b + +.DEFAULT_GOAL := help + + ########################################################################## + # Targets will not work properly if a file with the same name is ever + # created in this directory. We explicitly declare our targets to be phony + # by making them a prerequisite of the special target .PHONY + ########################################################################## + +.PHONY: help +## help: prints this help message +help: + @echo "Usage:" + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +.PHONY: lintinstall +## lintinstall: install common linting tools +# https://github.com/golang/go/issues/30515#issuecomment-582044819 +lintinstall: + @echo "Installing linting tools" + + @export PATH="${PATH}:$(go env GOPATH)/bin" + + @echo "Installing latest stable staticcheck version via go install command ..." + go install honnef.co/go/tools/cmd/staticcheck@latest + staticcheck --version + + @echo Installing latest stable golangci-lint version per official installation script ... + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin + golangci-lint --version + + @echo "Finished updating linting tools" + +.PHONY: linting +## linting: runs common linting checks +linting: + @echo "Running linting tools ..." + + @echo "Running go vet ..." + @go vet -mod=vendor $(shell go list -mod=vendor ./... | grep -v /vendor/) + + @echo "Running golangci-lint ..." + @golangci-lint --version + @golangci-lint run + + @echo "Running staticcheck ..." + @staticcheck --version + @staticcheck $(shell go list -mod=vendor ./... | grep -v /vendor/) + + @echo "Finished running linting checks" + +.PHONY: gotests +## gotests: runs go test recursively, verbosely +gotests: + @echo "Running go tests ..." + @go test -v -mod=vendor ./... + @echo "Finished running go tests" + +.PHONY: goclean +## goclean: removes local build artifacts, temporary files, etc +goclean: + @echo "Removing object files and cached files ..." + @$(GOCLEANCMD) + +.PHONY: clean +## clean: alias for goclean +clean: goclean + +.PHONY: gitclean +## gitclean: WARNING - recursively cleans working tree by removing non-versioned files +gitclean: + @echo "Removing non-versioned files ..." + @$(GITCLEANCMD) + +.PHONY: pristine +## pristine: run goclean and gitclean to remove local changes +pristine: goclean gitclean + +.PHONY: all +# https://stackoverflow.com/questions/3267145/makefile-execute-another-target +## all: run all applicable build steps +all: clean build + @echo "Completed build process ..." + +.PHONY: quick +## quick: alias for build recipe +quick: clean build + @echo "Completed tasks for quick build" + +.PHONY: build +## build: ensure that packages build +build: + @echo "Building packages ..." + + $(BUILDCMD) + + @echo "Completed build tasks" diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/README.md b/vendor/github.com/atc0005/go-teams-notify/v2/README.md new file mode 100644 index 0000000..1766fab --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/README.md @@ -0,0 +1,525 @@ + +# goteamsnotify + +A package to send messages to a Microsoft Teams channel. + +[![Latest release][githubtag-image]][githubtag-url] +[![Go Reference][goref-image]][goref-url] +[![License][license-image]][license-url] +[![go.mod Go version](https://img.shields.io/github/go-mod/go-version/atc0005/go-teams-notify)](https://github.com/atc0005/go-teams-notify) +[![Lint and Build](https://github.com/atc0005/go-teams-notify/actions/workflows/lint-and-build.yml/badge.svg)](https://github.com/atc0005/go-teams-notify/actions/workflows/lint-and-build.yml) +[![Project Analysis](https://github.com/atc0005/go-teams-notify/actions/workflows/project-analysis.yml/badge.svg)](https://github.com/atc0005/go-teams-notify/actions/workflows/project-analysis.yml) + + +## Table of contents + +- [Project home](#project-home) +- [Overview](#overview) +- [Features](#features) +- [Project Status](#project-status) +- [Supported Releases](#supported-releases) + - [Plans: v2](#plans-v2) + - [Plans: v3](#plans-v3) +- [Changelog](#changelog) +- [Usage](#usage) + - [Add this project as a dependency](#add-this-project-as-a-dependency) + - [Setup a connection to Microsoft Teams](#setup-a-connection-to-microsoft-teams) + - [Overview](#overview-1) + - [Workflow connectors](#workflow-connectors) + - [Workflow webhook URL format](#workflow-webhook-url-format) + - [How to create a Workflow connector webhook URL](#how-to-create-a-workflow-connector-webhook-url) + - [Using Teams client Workflows context option](#using-teams-client-workflows-context-option) + - [Using Teams client app](#using-teams-client-app) + - [Using Power Automate web UI](#using-power-automate-web-ui) + - [O365 connectors](#o365-connectors) + - [O365 webhook URL format](#o365-webhook-url-format) + - [How to create an O365 connector webhook URL](#how-to-create-an-o365-connector-webhook-url) + - [Examples](#examples) + - [Basic](#basic) + - [Specify proxy server](#specify-proxy-server) + - [User Mention](#user-mention) + - [CodeBlock](#codeblock) + - [Tables](#tables) + - [Set custom user agent](#set-custom-user-agent) + - [Add an Action](#add-an-action) + - [Toggle visibility](#toggle-visibility) + - [Disable webhook URL prefix validation](#disable-webhook-url-prefix-validation) + - [Enable custom patterns' validation](#enable-custom-patterns-validation) +- [Used by](#used-by) +- [References](#references) + +## Project home + +See [our GitHub repo](https://github.com/atc0005/go-teams-notify) for the +latest code, to file an issue or submit improvements for review and potential +inclusion into the project. + +## Overview + +The `goteamsnotify` package (aka, `go-teams-notify`) allows sending messages +to a Microsoft Teams channel. These messages can be composed of +[🚫 deprecated][o365-connector-retirement-announcement] legacy +[`MessageCard`][msgcard-ref] or [`Adaptive Card`][adaptivecard-ref] card +formats. + +Simple messages can be created by specifying only a title and a text body. +More complex messages may be composed of multiple sections ([🚫 +deprecated][o365-connector-retirement-announcement] `MessageCard`) or +containers (`Adaptive Card`), key/value pairs (aka, `Facts`) and externally +hosted images. See the [Features](#features) list for more information. + +**NOTE**: `Adaptive Card` support is currently limited. The goal is to expand +this support in future releases to include additional features supported by +Microsoft Teams. + +## Features + +- Submit simple or complex messages to Microsoft Teams + - simple messages consist of only a title and a text body (one or more + strings) + - complex messages may consist of multiple sections ([🚫 + deprecated][o365-connector-retirement-announcement] `MessageCard`), + containers (`Adaptive Card`) key/value pairs (aka, `Facts`) and externally + hosted images +- Support for Actions, allowing users to take quick actions within Microsoft + Teams + - [🚫 deprecated][o365-connector-retirement-announcement] [`MessageCard` `Actions`][msgcard-ref-actions] + - [`Adaptive Card` `Actions`][adaptivecard-ref-actions] +- Support for [user mentions][adaptivecard-user-mentions] (`Adaptive + Card` format) +- Configurable validation of webhook URLs + - enabled by default, attempts to match most common known webhook URL + patterns + - option to disable validation entirely + - option to use custom validation patterns +- Configurable validation of [🚫 + deprecated][o365-connector-retirement-announcement] `MessageCard` type + - default assertion that bare-minimum required fields are present + - support for providing a custom validation function to override default + validation behavior +- Configurable validation of `Adaptive Card` type + - default assertion that bare-minimum required fields are present + - support for providing a custom validation function to override default + validation behavior +- Configurable timeouts +- Configurable retry support + +## Project Status + +In short: + +- The upstream project is no longer being actively developed or maintained. +- This fork is now a standalone project, accepting contributions, bug reports + and feature requests. + - see [Supported Releases](#supported-releases) for details +- Others have also taken an interest in [maintaining their own + forks](https://github.com/atc0005/go-teams-notify/network/members) of the + original project. See those forks for other ideas/changes that you may find + useful. + +For more details, see the +[Releases](https://github.com/atc0005/go-teams-notify/releases) section or our +[Changelog](https://github.com/atc0005/go-teams-notify/blob/master/CHANGELOG.md). + +## Supported Releases + +| Series | Example | Status | +| -------- | ---------------- | --------------------------------------- | +| `v1.x.x` | `v1.3.1` | Not Supported (EOL) | +| `v2.x.x` | `v2.6.0` | Supported (until approximately 2026-01) | +| `v3.x.x` | `v3.0.0-alpha.1` | Planning (target 2026-01) | +| `v4.x.x` | `v4.0.0-alpha.1` | TBD | + +### Plans: v2 + +| Task | Start Date / Version | Status | +| ------------------------------------------------------------ | -------------------- | -------- | +| support the v2 branch with bugfixes and minor changes | 2020-03-29 (v2.0.0) | Ongoing | +| add support & documentation for Power Automate workflow URLs | v2.11.0-alpha.1 | Complete | + +### Plans: v3 + +Early January 2026: + +- Microsoft [drops support for O365 + connectors][o365-connector-retirement-announcement] in December 2025 +- we release a v3 branch + - drop support for the [🚫 +deprecated][o365-connector-retirement-announcement] O365 connectors + - drop support for the [🚫 +deprecated][o365-connector-retirement-announcement] `MessageCard`) format +- we drop support for the v2 branch + - the focus would be on maintaining the v3 branch with bugfixes and minor + changes + +> [!NOTE] +> +> While the plan for the upcoming v3 series includes dropping support for the +[🚫 deprecated][o365-connector-retirement-announcement] `MessageCard` format +and O365 connectors, the focus would not be on refactoring the overall code +structure; many of the rough edges currently present in the API would remain +in the v3 series and await a more focused cleanup effort made in preparation +for a future v4 series. + +## Changelog + +See the [`CHANGELOG.md`](CHANGELOG.md) file for the changes associated with +each release of this application. Changes that have been merged to `master`, +but not yet an official release may also be noted in the file under the +`Unreleased` section. A helpful link to the Git commit history since the last +official release is also provided for further review. + +## Usage + +### Add this project as a dependency + +See the [Examples](#examples) section for more details. + +### Setup a connection to Microsoft Teams + +#### Overview + +> [!WARNING] +> +> Microsoft announced July 3rd, 2024 that Office 365 (O365) connectors within +Microsoft Teams would be [retired in 3 +months][o365-connector-retirement-announcement] and replaced by Power Automate +workflows (or just "Workflows" for short). + +Quoting from the microsoft365dev blog: + +> We will gradually roll out this change in waves: +> +> - Wave 1 - effective August 15th, 2024: All new Connector creation will be +> blocked within all clouds +> - Wave 2 - effective October 1st, 2024: All connectors within all clouds +> will stop working + +[Microsoft later changed some of the +details][o365-connector-retirement-announcement] regarding the retirement +timeline of O365 connectors: + +> Update 07/23/2024: We understand and appreciate the feedback that customers +> have shared with us regarding the timeline provided for the migration from +> Office 365 connectors. We have extended the retirement timeline through +> December 2025 to provide ample time to migrate to another solution such as +> Power Automate, an app within Microsoft Teams, or Microsoft Graph. Please +> see below for more information about the extension: +> +> - All existing connectors within all clouds will continue to work until +> December 2025, however using connectors beyond December 31, 2024 will +> require additional action. +> - Connector owners will be required to update the respective URL to post +> by December 31st, 2024. At least 90 days prior to the December 31, 2024 +> deadline, we will send further guidance about making this URL update. If +> the URL is not updated by December 31, 2024 the connector will stop +> working. This is due to further service hardening updates being +> implemented for Office 365 connectors in alignment with Microsoft’s +> [Secure Future +> Initiative](https://blogs.microsoft.com/blog/2024/05/03/prioritizing-security-above-all-else/) +> - Starting August 15th, 2024 all new creations should be created using the +> Workflows app in Microsoft Teams + +Since O365 connectors will likely persist in many environments until the very +end of the deprecation period this project will [continue to support +them](#supported-releases) until then alongside Power Automate workflows. + +#### Workflow connectors + +##### Workflow webhook URL format + +Valid Power Automate Workflow URLs used to submit messages to Microsoft Teams +use this format: + +- `https://*.logic.azure.com:443/workflows/GUID_HERE/triggers/manual/paths/invoke?api-version=YYYY-MM-DD&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=SIGNATURE_HERE` + +Example URL from the LinkedIn [Bring Microsoft Teams incoming webhook security to +the next level with Azure Logic App][linkedin-teams-webhook-security-article] +article: + +- `https://webhook-jenkins.azure-api.net/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=f2QjZY50uoRnX6PIpyPT3xk` + +##### How to create a Workflow connector webhook URL + +> [!TIP] +> +> Use a dedicated "service" account not tied to a specific team member to help +ensure that the Workflow connector is long lived. + +The [initial O365 retirement blog +post][o365-connector-retirement-announcement] provides a list of templates +which guide you through the process of creating a Power Automate Workflow +webhook URL. + +###### Using Teams client Workflows context option + +1. Navigate to a channel or chat +1. Select the ellipsis on the channel or chat +1. Select `Workflows` +1. Type `when a webhook request` +1. Select the appropriate template + - `Post to a channel when a webhook request is received` + - `Post to a chat when a webhook request is received` +1. Verify that `Microsoft Teams` is successfully enabled +1. Select `Next` +1. Select an appropriate value from the `Microsoft Teams Team` drop-down list. +1. Select an appropriate `Microsoft Teams Channel` drop-down list. +1. Select `Create flow` +1. Copy the new workflow URL +1. Select `Done` + +###### Using Teams client app + +1. Open `Workflows` application in teams +1. Select `Create` across the top of the UI +1. Choose `Notifications` at the left +1. Select `Post to a channel when a webhook request is received` +1. Verify that `Microsoft Teams` is successfully enabled +1. Select `Next` +1. Select an appropriate value from the `Microsoft Teams Team` drop-down list. +1. Select an appropriate `Microsoft Teams Channel` drop-down list. +1. Select `Create flow` +1. Copy the new workflow URL +1. Select `Done` + +###### Using Power Automate web UI + +[This][workflow-channel-post-from-webhook-request] template walks you through +the steps of creating a new Workflow using the + web UI: + +1. Select or create a new connection (e.g., ) to Microsoft + Teams +1. Select `Create` +1. Select an appropriate value from the `Microsoft Teams Team` drop-down list. +1. Select an appropriate `Microsoft Teams Channel` drop-down list. +1. Select `Create` +1. If prompted, read the info message (e.g., "Your flow is ready to go") and + dismiss it. +1. Select `Edit` from the menu across the top + - alternatively, select `My flows` from the side menu, then select `Edit` + from the "More commands" ellipsis +1. Select `When a Teams webhook request is received` (e.g., left click) +1. Copy the `HTTP POST URL` value + - this is your *private* custom Workflow connector URL + - by default anyone can `POST` a request to this Workflow connector URL + - while this access setting can be changed it will prevent this library + from being used to submit webhook requests + +#### O365 connectors + +##### O365 webhook URL format + +> [!WARNING] +> +> O365 connector webhook URLs are deprecated and [scheduled to be +retired][o365-connector-retirement-announcement] on 2024-10-01. + +Valid (***deprecated***) O365 webhook URLs for Microsoft Teams use one of several +(confirmed) FQDNs patterns: + +- `outlook.office.com` +- `outlook.office365.com` +- `*.webhook.office.com` + - e.g., `example.webhook.office.com` + +Using an O365 webhook URL with any of these FQDN patterns appears to give +identical results. + +Here are complete, equivalent example webhook URLs from Microsoft's +documentation using the FQDNs above: + +- +- +- + - note the `webhookb2` sub-URI specific to this FQDN pattern + +All of these patterns when provided to this library should pass the default +validation applied. See the example further down for the option of disabling +webhook URL validation entirely. + +##### How to create an O365 connector webhook URL + +> [!WARNING] +> +> O365 connector webhook URLs are deprecated and [scheduled to be +retired][o365-connector-retirement-announcement] on 2024-10-01. + +1. Open Microsoft Teams +1. Navigate to the channel where you wish to receive incoming messages from + this application +1. Select `β‹―` next to the channel name and then choose Connectors. +1. Scroll through the list of Connectors to Incoming Webhook, and choose Add. +1. Enter a name for the webhook, upload an image to associate with data from + the webhook, and choose Create. +1. Copy the webhook URL to the clipboard and save it. You'll need the webhook + URL for sending information to Microsoft Teams. + - NOTE: While you can create another easily enough, you should treat this + webhook URL as sensitive information as anyone with this unique URL is + able to send messages (without authentication) into the associated + channel. +1. Choose Done. + +Credit: +[docs.microsoft.com](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#setting-up-a-custom-incoming-webhook), +[gist comment from +shadabacc3934](https://gist.github.com/chusiang/895f6406fbf9285c58ad0a3ace13d025#gistcomment-3562501) + +### Examples + +#### Basic + +This is an example of a simple client application which uses this library. + +- `Adaptive Card` + - File: [basic](./examples/adaptivecard/basic/main.go) +- [🚫 deprecated][o365-connector-retirement-announcement] `MessageCard` + - File: [basic](./examples/messagecard/basic/main.go) + +#### Specify proxy server + +This is an example of a simple client application which uses this library to +route a generated message through a specified proxy server. + +- `Adaptive Card` + - File: [basic](./examples/adaptivecard/proxy/main.go) +- [🚫 deprecated][o365-connector-retirement-announcement] `MessageCard` + - File: [basic](./examples/messagecard/proxy/main.go) + +#### User Mention + +These examples illustrates the use of one or more user mentions. This feature +is not available in the legacy [🚫 +deprecated][o365-connector-retirement-announcement] `MessageCard` card format. + +- File: [user-mention-single](./examples/adaptivecard/user-mention-single/main.go) +- File: [user-mention-multiple](./examples/adaptivecard/user-mention-multiple/main.go) +- File: [user-mention-verbose](./examples/adaptivecard/user-mention-verbose/main.go) + - this example does not necessarily reflect an optimal implementation + +#### CodeBlock + +This example illustrates the use of a [`CodeBlock`][adaptivecard-codeblock]. +This feature is not available in the legacy [🚫 +deprecated][o365-connector-retirement-announcement] `MessageCard` card format. + +- File: [codeblock](./examples/adaptivecard/codeblock/main.go) + +#### Tables + +These examples illustrates the use of a [`Table`][adaptivecard-table]. This +feature is not available in the legacy [🚫 +deprecated][o365-connector-retirement-announcement] `MessageCard` card format. + +- File: [table-manually-created](./examples/adaptivecard/table-manually-created/main.go) +- File: [table-unordered-grid](./examples/adaptivecard/table-unordered-grid/main.go) +- File: [table-with-headers](./examples/adaptivecard/table-with-headers/main.go) + +#### Set custom user agent + +This example illustrates setting a custom user agent. + +- `Adaptive Card` + - File: [custom-user-agent](./examples/adaptivecard/custom-user-agent/main.go) +- [🚫 deprecated][o365-connector-retirement-announcement] `MessageCard` + - File: [custom-user-agent](./examples/messagecard/custom-user-agent/main.go) + +#### Add an Action + +This example illustrates adding an [`OpenUri`][msgcard-ref-actions] ([🚫 +deprecated][o365-connector-retirement-announcement] `MessageCard`) or +[`OpenUrl`][adaptivecard-ref-actions] Action. When used, this action triggers +opening a URL in a separate browser or application. + +- `Adaptive Card` + - File: [actions](./examples/adaptivecard/actions/main.go) +- [🚫 deprecated][o365-connector-retirement-announcement] `MessageCard` + - File: [actions](./examples/messagecard/actions/main.go) + +#### Toggle visibility + +These examples illustrates using +[`ToggleVisibility`][adaptivecard-ref-actions] Actions to control the +visibility of various Elements of an `Adaptive Card` message. + +- File: [toggle-visibility-single-button](./examples/adaptivecard/toggle-visibility-single-button/main.go) +- File: [toggle-visibility-multiple-buttons](./examples/adaptivecard/toggle-visibility-multiple-buttons/main.go) +- File: [toggle-visibility-column-action](./examples/adaptivecard/toggle-visibility-column-action/main.go) +- File: [toggle-visibility-container-action](./examples/adaptivecard/toggle-visibility-container-action/main.go) + +#### Disable webhook URL prefix validation + +This example disables the validation webhook URLs, including the validation of +known prefixes so that custom/private webhook URL endpoints can be used (e.g., +testing purposes). + +- `Adaptive Card` + - File: [disable-validation](./examples/adaptivecard/disable-validation/main.go) +- [🚫 deprecated][o365-connector-retirement-announcement] `MessageCard` + - File: [disable-validation](./examples/messagecard/disable-validation/main.go) + +#### Enable custom patterns' validation + +This example demonstrates how to enable custom validation patterns for webhook +URLs. + +- `Adaptive Card` + - File: [custom-validation](./examples/adaptivecard/custom-validation/main.go) +- [🚫 deprecated][o365-connector-retirement-announcement] `MessageCard` + - File: [custom-validation](./examples/messagecard/custom-validation/main.go) + +## Used by + +See the Known importers lists below for a dynamically updated list of projects +using either this library or the original project. + +- [this fork](https://pkg.go.dev/github.com/atc0005/go-teams-notify/v2?tab=importedby) +- [original project](https://pkg.go.dev/github.com/dasrick/go-teams-notify/v2?tab=importedby) + +## References + +- [Original project](https://github.com/dasrick/go-teams-notify) +- [Forks of original project](https://github.com/atc0005/go-teams-notify/network/members) + + +- Microsoft Teams + - Adaptive Cards + ([de-de](https://docs.microsoft.com/de-de/outlook/actionable-messages/adaptive-card), + [en-us](https://docs.microsoft.com/en-us/outlook/actionable-messages/adaptive-card)) + - O365 connectors + - [Send via connectors](https://docs.microsoft.com/en-us/outlook/actionable-messages/send-via-connectors)) + - [Create Incoming Webhooks](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook) + - [adaptivecards.io](https://adaptivecards.io/designer) + - [Legacy actionable message card reference][msgcard-ref] + - Workflow connectors + - [Creating a workflow from a chat in Teams](https://support.microsoft.com/en-us/office/creating-a-workflow-from-a-channel-in-teams-242eb8f2-f328-45be-b81f-9817b51a5f0e) + - [Creating a workflow from a channel in Teams](https://support.microsoft.com/en-us/office/creating-a-workflow-from-a-chat-in-teams-e3b51c4f-49de-40aa-a6e7-bcff96b99edc) + + + +[o365-connector-retirement-announcement]: "Retirement of Office 365 connectors within Microsoft Teams" +[workflow-channel-post-from-webhook-request]: "Post to a channel when a webhook request is received" +[linkedin-teams-webhook-security-article]: "Bring Microsoft Teams incoming webhook security to the next level with Azure Logic App" + +[githubtag-image]: https://img.shields.io/github/release/atc0005/go-teams-notify.svg?style=flat +[githubtag-url]: https://github.com/atc0005/go-teams-notify + +[goref-image]: https://pkg.go.dev/badge/github.com/atc0005/go-teams-notify/v2.svg +[goref-url]: https://pkg.go.dev/github.com/atc0005/go-teams-notify/v2 + +[license-image]: https://img.shields.io/github/license/atc0005/go-teams-notify.svg?style=flat +[license-url]: https://github.com/atc0005/go-teams-notify/blob/master/LICENSE + +[msgcard-ref]: +[msgcard-ref-actions]: + +[adaptivecard-ref]: +[adaptivecard-ref-actions]: +[adaptivecard-user-mentions]: +[adaptivecard-table]: + +[adaptivecard-codeblock]: + + diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/adaptivecard.go b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/adaptivecard.go new file mode 100644 index 0000000..fd36eb1 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/adaptivecard.go @@ -0,0 +1,3428 @@ +// Copyright 2022 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package adaptivecard + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "regexp" + "strconv" + "strings" + + goteamsnotify "github.com/atc0005/go-teams-notify/v2" + "github.com/atc0005/go-teams-notify/v2/internal/validator" +) + +// General constants. +const ( + // TypeMessage is the type for an Adaptive Card Message. + TypeMessage string = "message" + + // PixelSizeRegex is a regular expression pattern intended to match + // specific pixel size (height, width) values such as "50px". + PixelSizeRegex string = "^[0-9]+px$" + + // PixelSizeExample is an example of a valid pixel size (height, width) + // value. + PixelSizeExample string = "50px" +) + +// Card & TopLevelCard specific constants. +const ( + // TypeAdaptiveCard is the supported type value for an Adaptive Card. + TypeAdaptiveCard string = "AdaptiveCard" + + // AdaptiveCardSchema represents the URI of the Adaptive Card schema. + AdaptiveCardSchema string = "http://adaptivecards.io/schemas/adaptive-card.json" + + // AdaptiveCardMaxVersion represents the highest supported version of the + // Adaptive Card schema supported in Microsoft Teams messages. + // + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#support-for-adaptive-cards + // https://adaptivecards.io/designer + // + // NOTE: Documented as 1.5 (adaptivecards.io/designer), but in practice > + // 1.4 is rejected for Power Automate workflow connectors. + // + // Setting to 1.4 works both for legacy O365 connectors and Workflow + // connectors. + AdaptiveCardMaxVersion float64 = 1.4 + AdaptiveCardMinVersion float64 = 1.0 + AdaptiveCardVersionTmpl string = "%0.1f" +) + +// Mention constants. +const ( + // TypeMention is the type for a user mention for a Adaptive Card Message. + TypeMention string = "mention" + + // MentionTextFormatTemplate is the expected format of the Mention.Text + // field value. + MentionTextFormatTemplate string = "%s" + + // defaultMentionTextSeparator is the default separator used between the + // contents of the Mention.Text field and a TextBlock.Text field. + defaultMentionTextSeparator string = " " +) + +// Attachment constants. +// +// - https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference +// - https://docs.microsoft.com/en-us/dotnet/api/microsoft.bot.schema.attachmentlayouttypes +// - https://docs.microsoft.com/en-us/javascript/api/botframework-schema/attachmentlayouttypes +// - https://github.com/matthidinger/ContosoScubaBot/blob/master/Cards/1-Schools.JSON +const ( + + // AttachmentContentType is the supported type value for an attached + // Adaptive Card for a Microsoft Teams message. + AttachmentContentType string = "application/vnd.microsoft.card.adaptive" + + AttachmentLayoutList string = "list" + AttachmentLayoutCarousel string = "carousel" +) + +// TextBlock specific constants. +// https://adaptivecards.io/explorer/TextBlock.html +const ( + // TextBlockStyleDefault indicates that the TextBlock uses the default + // style which provides no special styling or behavior. + TextBlockStyleDefault string = "default" + + // TextBlockStyleHeading indicates that the TextBlock is a heading. This + // will apply the heading styling defaults and mark the text block as a + // heading for accessibility. + TextBlockStyleHeading string = "heading" +) + +// Column specific constants. +// https://adaptivecards.io/explorer/Column.html +const ( + // TypeColumn is the type for an Adaptive Card Column. + TypeColumn string = "Column" + + // ColumnWidthAuto indicates that a column's width should be determined + // automatically based on other columns in the column group. + ColumnWidthAuto string = "auto" + + // ColumnWidthStretch indicates that a column's width should be stretched + // to fill the enclosing column group. + ColumnWidthStretch string = "stretch" +) + +// Table specific constants. +// +// https://adaptivecards.io/explorer/Table.html +// https://adaptivecards.io/explorer/TableCell.html +const ( + + // NOTE: Table is not a type, it is an Card Element + // TypeTable string = "Table" + + TypeTableColumnDefinition string = "TableColumnDefinition" + TypeTableRow string = "TableRow" + TypeTableCell string = "TableCell" +) + +// Text size for TextBlock or TextRun elements. +const ( + SizeSmall string = "small" + SizeDefault string = "default" + SizeMedium string = "medium" + SizeLarge string = "large" + SizeExtraLarge string = "extraLarge" +) + +// Text weight for TextBlock or TextRun elements. +const ( + WeightBolder string = "bolder" + WeightLighter string = "lighter" + WeightDefault string = "default" +) + +// Supported colors for TextBlock or TextRun elements. +const ( + ColorDefault string = "default" + ColorDark string = "dark" + ColorLight string = "light" + ColorAccent string = "accent" + ColorGood string = "good" + ColorWarning string = "warning" + ColorAttention string = "attention" +) + +// Image specific constants. +// https://adaptivecards.io/explorer/Image.html +const ( + ImageStyleDefault string = "" + ImageStylePerson string = "" +) + +// ChoiceInput specific constants. +const ( + ChoiceInputStyleCompact string = "compact" + ChoiceInputStyleExpanded string = "expanded" + ChoiceInputStyleFiltered string = "filtered" // Introduced in version 1.5 +) + +// TextInput specific constants. +const ( + TextInputStyleText string = "text" + TextInputStyleTel string = "tel" + TextInputStyleURL string = "url" + TextInputStyleEmail string = "email" + TextInputStylePassword string = "password" // Introduced in version 1.5 +) + +// Container specific constants. +const ( + ContainerStyleDefault string = "default" + ContainerStyleEmphasis string = "emphasis" + ContainerStyleGood string = "good" + ContainerStyleAttention string = "attention" + ContainerStyleWarning string = "warning" + ContainerStyleAccent string = "accent" +) + +// Supported spacing values for FactSet, Container and other container element +// types. +const ( + SpacingDefault string = "default" + SpacingNone string = "none" + SpacingSmall string = "small" + SpacingMedium string = "medium" + SpacingLarge string = "large" + SpacingExtraLarge string = "extraLarge" + SpacingPadding string = "padding" +) + +// Supported Horizontal alignment values for (supported) container and text +// types. +const ( + HorizontalAlignmentLeft string = "left" + HorizontalAlignmentCenter string = "center" + HorizontalAlignmentRight string = "right" +) + +// Supported Horizontal alignment values for (supported) container types. +const ( + VerticalAlignmentTop string = "top" + VerticalAlignmentCenter string = "center" + VerticalAlignmentBottom string = "bottom" +) + +// Supported width values for the msteams property used in in Adaptive Card +// messages sent via Microsoft Teams. +const ( + MSTeamsWidthFull string = "Full" +) + +// Supported Actions +const ( + + // TeamsActionsDisplayLimit is the observed limit on the number of visible + // URL "buttons" in a Microsoft Teams message. + // + // Unlike the MessageCard format which has a clearly documented limit of 4 + // actions, testing reveals that Desktop / Web displays 6 without the + // option to expand and see any additional defined actions. Mobile + // displays 6 with an ellipsis to expand into a list of other Actions. + // + // This results in a maximum limit of 6 actions in the Actions array for a + // Card. + // + // A workaround is to create multiple ActionSet elements and limit the + // number of Actions in each set ot 6. + // + // https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#actions + TeamsActionsDisplayLimit int = 6 + + // TypeActionExecute is an action that gathers input fields, merges with + // optional data field, and sends an event to the client. Clients process + // the event by sending an Invoke activity of type adaptiveCard/action to + // the target Bot. The inputs that are gathered are those on the current + // card, and in the case of a show card those on any parent cards. See + // Universal Action Model documentation for more details: + // https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model + // + // TypeActionExecute was introduced in Adaptive Cards schema version 1.4. + // TypeActionExecute actions may not render with earlier versions of the + // Teams client. + TypeActionExecute string = "Action.Execute" + + // ActionExecuteMinCardVersionRequired is the minimum version of the + // Adaptive Card schema required to support Action.Execute. + ActionExecuteMinCardVersionRequired float64 = 1.4 + + // TypeActionSubmit is used in Adaptive Cards schema version 1.3 and + // earlier or as a fallback for TypeActionExecute in schema version 1.4. + // TypeActionSubmit is not supported in Incoming Webhooks. + TypeActionSubmit string = "Action.Submit" + + // TypeActionOpenURL (when invoked) shows the given url either by + // launching it in an external web browser or showing within an embedded + // web browser. + TypeActionOpenURL string = "Action.OpenUrl" + + // TypeActionShowCard defines an AdaptiveCard which is shown to the user + // when the button or link is clicked. + TypeActionShowCard string = "Action.ShowCard" + + // TypeActionToggleVisibility toggles the visibility of associated card + // elements. + TypeActionToggleVisibility string = "Action.ToggleVisibility" +) + +// Supported Fallback options. +const ( + TypeFallbackActionExecute string = TypeActionExecute + TypeFallbackActionOpenURL string = TypeActionOpenURL + TypeFallbackActionShowCard string = TypeActionShowCard + TypeFallbackActionSubmit string = TypeActionSubmit + TypeFallbackActionToggleVisibility string = TypeActionToggleVisibility + + // TypeFallbackOptionDrop causes this element to be dropped immediately + // when unknown elements are encountered. The unknown element doesn't + // bubble up any higher. + TypeFallbackOptionDrop string = "drop" +) + +// Valid types for an Adaptive Card element. Not all types are supported by +// Microsoft Teams. +// +// TODO: Confirm whether all types are supported. +// +// - https://adaptivecards.io/explorer/AdaptiveCard.html +// - https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#support-for-adaptive-cards +// - https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model#schema +const ( + TypeElementActionSet string = "ActionSet" + TypeElementColumnSet string = "ColumnSet" + TypeElementContainer string = "Container" + TypeElementFactSet string = "FactSet" + TypeElementImage string = "Image" + TypeElementImageSet string = "ImageSet" + TypeElementInputChoiceSet string = "Input.ChoiceSet" + TypeElementInputDate string = "Input.Date" + TypeElementInputNumber string = "Input.Number" + TypeElementInputText string = "Input.Text" + TypeElementInputTime string = "Input.Time" + TypeElementInputToggle string = "Input.Toggle" + TypeElementMedia string = "Media" // Introduced in version 1.1 (TODO: Is this supported in Teams message?) + TypeElementRichTextBlock string = "RichTextBlock" // Introduced in version 1.2 + TypeElementTable string = "Table" // Introduced in version 1.5 + TypeElementTextBlock string = "TextBlock" + TypeElementTextRun string = "TextRun" // Introduced in version 1.2 +) + +// Known extension types for an Adaptive Card element. +// +// - https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format?tabs=adaptive-md%2Cdesktop%2Cconnector-html#codeblock-in-adaptive-cards +const ( + TypeElementMSTeamsCodeBlock string = "CodeBlock" +) + +// Sentinel errors for this package. +var ( + // ErrInvalidType indicates that an invalid type was specified. + ErrInvalidType = errors.New("invalid type value") + + // ErrInvalidFieldValue indicates that an invalid value was specified. + ErrInvalidFieldValue = errors.New("invalid field value") + + // ErrMissingValue indicates that an expected value was missing. + ErrMissingValue = errors.New("missing expected value") + + // ErrValueNotFound indicates that a requested value was not found. + ErrValueNotFound = errors.New("requested value not found") +) + +// Message represents a Microsoft Teams message containing one or more +// Adaptive Cards. +type Message struct { + // Type is required; must be set to "message". + Type string `json:"type"` + + // Attachments is a collection of one or more Adaptive Cards. + // + // NOTE: Including multiple attachment *without* AttachmentLayout set to + // "carousel" hides cards after the first. Not sure if this is a bug, or + // if it's intentional. + Attachments []Attachment `json:"attachments"` + + // AttachmentLayout controls the layout for Adaptive Cards in the + // Attachments collection. + AttachmentLayout string `json:"attachmentLayout,omitempty"` + + // ValidateFunc is an optional user-specified validation function that is + // responsible for validating a Message. If not specified, default + // validation is performed. + ValidateFunc func() error `json:"-"` + + // payload is a prepared Message in JSON format for submission or pretty + // printing. + payload *bytes.Buffer `json:"-"` +} + +// Attachments is a collection of Adaptive Cards for a Microsoft Teams +// message. +type Attachments []Attachment + +// Attachment represents an attached Adaptive Card for a Microsoft Teams +// message. +type Attachment struct { + + // ContentType is required; must be set to + // "application/vnd.microsoft.card.adaptive". + ContentType string `json:"contentType"` + + // ContentURL appears to be related to support for tabs. Most examples + // have this value set to null. + // + // TODO: Update this description with confirmed details. + ContentURL NullString `json:"contentUrl,omitempty"` + + // Content represents the content of an Adaptive Card. + // + // TODO: Should this be a pointer? + Content TopLevelCard `json:"content"` +} + +// TopLevelCard represents the outer or top-level Card for a Microsoft Teams +// Message attachment. +type TopLevelCard struct { + Card +} + +// Card represents the content of an Adaptive Card. The TopLevelCard is a +// superset of this one, asserting that the Version field is properly set. +// That type is used exclusively for Message Attachments. This type is used +// directly for the Action.ShowCard Card field. +type Card struct { + + // Type is required; must be set to "AdaptiveCard" + Type string `json:"type"` + + // Schema represents the URI of the Adaptive Card schema. + Schema string `json:"$schema"` + + // Version is required for top-level cards (i.e., the outer card in an + // attachment); the schema version that the content for an Adaptive Card + // requires. + // + // The TopLevelCard type is a superset of the Card type and asserts that + // this field is properly set, whereas the validation logic for this + // (Card) type skips that assertion. + Version string `json:"version"` + + // FallbackText is the text shown when the client doesn't support the + // version specified (may contain markdown). + FallbackText string `json:"fallbackText,omitempty"` + + // Body represents the body of an Adaptive Card. The body is made up of + // building-blocks known as elements. Elements can be composed to create + // many types of cards. These elements are shown in the primary card + // region. + Body []Element `json:"body"` + + // Actions is a collection of actions to show in the card's action bar. + // The action bar is displayed at the bottom of a Card. + // + // NOTE: The max display limit has been observed to be a fixed value for + // web/desktop app and a matching value as an initial display limit for + // mobile app with the option to expand remaining actions in a list. + // + // This value is recorded in this package as "TeamsActionsDisplayLimit". + // + // To work around this limit, create multiple ActionSets each limited to + // the value of TeamsActionsDisplayLimit. + Actions []Action `json:"actions,omitempty"` + + // MSTeams is a container for properties specific to Microsoft Teams + // messages, including formatting properties and user mentions. + // + // NOTE: Using pointer in order to omit unused field from JSON output. + // https://stackoverflow.com/questions/18088294/how-to-not-marshal-an-empty-struct-into-json-with-go + // MSTeams *MSTeams `json:"msteams,omitempty"` + // + // TODO: Revisit this and use a pointer if remote API doesn't like + // receiving an empty object, though brief testing doesn't show this to be + // a problem. + MSTeams MSTeams `json:"msteams,omitempty"` + + // MinHeight specifies the minimum height of the card. + MinHeight string `json:"minHeight,omitempty"` + + // VerticalContentAlignment defines how the content should be aligned + // vertically within the container. Only relevant for fixed-height cards, + // or cards with a minHeight specified. If MinHeight field is specified, + // this field is required. + VerticalContentAlignment string `json:"verticalContentAlignment,omitempty"` +} + +// Elements is a collection of Element values. +type Elements []Element + +// Element is a "building block" for an Adaptive Card. Elements are shown +// within the primary card region (aka, "body"), columns and other container +// types. Not all fields of this Go struct type are supported by all Adaptive +// Card element types. +type Element struct { + + // Type is required and indicates the type of the element used in the body + // of an Adaptive Card. + // https://adaptivecards.io/explorer/AdaptiveCard.html + Type string `json:"type"` + + // ID is a unique identifier associated with this Element. + ID string `json:"id,omitempty"` + + // Text is required by the TextBlock and TextRun element types. Text is + // used to display text. A subset of markdown is supported for text used + // in TextBlock elements, but no formatting is permitted in text used in + // TextRun elements. + // + // https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features + // https://adaptivecards.io/explorer/TextBlock.html + // https://adaptivecards.io/explorer/TextRun.html + Text string `json:"text,omitempty"` + + // URL is required for the Image element type. URL is the URL to an Image + // in an ImageSet element type. + // + // https://adaptivecards.io/explorer/Image.html + // https://adaptivecards.io/explorer/ImageSet.html + URL string `json:"url,omitempty"` + + // Size controls the size of text within a TextBlock element. + Size string `json:"size,omitempty"` + + // Weight controls the weight of text in TextBlock or TextRun elements. + Weight string `json:"weight,omitempty"` + + // Color controls the color of TextBlock elements or text used in TextRun + // elements. + Color string `json:"color,omitempty"` + + // Spacing controls the amount of spacing between this element and the + // preceding element. + Spacing string `json:"spacing,omitempty"` + + // HorizontalAlignment controls the horizontal text alignment. + HorizontalAlignment string `json:"horizontalAlignment,omitempty"` + + // The style of the element for accessibility purposes. Valid values + // differ based on the element type. For example, a TextBlock element + // supports the "heading" style, whereas the Column element supports the + // "attention" style (TextBlock does not). + Style string `json:"style,omitempty"` + + // Items is required for most Container element types. Items is a + // collection of card elements to render inside the Container. + Items []Element `json:"items,omitempty"` + + // Columns is a collection of Columns used to divide a region. This field + // is used both by ColumnSet and Table element types. The specific field + // validation applied is based on the Type field of this Element. + Columns []Column `json:"columns,omitempty"` + + // Rows defines the rows of the table. This field is used by a Table + // element type. + Rows []TableRow `json:"rows,omitempty"` + + // GridStyle defines the style of the grid. This property currently only + // controls the grid's color. This field is used by a Table element type. + GridStyle string `json:"gridStyle,omitempty"` + + // FirstRowAsHeaders specifies whether the first row of the table should be + // treated as a header row, and be announced as such by accessibility + // software. This field is used by a Table element type. + // + // If not specified defaults to true. + // + // NOTE: We define this field as a pointer type so that omitting a value + // for the pointer leaves the field out of the generated JSON payload (due + // to 'omitempty' behavior of the JSON encoder and results in the + // "defaults to true" behavior as defined by the schema. + FirstRowAsHeaders *bool `json:"firstRowAsHeaders,omitempty"` + + // Visible specifies whether this element will be removed from the visual + // tree. + // + // If not specified defaults to true. + // + // NOTE: We define this field as a pointer type so that omitting a value + // for the pointer leaves the field out of the generated JSON payload (due + // to 'omitempty' behavior of the JSON encoder and results in the + // "defaults to true" behavior as defined by the schema. + Visible *bool `json:"isVisible,omitempty"` + + // ShowGridLines specified whether grid lines should be displayed. This + // field is used by a Table element type. + // + // If not specified defaults to true. + // + // NOTE: We define this field as a pointer type so that omitting a value + // for the pointer leaves the field out of the generated JSON payload (due + // to 'omitempty' behavior of the JSON encoder and results in the + // "defaults to true" behavior as defined by the schema. + ShowGridLines *bool `json:"showGridLines,omitempty"` + + // Actions is required for the ActionSet element type. Actions is a + // collection of Actions to show for an ActionSet element type. + // + // TODO: Should this be a pointer? + Actions []Action `json:"actions,omitempty"` + + // SelectAction is an Action that will be invoked when the Container + // element is tapped or selected. Action.ShowCard is not supported. + // + // This field is used by supported Container element types (Column, + // ColumnSet, Container). + // + SelectAction *ISelectAction `json:"selectAction,omitempty"` + + // Facts is required for the FactSet element type. Actions is a collection + // of Fact values that are part of a FactSet element type. Each Fact value + // is a key/value pair displayed in tabular form. + // + // TODO: Should this be a pointer? + Facts []Fact `json:"facts,omitempty"` + + // Wrap controls whether text is allowed to wrap or is clipped for + // TextBlock elements. + Wrap bool `json:"wrap,omitempty"` + + // IsSubtle specifies whether this element should appear slightly toned + // down. + IsSubtle bool `json:"isSubtle,omitempty"` + + // Separator, when true, indicates that a separating line shown should be + // drawn at the top of the element. + Separator bool `json:"separator,omitempty"` + + // CodeSnippet provides the content for a CodeBlock element, specific to MSTeams. + CodeSnippet string `json:"codeSnippet,omitempty"` + + // Language specifies the language of a CodeBlock element, specific to MSTeams. + Language string `json:"language,omitempty"` + + // StartLineNumber specifies the initial line number of CodeBlock element, specific to MSTeams. + StartLineNumber int `json:"startLineNumber,omitempty"` +} + +// Container is an Element type that allows grouping items together. +type Container Element + +// FactSet is an Element type that groups and displays a series of facts (i.e. +// name/value pairs) in a tabular form. +type FactSet Element + +// Columns is a collection of Column values for a ColumnSet or a Table. +type Columns []Column + +// ColumnItems is a collection of card elements that should be rendered inside +// of the column. +type ColumnItems []*Element + +// Column is a container used by a ColumnSet or Table element type. Each +// container may contain one or more elements. +// +// https://adaptivecards.io/explorer/Column.html +type Column struct { + // Type is required; must be set to "Column" when used with ColumnSet type + // or "TableColumnDefinition" when used as a Table column. + Type string `json:"type,omitempty"` + + // ID is a unique identifier associated with this Column. + ID string `json:"id,omitempty"` + + // Width represents the width of a column in the column group OR a column + // in a table. Valid values consist of fixed strings OR a number + // representing the relative width. + // + // If used in a column group, valid values are "auto", "stretch", a number + // representing relative width of the column in the column group or a + // string that specifies a pixel width, like "50px". + // + // If used in a table, valid values are a number representing relative + // width of the column relative to the other columns in the table or a + // string that specifies a pixel width, like "50px". + Width interface{} `json:"width,omitempty"` + + // Items are the card elements that should be rendered inside of the + // column. + Items []*Element `json:"items,omitempty"` + + // SelectAction is an action that will be invoked when the Column is + // tapped or selected. Action.ShowCard is not supported. + SelectAction *ISelectAction `json:"selectAction,omitempty"` + + // HorizontalCellContentAlignment is a property of the Table element type. + // + // This field controls how the content of all cells in the column is + // horizontally aligned by default. When specified, this value overrides + // the setting at the table level. When not specified, horizontal + // alignment is defined at the table, row or cell level. + HorizontalCellContentAlignment string `json:"horizontalCellContentAlignment,omitempty"` + + // VerticalCellContentAlignment is a property of the Table element type. + // + // This field controls how the content of all cells in the column is + // vertically aligned by default. When specified, this value overrides the + // setting at the table level. When not specified, vertical alignment is + // defined at the table, row or cell level. + VerticalCellContentAlignment string `json:"verticalCellContentAlignment,omitempty"` +} + +// Facts is a collection of Fact values. +type Facts []Fact + +// Fact represents a Fact in a FactSet as a key/value pair. +type Fact struct { + // Title is required; the title of the fact. + Title string `json:"title"` + + // Value is required; the value of the fact. + Value string `json:"value"` +} + +// TableColumnDefinition defines the characteristics of a column in a Table +// element such as number of columns or their sizes. +// +// https://adaptivecards.io/explorer/Table.html +type TableColumnDefinition Column + +// TableColumnDefinitions is a collection of TableColumnDefinition values. +// +// We use this as a "wrapper" type to convert a Columns collection so that we +// can apply specific validation requirements specific to a Table column. +type TableColumnDefinitions []Column + +// TableCell represents a cell within a row of a Table element. +// +// https://adaptivecards.io/explorer/TableCell.html +type TableCell struct { + // Type is required; must be set to "TableCell". + Type string `json:"type"` + + // Style is a style hint for a TableCell. + Style string `json:"style,omitempty"` + + // Bleed determines whether the element should bleed through its parent's + // padding. + Bleed bool `json:"bleed,omitempty"` + + // MinHeight specifies the minimum height of the container in pixels + // (e.g., 80px). + MinHeight string `json:"minHeight,omitempty"` + + // VerticalContentAlignment defines how the content should be aligned + // vertically within the container. + // + // When not specified, the value of VerticalContentAlignment is inherited + // from the parent container. If no parent container has + // VerticalContentAlignment set, it defaults to Top. + VerticalContentAlignment string `json:"verticalContentAlignment,omitempty"` + + // Items are the card elements that should be rendered inside of the + // cell. + Items []*Element `json:"items,omitempty"` +} + +// TableCells is a collection of TableCell values. +type TableCells []TableCell + +// TableRow is a row within a Table each being a collection of cells. Rows are +// not required, which allows empty Tables to be generated via templating +// without breaking the rendering of the whole card. +// +// https://adaptivecards.io/explorer/Table.html +type TableRow struct { + // Type is required; must be set to "TableRow". + Type string `json:"type"` + + // Style defines the style of the entire row. + Style string `json:"style,omitempty"` + + // HorizontalCellContentAlignment is a property of the Table element type. + // + // This field controls how the content of all cells in the row is + // horizontally aligned by default. When specified, this value overrides + // both the setting at the table and columns level. When not specified, + // horizontal alignment is defined at the table, column or cell level. + HorizontalCellContentAlignment string `json:"horizontalCellContentAlignment,omitempty"` + + // VerticalCellContentAlignment is a property of the Table element type. + // + // This field controls how the content of all cells in the column is + // vertically aligned by default. When specified, this value overrides the + // setting at the table and column level. When not specified, vertical + // alignment is defined either at the table, column or cell level. + VerticalCellContentAlignment string `json:"verticalCellContentAlignment,omitempty"` + + // Cells are the cells in this row. If a row contains more cells than + // there are columns defined on the Table element, the extra cells are + // ignored. + Cells []TableCell `json:"cells"` +} + +// TableRows is a collection of TableRow values. +type TableRows []TableRow + +// Actions is a collection of Action values. +type Actions []Action + +// Action represents an action that a user may take on a card. Actions +// typically get rendered in an "action bar" at the bottom of a card. +// +// - https://adaptivecards.io/explorer/ActionSet.html +// - https://adaptivecards.io/explorer/AdaptiveCard.html +// - https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference +// +// TODO: Extend with additional supported fields. +type Action struct { + + // Type is required; specific values are supported. + // + // Action.Submit is not supported for Incoming Webhooks. + // + // Action.Execute was added in Adaptive Card schema version 1.4. which + // Teams MAY not fully support. + // + // The supported actions are Action.OpenURL, Action.ShowCard, + // Action.ToggleVisibility, and Action.Execute (see above). + // + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#support-for-adaptive-cards + // https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model#schema + Type string `json:"type"` + + // ID is a unique identifier associated with this Action. + ID string `json:"id,omitempty"` + + // Title is a label for the button or link that represents this action. + Title string `json:"title,omitempty"` + + // URL to open; required for the Action.OpenUrl type, optional for other + // action types. + URL string `json:"url,omitempty"` + + // Fallback describes what to do when an unknown element is encountered or + // the requirements of this or any children can't be met. + Fallback string `json:"fallback,omitempty"` + + // Card property is used by Action.ShowCard type. + // + // NOTE: Based on a review of JSON content, it looks like `ActionCard` is + // really just a `Card` type. + // + // refs https://github.com/matthidinger/ContosoScubaBot/blob/master/Cards/SubscriberNotification.JSON + Card *Card `json:"card,omitempty"` + + // TargetElements is the collection of TargetElement values. + // + // It is not recommended to include Input elements with validation due to + // confusion that can arise from invalid inputs that are not currently + // visible. + // + // https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/input-validation + TargetElements []TargetElement `json:"targetElements,omitempty"` +} + +// TargetElement represents an entry for Action.ToggleVisibility's +// targetElements property. +// +// - https://adaptivecards.io/explorer/TargetElement.html +// - https://adaptivecards.io/explorer/Action.ToggleVisibility.html +type TargetElement struct { + // ElementID is the ID value of the element to toggle. + ElementID string `json:"elementId"` + + // Visible provides display or visibility control for a target Element. + // + // - If true, always show target element. + // - If false, always hide target element. + // - If not supplied, toggle target element's visibility. + // + // NOTE: We define this field as a pointer type so that omitting a value + // for the pointer leaves the field out of the generated JSON payload (due + // to 'omitempty' behavior of the JSON encoder. If leaving this field out, + // visibility can be toggled for target Elements. + Visible *bool `json:"isVisible,omitempty"` +} + +/* + +General scratch notes for https://github.com/atc0005/go-teams-notify/issues/243 +=============================================================================== + +https://adaptivecards.io/explorer/Action.ToggleVisibility.html +https://adaptivecards.io/explorer/TargetElement.html + +While the targetElements array (JSON) supports raw text strings OR +TargetElement values, we will opt to only support TargetElement values. +Otherwise, we end up needing to use more complicated logic. + +Instead of trying to support this: + + "targetElements": [ + "textToToggle", + "imageToToggle", + "imageToToggle2" + ] + +we support this instead: + + "targetElements": [ + { + "elementId": "textToToggle" + }, + { + "elementId": "imageToToggle" + }, + { + "elementId": "imageToToggle2" + } + ] + + +A Container type has a selectAction field. That slice contains TargetElement +entries. + +*/ + +// ISelectAction represents an Action that will be invoked when a container +// type (e.g., Column, ColumnSet, Container) is tapped or selected. +// Action.ShowCard is not supported. +// +// - https://adaptivecards.io/explorer/Container.html +// - https://adaptivecards.io/explorer/ColumnSet.html +// - https://adaptivecards.io/explorer/Column.html +// +// TODO: Extend with additional supported fields. +type ISelectAction struct { + + // Type is required; specific values are supported. + // + // The supported actions are Action.Execute, Action.OpenUrl, + // Action.ToggleVisibility. + // + // See also https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference + Type string `json:"type"` + + // ID is a unique identifier associated with this ISelectAction. + ID string `json:"id,omitempty"` + + // Title is a label for the button or link that represents this action. + Title string `json:"title,omitempty"` + + // URL is required for the Action.OpenUrl type, optional for other action + // types. + URL string `json:"url,omitempty"` + + // Fallback describes what to do when an unknown element is encountered or + // the requirements of this or any children can't be met. + Fallback string `json:"fallback,omitempty"` + + // TargetElements is the collection of TargetElement values. + // + // This field is specific to the Action.ToggleVisibility Action type. + // + // It is not recommended to include Input elements with validation due to + // confusion that can arise from invalid inputs that are not currently + // visible. + // + // https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/input-validation + TargetElements []TargetElement `json:"targetElements,omitempty"` +} + +// MSTeams represents a container for properties specific to Microsoft Teams +// messages, including formatting properties and user mentions. +type MSTeams struct { + + // Width controls the width of Adaptive Cards within a Microsoft Teams + // messages. + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format#full-width-adaptive-card + Width string `json:"width,omitempty"` + + // AllowExpand controls whether images can be displayed in stage view + // selectively. + // + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format#stage-view-for-images-in-adaptive-cards + AllowExpand bool `json:"allowExpand,omitempty"` + + // Entities is a collection of user mentions. + // TODO: Should this be a slice of pointers? + Entities []Mention `json:"entities,omitempty"` +} + +// Mentions is a collection of Mention values. +type Mentions []Mention + +// Mention represents a mention in the message for a specific user. +type Mention struct { + // Type is required; must be set to "mention". + Type string `json:"type"` + + // Text must match a portion of the message text field. If it does not, + // the mention is ignored. + // + // Brief testing indicates that this needs to wrap a name/value in NAME + // HERE tags. + Text string `json:"text"` + + // Mentioned represents a user that is mentioned. + Mentioned Mentioned `json:"mentioned"` +} + +// Mentioned represents the user id and name of a user that is mentioned. +type Mentioned struct { + // ID is the unique identifier for a user that is mentioned. This value + // can be an object ID (e.g., 5e8b0f4d-2cd4-4e17-9467-b0f6a5c0c4d0) or a + // UserPrincipalName (e.g., NewUser@contoso.onmicrosoft.com). + ID string `json:"id"` + + // Name is the DisplayName of the user mentioned. + Name string `json:"name"` +} + +// NewMessage creates a new Message with required fields predefined. +func NewMessage() *Message { + return &Message{ + Type: TypeMessage, + } +} + +// NewSimpleMessage creates a new simple Message using the specified text and +// optional title. If specified, text wrapping is enabled. An error is +// returned if an empty text string is specified. +func NewSimpleMessage(text string, title string, wrap bool) (*Message, error) { + if text == "" { + return nil, fmt.Errorf( + "required field text is empty: %w", + ErrMissingValue, + ) + } + + msg := Message{ + Type: TypeMessage, + } + + textCard, err := NewTextBlockCard(text, title, wrap) + if err != nil { + return nil, fmt.Errorf( + "failed to create TextBlock card: %w", + err, + ) + } + + if err := msg.Attach(textCard); err != nil { + return nil, fmt.Errorf( + "failed to create simple message: %w", + err, + ) + } + + return &msg, nil +} + +// NewTextBlockCard creates a new Card using the specified text and optional +// title. If specified, the TextBlock has text wrapping enabled. +func NewTextBlockCard(text string, title string, wrap bool) (Card, error) { + if text == "" { + return Card{}, fmt.Errorf( + "required field text is empty: %w", + ErrMissingValue, + ) + } + + textBlock := Element{ + Type: TypeElementTextBlock, + Wrap: wrap, + Text: text, + } + + card := Card{ + Type: TypeAdaptiveCard, + Schema: AdaptiveCardSchema, + Version: fmt.Sprintf(AdaptiveCardVersionTmpl, AdaptiveCardMaxVersion), + Body: []Element{ + textBlock, + }, + } + + if title != "" { + titleTextBlock := NewTitleTextBlock(title, wrap) + card.Body = append([]Element{titleTextBlock}, card.Body...) + } + + return card, nil +} + +// NewCard creates and returns an empty Card. +func NewCard() Card { + return Card{ + Type: TypeAdaptiveCard, + Schema: AdaptiveCardSchema, + Version: fmt.Sprintf(AdaptiveCardVersionTmpl, AdaptiveCardMaxVersion), + } +} + +// Attach receives and adds one or more Card values to the Attachments +// collection for a Microsoft Teams message. +// +// NOTE: Including multiple cards in the attachments collection *without* +// attachmentLayout set to "carousel" hides cards after the first. Not sure if +// this is a bug, or if it's intentional. +func (m *Message) Attach(cards ...Card) error { + if len(cards) == 0 { + return fmt.Errorf( + "received empty collection of cards: %w", + ErrMissingValue, + ) + } + + for _, card := range cards { + attachment := Attachment{ + ContentType: AttachmentContentType, + + // Explicitly convert Card to TopLevelCard in order to assert that + // TopLevelCard specific requirements are checked during + // validation. + Content: TopLevelCard{card}, + } + + m.Attachments = append(m.Attachments, attachment) + } + + return nil +} + +// Carousel sets the Message Attachment layout to Carousel display mode. +func (m *Message) Carousel() *Message { + m.AttachmentLayout = AttachmentLayoutCarousel + return m +} + +// PrettyPrint returns a formatted JSON payload of the Message if the +// Prepare() method has been called, or an empty string otherwise. +func (m *Message) PrettyPrint() string { + if m.payload != nil { + var prettyJSON bytes.Buffer + _ = json.Indent(&prettyJSON, m.payload.Bytes(), "", "\t") + + return prettyJSON.String() + } + + return "" +} + +// Prepare handles tasks needed to construct a payload from a Message for +// delivery to an endpoint. +func (m *Message) Prepare() error { + jsonMessage, err := json.Marshal(m) + if err != nil { + return fmt.Errorf( + "error marshalling Message to JSON: %w", + err, + ) + } + + switch { + case m.payload == nil: + m.payload = &bytes.Buffer{} + default: + m.payload.Reset() + } + + _, err = m.payload.Write(jsonMessage) + if err != nil { + return fmt.Errorf( + "error updating JSON payload for Message: %w", + err, + ) + } + + return nil +} + +// Payload returns the prepared Message payload. The caller should call +// Prepare() prior to calling this method, results are undefined otherwise. +func (m *Message) Payload() io.Reader { + return m.payload +} + +// Validate performs validation for Message using ValidateFunc if defined, +// otherwise applying default validation. +func (m Message) Validate() error { + if m.ValidateFunc != nil { + return m.ValidateFunc() + } + + v := validator.Validator{} + + v.FieldHasSpecificValue( + m.Type, + "type", + TypeMessage, + "message", + ErrInvalidType, + ) + + // We need an attachment (containing one or more Adaptive Cards) in order + // to generate a valid Message for Microsoft Teams delivery. + v.NotEmptyCollection("Attachments", m.Type, ErrMissingValue, m.Attachments) + + v.SelfValidate(Attachments(m.Attachments)) + + // Optional field, but only specific values permitted if set. + v.InListIfFieldValNotEmpty( + m.AttachmentLayout, + "AttachmentLayout", + "message", + supportedAttachmentLayoutValues(), + ErrInvalidFieldValue, + ) + + return v.Err() +} + +// Validate asserts that fields have valid values. +func (a Attachment) Validate() error { + v := validator.Validator{} + + v.FieldHasSpecificValue( + a.ContentType, + "attachment type", + AttachmentContentType, + "attachment", + ErrInvalidType, + ) + + v.SelfValidate(a.Content) + + return v.Err() +} + +// Validate asserts that the collection of Attachment values are all valid. +func (a Attachments) Validate() error { + for _, attachment := range a { + if err := attachment.Validate(); err != nil { + return err + } + } + + return nil +} + +// Validate asserts that fields have valid values. +func (c Card) Validate() error { + v := validator.Validator{} + + // TODO: Version field validation + // + // The Version field is required for top-level cards, optional for Cards + // nested within an Action.ShowCard. Because we don't have a reliable way + // to assert that relationship, we skip applying validation for that value + // for now. + + v.FieldHasSpecificValue( + c.Type, + "type", + TypeAdaptiveCard, + "card", + ErrInvalidType, + ) + + // While the schema value should be set it is not strictly required. If it + // is set, we assert that it is the correct value. + v.FieldHasSpecificValueIfFieldNotEmpty( + c.Schema, + "Schema", + AdaptiveCardSchema, + "card", + ErrInvalidFieldValue, + ) + + // Both are optional fields, unless MinHeight is set in which case + // VerticalContentAlignment is required. + v.SuccessfulFuncCall( + func() error { + return assertHeightAlignmentFieldsSetWhenRequired( + c.MinHeight, c.VerticalContentAlignment, + ) + }, + ) + + v.SuccessfulFuncCall( + func() error { + return assertCardBodyHasMention(c.Body, c.MSTeams.Entities) + }, + ) + + v.SelfValidate(Elements(c.Body)) + v.SelfValidate(Actions(c.Actions)) + + return v.Err() +} + +// Validate asserts that fields have valid values. +func (tc TopLevelCard) Validate() error { + v := validator.Validator{} + + // Validate embedded Card first as those validation requirements apply + // here also. + v.SelfValidate(tc.Card) + + // The Version field is required for top-level cards (this one), optional + // for Cards nested within an Action.ShowCard. + v.SuccessfulFuncCall( + func() error { return assertValidVersionFieldValue(tc.Version) }, + ) + + return v.Err() +} + +// Validate asserts that the collection of Element values are all valid. +func (e Elements) Validate() error { + for _, element := range e { + if err := element.Validate(); err != nil { + return err + } + } + + return nil +} + +// Validate asserts that fields have valid values. +func (e Element) Validate() error { + v := validator.Validator{} + + supportedElementTypes := supportedElementTypes() + supportedSizeValues := supportedSizeValues() + supportedWeightValues := supportedWeightValues() + supportedColorValues := supportedColorValues() + supportedSpacingValues := supportedSpacingValues() + supportedHorizontalAlignmentValues := supportedHorizontalAlignmentValues() + + // Valid Style field values differ based on type. For example, a Container + // element supports Container styles whereas a TextBlock supports a + // different and more limited set of style values. We use a helper + // function to retrieve valid style values for evaluation. + supportedStyleValues := supportedStyleValues(e.Type) + + /****************************************************************** + General requirements for all Element types. + ******************************************************************/ + + v.InListIfFieldValNotEmpty(e.Type, "Type", "element", supportedElementTypes, ErrInvalidType) + v.InListIfFieldValNotEmpty(e.Size, "Size", "element", supportedSizeValues, ErrInvalidFieldValue) + v.InListIfFieldValNotEmpty(e.Weight, "Weight", "element", supportedWeightValues, ErrInvalidFieldValue) + v.InListIfFieldValNotEmpty(e.Color, "Color", "element", supportedColorValues, ErrInvalidFieldValue) + v.InListIfFieldValNotEmpty(e.Spacing, "Spacing", "element", supportedSpacingValues, ErrInvalidFieldValue) + v.InListIfFieldValNotEmpty(e.HorizontalAlignment, "HorizontalAlignment", "element", supportedHorizontalAlignmentValues, ErrInvalidFieldValue) + v.InListIfFieldValNotEmpty(e.Style, "Style", "element", supportedStyleValues, ErrInvalidFieldValue) + + /****************************************************************** + Requirements for specific Element types. + ******************************************************************/ + + switch { + // The Text field is required by TextBlock and TextRun elements, but an + // empty string appears to be permitted. Because of this, we avoid + // asserting that a value is present for the field. + // case e.Type == TypeElementTextBlock: + // case e.Type == TypeElementTextRun: + + // Columns collection is used by the ColumnSet type. While not required, + // the collection should be checked. + case e.Type == TypeElementColumnSet: + v.SelfValidate(Columns(e.Columns)) + + if e.SelectAction != nil { + v.SelfValidate(e.SelectAction) + } + + // Actions collection is required for ActionSet element type. + // https://adaptivecards.io/explorer/ActionSet.html + case e.Type == TypeElementActionSet: + v.NotEmptyCollection("Actions", e.Type, ErrMissingValue, e.Actions) + v.SelfValidate(Actions(e.Actions)) + + // Items collection is required for Container element type. + // https://adaptivecards.io/explorer/Container.html + case e.Type == TypeElementContainer: + v.NotEmptyCollection("Items", e.Type, ErrMissingValue, e.Items) + v.SelfValidate(Elements(e.Items)) + + if e.SelectAction != nil { + v.SelfValidate(e.SelectAction) + } + + // URL is required for Image element type. + // https://adaptivecards.io/explorer/Image.html + case e.Type == TypeElementImage: + v.NotEmptyValue(e.URL, "URL", e.Type, ErrMissingValue) + + // Facts collection is required for FactSet element type. + // https://adaptivecards.io/explorer/FactSet.html + case e.Type == TypeElementFactSet: + v.NotEmptyCollection("Facts", e.Type, ErrMissingValue, e.Facts) + v.SelfValidate(Facts(e.Facts)) + + case e.Type == TypeElementTable: + v.InListIfFieldValNotEmpty( + e.GridStyle, + "GridStyle", + e.Type, + supportedContainerStyleValues(), + ErrInvalidFieldValue, + ) + + v.SelfValidate(TableRows(e.Rows)) + + v.SelfValidate(TableColumnDefinitions(e.Columns)) + + case e.Type == TypeElementMSTeamsCodeBlock: + v.NotEmptyValue(e.CodeSnippet, "CodeSnippet", e.Type, ErrMissingValue) + v.NotEmptyValue(e.Language, "Language", e.Type, ErrMissingValue) + } + + // Return the last recorded validation error, or nil if no validation + // errors occurred. + return v.Err() +} + +// Validate asserts that the collection of Column values are all valid. +func (c Columns) Validate() error { + for _, column := range c { + if err := column.Validate(); err != nil { + return err + } + } + + return nil +} + +// Validate asserts that the Items collection field for a column contains +// valid values. Special handling is applied since the collection could +// contain nil values. +func (ci ColumnItems) Validate() error { + for _, item := range ci { + if item == nil { + return fmt.Errorf( + "card element in Column is nil: %w", + ErrMissingValue, + ) + } + + if err := item.Validate(); err != nil { + return err + } + } + + return nil +} + +// Validate asserts that the collection of TableColumnDefinition values are +// all valid. +func (tcds TableColumnDefinitions) Validate() error { + for _, c := range tcds { + // We convert the Column type to a TableColumnDefinition so that + // fields specific to that "subtype" have separate validation logic + // applied vs the Column type used by the ColumnSet container type. + if err := TableColumnDefinition(c).Validate(); err != nil { + return err + } + } + + return nil +} + +// Validate asserts that fields have valid values. +func (tcd TableColumnDefinition) Validate() error { + v := validator.Validator{} + + // The schema shows that this is supposed to be set to + // "TableColumnDefinition", though the example payload I reviewed did not + // set the Type field. Because of this, we should support either not + // setting the field at all OR requiring this specific type. + v.FieldHasSpecificValueIfFieldNotEmpty( + tcd.Type, + "type", + TypeTableColumnDefinition, + "column", + ErrInvalidType, + ) + + v.SuccessfulFuncCall( + func() error { return assertTableColumnDefinitionWidthValidValues(tcd) }, + ) + + v.InListIfFieldValNotEmpty( + tcd.VerticalCellContentAlignment, + "VerticalCellContentAlignment", + TypeTableColumnDefinition, + supportedVerticalContentAlignmentValues(), + ErrInvalidFieldValue, + ) + + v.InListIfFieldValNotEmpty( + tcd.HorizontalCellContentAlignment, + "HorizontalCellContentAlignment", + TypeTableColumnDefinition, + supportedHorizontalAlignmentValues(), + ErrInvalidFieldValue, + ) + + return v.Err() +} + +// Validate asserts that the collection of TableRow values are all valid. +func (trs TableRows) Validate() error { + for _, row := range trs { + if err := row.Validate(); err != nil { + return err + } + } + + return nil +} + +// AddCell adds one or many TableCell values to a TableRow. An error is +// returned if any TableCell value fails validation. +func (tr *TableRow) AddCell(cells ...TableCell) error { + if len(cells) == 0 { + return fmt.Errorf("no data provided: %w", ErrMissingValue) + } + + for _, cell := range cells { + if err := cell.Validate(); err != nil { + return err + } + } + + tr.Cells = append(tr.Cells, cells...) + + return nil +} + +// // TableCells returns a collection of underlying TableCell pointers or an +// // empty collection if no TableCell values are available. +// func (trs *TableRows) TableCells() []*TableCell { +// if trs == nil { +// return []*TableCell{} +// } +// +// var numCells int +// for _, row := range *trs { +// for range row.Cells { +// numCells++ +// } +// } +// cells := make([]*TableCell, numCells) +// for _, row := range *trs { +// for i := range row.Cells { +// cells = append(cells, &row.Cells[i]) +// } +// } +// +// return cells +// } + +// Validate asserts that fields have valid values. +func (tr TableRow) Validate() error { + v := validator.Validator{} + + v.FieldHasSpecificValueIfFieldNotEmpty( + tr.Type, + "type", + TypeTableRow, + "table row", + ErrInvalidType, + ) + + v.InListIfFieldValNotEmpty( + tr.Style, + "Style", + TypeTableRow, + supportedContainerStyleValues(), + ErrInvalidFieldValue, + ) + + v.InListIfFieldValNotEmpty( + tr.VerticalCellContentAlignment, + "VerticalCellContentAlignment", + TypeTableRow, + supportedVerticalContentAlignmentValues(), + ErrInvalidFieldValue, + ) + + v.InListIfFieldValNotEmpty( + tr.HorizontalCellContentAlignment, + "HorizontalCellContentAlignment", + TypeTableRow, + supportedHorizontalAlignmentValues(), + ErrInvalidFieldValue, + ) + + // Validate collection by using "wrapper" type. + v.SelfValidate(TableCells(tr.Cells)) + + return v.Err() +} + +// Validate asserts that the collection of TableCell values are all valid. +func (tcs TableCells) Validate() error { + for _, cell := range tcs { + if err := cell.Validate(); err != nil { + return err + } + } + + return nil +} + +// AddElement adds one or many Element value pointers to a TableCell. An error +// is returned if any Element value fails validation. +func (tr *TableCell) AddElement(elements ...*Element) error { + if len(elements) == 0 { + return fmt.Errorf("no data provided: %w", ErrMissingValue) + } + + for _, cell := range elements { + if cell == nil { + return fmt.Errorf("no data provided: %w", ErrMissingValue) + } + + if err := cell.Validate(); err != nil { + return err + } + } + + tr.Items = append(tr.Items, elements...) + + return nil +} + +// Validate asserts that fields have valid values. +func (tr TableCell) Validate() error { + v := validator.Validator{} + + v.FieldHasSpecificValueIfFieldNotEmpty( + tr.Type, + "type", + TypeTableCell, + "table cell", + ErrInvalidType, + ) + + v.InListIfFieldValNotEmpty( + tr.Style, + "Style", + TypeTableCell, + supportedContainerStyleValues(), + ErrInvalidFieldValue, + ) + + v.SuccessfulFuncCall( + func() error { + return assertValidPixelSizeOrEmptyValue(tr.MinHeight) + }, + ) + + v.InListIfFieldValNotEmpty( + tr.VerticalContentAlignment, + "VerticalContentAlignment", + TypeTableCell, + supportedVerticalContentAlignmentValues(), + ErrInvalidFieldValue, + ) + + v.NotEmptyCollection( + "TableCellItems", + TypeTableCell, + ErrMissingValue, + tr.Items, + ) + + v.NoNilValuesInCollection( + "TableCellItems", + TypeTableCell, + ErrMissingValue, + tr.Items, + ) + + for _, item := range tr.Items { + v.SelfValidate(item) + } + + return v.Err() +} + +// AddSelectAction adds a given Action or ISelectAction value to the +// associated Column. This action will be invoked when the Column is +// tapped or selected. +// +// An error is returned if the given Action or ISelectAction value fails +// validation or if a value other than an Action or ISelectAction is provided. +func (c *Column) AddSelectAction(action interface{}) error { + switch v := action.(type) { + case Action: + // Perform manual conversion to the supported type. + selectAction := ISelectAction{ + Type: v.Type, + ID: v.ID, + Title: v.Title, + URL: v.URL, + Fallback: v.Fallback, + } + + // Don't touch the new TargetElements field unless the provided Action + // has specified values. + if len(v.TargetElements) > 0 { + selectAction.TargetElements = append( + selectAction.TargetElements, + v.TargetElements..., + ) + } + + c.SelectAction = &selectAction + + case ISelectAction: + c.SelectAction = &v + + // unsupported value provided + default: + return fmt.Errorf( + "error: unsupported value provided; "+ + " only Action or ISelectAction values are supported: %w", + ErrInvalidFieldValue, + ) + } + + return nil +} + +// Validate asserts that fields have valid values. +func (c Column) Validate() error { + v := validator.Validator{} + + v.FieldHasSpecificValue( + c.Type, + "type", + TypeColumn, + "column", + ErrInvalidType, + ) + + v.SuccessfulFuncCall( + func() error { return assertColumnWidthValidValues(c) }, + ) + + // Assert that the collection does not contain nil items. + v.NoNilValuesInCollection("Items", c.Type, ErrMissingValue, c.Items) + + // Convert []*Element to ColumnItems so that we can use its Validate() + // method to handle cases where nil values could be present in the + // collection. + v.SelfValidate(ColumnItems(c.Items)) + + if c.SelectAction != nil { + v.SelfValidate(c.SelectAction) + } + + return v.Err() +} + +// Validate asserts that the collection of Fact values are all valid. +func (f Facts) Validate() error { + for _, fact := range f { + if err := fact.Validate(); err != nil { + return err + } + } + + return nil +} + +// Validate asserts that fields have valid values. +func (f Fact) Validate() error { + v := validator.Validator{} + + v.NotEmptyValue(f.Title, "Title", "Fact", ErrMissingValue) + v.NotEmptyValue(f.Value, "Value", "Fact", ErrMissingValue) + + return v.Err() +} + +// Validate asserts that fields have valid values. +func (m MSTeams) Validate() error { + v := validator.Validator{} + + // If an optional width value is set, assert that it is a valid value. + v.InListIfFieldValNotEmpty( + m.Width, + "Width", + "MSTeams", + supportedMSTeamsWidthValues(), + ErrInvalidFieldValue, + ) + + v.SelfValidate(Mentions(m.Entities)) + + return v.Err() +} + +// Validate asserts that fields have valid values. +func (i ISelectAction) Validate() error { + supportedISelectActionValues := supportedISelectActionValues(AdaptiveCardMaxVersion) + fallbackValues := supportedActionFallbackValues(AdaptiveCardMaxVersion) + + v := validator.Validator{} + + // Some supportedISelectActionValues are restricted to later Adaptive Card + // schema versions. + v.InList( + i.Type, + "Type", + "ISelectAction", + supportedISelectActionValues, + ErrInvalidType, + ) + + v.InListIfFieldValNotEmpty( + i.Fallback, + "Fallback", + "ISelectAction", + supportedISelectActionFallbackValues(AdaptiveCardMaxVersion), + ErrInvalidFieldValue, + ) + + // See also: Action.Validate() logic. + switch { + case i.Type == TypeActionOpenURL: + v.NotEmptyValue(i.URL, "URL", i.Type, ErrMissingValue) + + case i.Fallback != "": + v.InList(i.Fallback, "Fallback", "action", fallbackValues, ErrInvalidFieldValue) + + case i.Type == TypeActionToggleVisibility: + v.NotEmptyCollection("TargetElements", i.Type, ErrMissingValue, i.TargetElements) + } + + return v.Err() +} + +// Validate asserts that the collection of Action values are all valid. +func (a Actions) Validate() error { + for _, action := range a { + if err := action.Validate(); err != nil { + return err + } + } + + return nil +} + +// AddTargetElement records the IDs from the given Elements in new +// TargetElement values. The specified visibility setting is used for the new +// TargetElement values. +// +// - If true, always show target Element. +// - If false, always hide target Element. +// - If nil, allow toggling target Element's visibility. +// +// If the given visibility setting is nil, then the visibility setting for the +// TargetElement values is omitted. This enables toggling visibility for the +// target Elements (e.g., toggle button behavior). +func (a *Action) AddTargetElement(visible *bool, elements ...Element) error { + elementIDs := make([]string, 0, len(elements)) + for _, e := range elements { + if strings.TrimSpace(e.ID) == "" { + return fmt.Errorf( + "given Element has empty ID value: %w", + ErrInvalidFieldValue, + ) + } + + elementIDs = append(elementIDs, e.ID) + } + + return a.AddTargetElementID(visible, elementIDs...) +} + +// AddVisibleTargetElement records the Element IDs from the given Elements in +// new TargetElement values. All new TargetElement values are explicitly set +// as visible. +func (a *Action) AddVisibleTargetElement(elements ...Element) error { + visible := true + + return a.AddTargetElement(&visible, elements...) +} + +// AddHiddenTargetElement records the Element IDs from the given Elements in +// new TargetElement values. All new TargetElement values are explicitly set +// as not visible. +func (a *Action) AddHiddenTargetElement(elements ...Element) error { + visible := false + + return a.AddTargetElement(&visible, elements...) +} + +// AddTargetElementID records the given Element ID values in the TargetElements +// collection. A non-empty ID value is required, but the Adaptive Card "tree" +// is not searched for a valid match; it is up to the caller to ensure that +// the given ID value is valid. +// +// The specified visibility setting is used for the new TargetElement values. +// +// - If true, always show target Element. +// - If false, always hide target Element. +// - If nil, allow toggling target Element's visibility. +// +// If the given visibility setting is nil, then the visibility setting for the +// TargetElement values is omitted. This enables toggling visibility for the +// target Elements (e.g., toggle button behavior). +func (a *Action) AddTargetElementID(visible *bool, elementIDs ...string) error { + for _, id := range elementIDs { + if strings.TrimSpace(id) == "" { + return fmt.Errorf( + "received empty Element ID value: %w", + ErrMissingValue, + ) + } + + existingElementIDs := func() []string { + ids := make([]string, 0, len(a.TargetElements)) + for _, targetElement := range a.TargetElements { + ids = append(ids, targetElement.ElementID) + } + + return ids + }() + + // Assert that the ID is not already in the collection. + if goteamsnotify.InList(id, existingElementIDs, false) { + return fmt.Errorf( + "received duplicate Element ID value %q: %w", + id, + ErrInvalidFieldValue, + ) + } + + a.TargetElements = append( + a.TargetElements, + TargetElement{ + ElementID: id, + Visible: visible, + }, + ) + } + + return nil +} + +// Validate asserts that fields have valid values. +func (a Action) Validate() error { + actionValues := supportedActionValues(AdaptiveCardMaxVersion) + fallbackValues := supportedActionFallbackValues(AdaptiveCardMaxVersion) + + v := validator.Validator{} + + // Some Actions are restricted to later Adaptive Card schema versions. + v.InList(a.Type, "Type", "action", actionValues, ErrInvalidType) + + switch { + case a.Type == TypeActionOpenURL: + v.NotEmptyValue(a.URL, "URL", a.Type, ErrMissingValue) + + case a.Fallback != "": + v.InList(a.Fallback, "Fallback", "action", fallbackValues, ErrInvalidFieldValue) + + case a.Type == TypeActionToggleVisibility: + v.NotEmptyCollection("TargetElements", a.Type, ErrMissingValue, a.TargetElements) + + // Optional, but only supported by the Action.ShowCard type. + case a.Card != nil: + v.FieldHasSpecificValue(a.Type, "type", TypeActionShowCard, "type", ErrInvalidType) + } + + // Return the last recorded validation error, or nil if no validation + // errors occurred. + return v.Err() +} + +// Validate asserts that the collection of Mention values are all valid. +func (m Mentions) Validate() error { + for _, mention := range m { + if err := mention.Validate(); err != nil { + return err + } + } + + return nil +} + +// Validate asserts that fields have valid values. +// +// Element.Validate() asserts that required Mention.Text content is found for +// each recorded user mention the Card.. +func (m Mention) Validate() error { + if m.Type != TypeMention { + return fmt.Errorf( + "invalid Mention type %q; expected %q: %w", + m.Type, + TypeMention, + ErrInvalidType, + ) + } + + if m.Text == "" { + return fmt.Errorf( + "required field Text is empty for Mention: %w", + ErrMissingValue, + ) + } + + return nil +} + +// Validate asserts that fields have valid values. +func (m Mentioned) Validate() error { + if m.ID == "" { + return fmt.Errorf( + "required field ID is empty: %w", + ErrMissingValue, + ) + } + + if m.Name == "" { + return fmt.Errorf( + "required field Name is empty: %w", + ErrMissingValue, + ) + } + + return nil +} + +// Mention uses the provided display name, ID and text values to add a new +// user Mention and TextBlock element to the first Card in the Message. +// +// If no Cards are yet attached to the Message, a new card is created using +// the Mention and TextBlock element. If specified, the new TextBlock element +// is added as the first element of the Card, otherwise it is added last. An +// error is returned if insufficient values are provided. +func (m *Message) Mention(prependElement bool, displayName string, id string, msgText string) error { + // NOTE: Rely on called functions to validate given arguments. + + switch { + // If no existing cards, add a new one. + case len(m.Attachments) == 0: + mentionCard, err := NewMentionCard(displayName, id, msgText) + if err != nil { + return err + } + + if err := m.Attach(mentionCard); err != nil { + return err + } + + // We have at least one Card already, use it. + default: + + // Build mention. + mention, err := NewMention(displayName, id) + if err != nil { + return fmt.Errorf( + "add new Mention to Message: %w", + err, + ) + } + + textBlock := Element{ + Type: TypeElementTextBlock, + + // TODO: Any issues caused by enabling wrapping? The goal is to + // prevent the Mention.Text content from pushing user specified + // text off of the Card, out of sight. + Wrap: true, + + // The text block contains the mention text string (required) and + // user-specified message text string. Use the mention text as a + // "greeting" or lead-in for the user-specified message text. + Text: mention.Text + " " + msgText, + } + + switch { + case prependElement: + m.Attachments[0].Content.Body = append( + []Element{textBlock}, + m.Attachments[0].Content.Body..., + ) + default: + m.Attachments[0].Content.Body = append( + m.Attachments[0].Content.Body, + textBlock, + ) + } + + m.Attachments[0].Content.MSTeams.Entities = append( + m.Attachments[0].Content.MSTeams.Entities, + mention, + ) + } + + return nil +} + +// Mention uses the given display name, ID and message text to add a new user +// Mention and TextBlock element to the Card. If specified, the new TextBlock +// element is added as the first element of the Card, otherwise it is added +// last. An error is returned if provided values are insufficient to create +// the user mention. +func (c *Card) Mention(displayName string, id string, msgText string, prependElement bool) error { + if msgText == "" { + return fmt.Errorf( + "required msgText argument is empty: %w", + ErrMissingValue, + ) + } + + // Rely on this called function to validate the other arguments. + mention, err := NewMention(displayName, id) + if err != nil { + return err + } + + textBlock := Element{ + Type: TypeElementTextBlock, + + // TODO: Any issues caused by enabling wrapping? The goal is to + // prevent the Mention.Text content from pushing user specified text + // off of the Card, out of sight. + Wrap: true, + Text: mention.Text + " " + msgText, + } + + switch { + case prependElement: + c.Body = append(c.Body, textBlock) + default: + c.Body = append([]Element{textBlock}, c.Body...) + } + + return nil +} + +// AddMention adds one or more provided user mentions to the associated Card +// along with a new TextBlock element. The Text field for the new TextBlock +// element is updated with the Mention Text. +// +// If specified, the new TextBlock element is inserted as the first element in +// the Card body. This effectively creates a dedicated TextBlock that acts as +// a "lead-in" or "announcement block" for other elements in the Card. If +// false, the newly created TextBlock is appended to the Card, effectively +// creating a "CC" list commonly found at the end of an email message. +// +// An error is returned if specified Mention values fail validation. +func (c *Card) AddMention(prepend bool, mentions ...Mention) error { + textBlock := Element{ + Type: TypeElementTextBlock, + + // The goal is to prevent the Mention.Text from extending off of the + // Card, out of sight. + Wrap: true, + } + + // Whether the mention text is prepended or appended doesn't matter since + // the TextBlock element we are adding is empty. Likewise, the separator + // chosen doesn't really matter either as there isn't any existing text + // that we need to separate from the mention text. + // + // NOTE: WE rely on this function to apply validation of user mention + // values instead of duplicating that logic here. + err := AddMention(c, &textBlock, true, defaultMentionTextSeparator, mentions...) + if err != nil { + return err + } + + switch prepend { + case true: + c.Body = append([]Element{textBlock}, c.Body...) + case false: + c.Body = append(c.Body, textBlock) + } + + return nil +} + +// AddElement adds one or more provided Elements to the Body of the associated +// Card. If specified, the Element values are prepended to the Card Body (as a +// contiguous set retaining current order), otherwise appended to the Card +// Body. +// +// An error is returned if specified Element values fail validation. +func (c *Card) AddElement(prepend bool, elements ...Element) error { + if len(elements) == 0 { + return fmt.Errorf( + "received empty collection of elements: %w", + ErrMissingValue, + ) + } + + // Validate first before adding to Card Body. + for _, element := range elements { + if err := element.Validate(); err != nil { + return err + } + } + + switch prepend { + case true: + c.Body = append(elements, c.Body...) + case false: + c.Body = append(c.Body, elements...) + } + + return nil +} + +// AddAction adds one or more provided Actions to the associated Card. If +// specified, the Action values are prepended to the Card (as a collection +// retaining current order), otherwise appended. +// +// NOTE: The max display limit for a Card's actions array has been observed to +// be a fixed value for web/desktop app and a matching value as an initial +// display limit for mobile app with the option to expand remaining actions in +// a list. +// +// This value is recorded in this package as "TeamsActionsDisplayLimit". +// +// Consider adding Action values to one or more ActionSet elements as needed +// and include within the Card.Body directly or within a Container to +// workaround this limit. +// +// An error is returned if specified Action values fail validation. +func (c *Card) AddAction(prepend bool, actions ...Action) error { + if len(actions) == 0 { + return fmt.Errorf( + "received empty collection of actions: %w", + ErrMissingValue, + ) + } + + for _, action := range actions { + if err := action.Validate(); err != nil { + return err + } + } + + switch prepend { + case true: + c.Actions = append(actions, c.Actions...) + case false: + c.Actions = append(c.Actions, actions...) + } + + return nil +} + +// GetElement searches all Element values attached to the Card for the +// specified ID (case sensitive). If found, a pointer to the Element is +// returned, otherwise an error is returned. +func (c *Card) GetElement(id string) (*Element, error) { + if id == "" { + return nil, fmt.Errorf( + "empty ID value specified: %w", + ErrMissingValue, + ) + } + + for _, element := range c.Body { + if element.ID == id { + return &element, nil + } + + // If the Element is a Container, we need to evaluate its collection + // of Elements. + for _, item := range element.Items { + if item.ID == id { + return &element, nil + } + } + } + + return nil, fmt.Errorf( + "unable to retrieve element id: %w", + ErrValueNotFound, + ) +} + +// AddFactSet adds one or more provided FactSet elements to the Body of the +// associated Card. If specified, the FactSet values are prepended to the Card +// Body (as a contiguous set retaining current order), otherwise appended to +// the Card Body. +// +// An error is returned if specified FactSet values fail validation. +// +// TODO: Is this needed? Should we even have a separate FactSet type that is +// so difficult to work with? +func (c *Card) AddFactSet(prepend bool, factsets ...FactSet) error { + if len(factsets) == 0 { + return fmt.Errorf( + "received empty collection of factsets: %w", + ErrMissingValue, + ) + } + + // Convert to base Element type + factsetElements := make([]Element, 0, len(factsets)) + for _, factset := range factsets { + element := Element(factset) + factsetElements = append(factsetElements, element) + } + + // Validate first before adding to Card Body. + for _, element := range factsetElements { + if err := element.Validate(); err != nil { + return err + } + } + + switch prepend { + case true: + c.Body = append(factsetElements, c.Body...) + case false: + c.Body = append(c.Body, factsetElements...) + } + + return nil +} + +// SetFullWidth enables full width display for the Card. +func (c *Card) SetFullWidth() { + c.MSTeams.Width = MSTeamsWidthFull +} + +// NewMention uses the given display name and ID to create a user Mention +// value for inclusion in a Card. An error is returned if provided values are +// insufficient to create the user mention. +func NewMention(displayName string, id string) (Mention, error) { + switch { + case displayName == "": + return Mention{}, fmt.Errorf( + "required name argument is empty: %w", + ErrMissingValue, + ) + + case id == "": + return Mention{}, fmt.Errorf( + "required id argument is empty: %w", + ErrMissingValue, + ) + + default: + + // Build mention. + mention := Mention{ + Type: TypeMention, + Text: fmt.Sprintf(MentionTextFormatTemplate, displayName), + Mentioned: Mentioned{ + ID: id, + Name: displayName, + }, + } + + return mention, nil + } +} + +// AddMention adds one or more provided user mentions to the specified Card. +// The Text field for the specified TextBlock element is updated with the +// Mention Text. If specified, the Mention Text is prepended, otherwise +// appended. If specified, a custom separator is used between the Mention Text +// and the TextBlock Text field, otherwise the default separator is used. +// +// NOTE: This function "registers" the specified Mention values with the Card +// and updates the specified textBlock element, however the caller is +// responsible for ensuring that the specified textBlock element is added to +// the Card. +// +// An error is returned if specified Mention values fail validation, or one of +// Card or Element pointers are null. +func AddMention(card *Card, textBlock *Element, prependText bool, separator string, mentions ...Mention) error { + if card == nil { + return fmt.Errorf( + "specified pointer to Card is nil: %w", + ErrMissingValue, + ) + } + + if textBlock == nil { + return fmt.Errorf( + "specified pointer to TextBlock element is nil: %w", + ErrMissingValue, + ) + } + + if textBlock.Type != TypeElementTextBlock { + return fmt.Errorf( + "invalid element type %q; expected %q: %w", + textBlock.Type, + TypeElementTextBlock, + ErrInvalidType, + ) + } + + if len(mentions) == 0 { + return fmt.Errorf( + "received empty collection of mentions: %w", + ErrMissingValue, + ) + } + + // Validate all user mentions before modifying Card or Element. + for _, mention := range mentions { + if err := mention.Validate(); err != nil { + return err + } + } + + if separator == "" { + separator = defaultMentionTextSeparator + } + + mentionsText := make([]string, 0, len(mentions)) + + // Record user mentions in the Card and collect all required user mention + // text values. + for _, mention := range mentions { + mentionsText = append(mentionsText, mention.Text) + card.MSTeams.Entities = append(card.MSTeams.Entities, mention) + } + + // Update TextBlock element text with required user mention text string. + switch prependText { + case true: + textBlock.Text = strings.Join(mentionsText, " ") + separator + textBlock.Text + case false: + textBlock.Text = textBlock.Text + separator + strings.Join(mentionsText, " ") + } + + // The original text may have been sufficiently short to not be truncated, + // but once we add the user mention text it is more likely that truncation + // could occur. Indicate that the text should be wrapped to avoid this. + textBlock.Wrap = true + + return nil +} + +// NewMentionMessage creates a new simple Message. Using the given message +// text, displayName and ID, a user Mention is also created and added to the +// new Message. An error is returned if provided values are insufficient to +// create the user mention. +func NewMentionMessage(displayName string, id string, msgText string) (*Message, error) { + msg := Message{ + Type: TypeMessage, + } + + // Rely on function to apply validation instead of duplicating it here. + mentionCard, err := NewMentionCard(displayName, id, msgText) + if err != nil { + return nil, err + } + + if err := msg.Attach(mentionCard); err != nil { + return nil, err + } + + return &msg, nil +} + +// NewMentionCard creates a new Card with user Mention using the given +// displayName, ID and message text. An error is returned if provided values +// are insufficient to create the user mention. +func NewMentionCard(displayName string, id string, msgText string) (Card, error) { + if msgText == "" { + return Card{}, fmt.Errorf( + "required msgText argument is empty: %w", + ErrMissingValue, + ) + } + + // Build mention. + mention, err := NewMention(displayName, id) + if err != nil { + return Card{}, err + } + + // Create basic card. + textCard, err := NewTextBlockCard(msgText, "", true) + if err != nil { + return Card{}, err + } + + // Update the text block so that it contains the mention text string + // (required) and user-specified message text string. Use the mention + // text as a "greeting" or lead-in for the user-specified message + // text. + textCard.Body[0].Text = mention.Text + + " " + textCard.Body[0].Text + + textCard.MSTeams.Entities = append( + textCard.MSTeams.Entities, + mention, + ) + + return textCard, nil +} + +// NewMessageFromCard is a helper function for creating a new Message based +// off of an existing Card value. +func NewMessageFromCard(card Card) (*Message, error) { + msg := Message{ + Type: TypeMessage, + } + + if err := msg.Attach(card); err != nil { + return nil, err + } + + return &msg, nil +} + +// NewContainer creates an empty Container. +func NewContainer() Container { + container := Container{ + Type: TypeElementContainer, + } + + return container +} + +// NewHiddenContainer creates an empty Container whose initial state is +// set as hidden from view. +func NewHiddenContainer() Container { + visible := false + container := Container{ + Type: TypeElementContainer, + Visible: &visible, + } + + return container +} + +// NewColumn creates an empty Column. +func NewColumn() Column { + column := Column{ + Type: TypeColumn, + } + + return column +} + +// NewColumnSet creates an empty Element of type ColumnSet. +func NewColumnSet() Element { + columnSet := Element{ + Type: TypeElementColumnSet, + } + + return columnSet +} + +// NewActionSet creates an empty ActionSet. +// +// TODO: Should we create a type alias for ActionSet, or keep it as a "base" +// Element type? +func NewActionSet() Element { + actionSet := Element{ + Type: TypeElementActionSet, + } + + return actionSet +} + +// NewTextBlock creates a new TextBlock element using the optional user +// specified Text. If specified, text wrapping is enabled. +func NewTextBlock(text string, wrap bool) Element { + textBlock := Element{ + Type: TypeElementTextBlock, + Wrap: wrap, + Text: text, + } + + return textBlock +} + +// NewHiddenTextBlock creates a new TextBlock element using the optional user +// specified Text. If specified, text wrapping is enabled. +// +// The new TextBlock is explicitly hidden from view. To view this Element, the +// caller should set an ID value and then allow toggling visibility by +// referencing this TextBlock's ID from a TargetElement associated with a +// ToggleVisibility Action. +func NewHiddenTextBlock(text string, wrap bool) Element { + isVisible := false + textBlock := Element{ + Type: TypeElementTextBlock, + Wrap: wrap, + Text: text, + Visible: &isVisible, + } + + return textBlock +} + +// NewTitleTextBlock uses the specified text to create a new TextBlock +// formatted as a "header" or "title" element. If specified, the TextBlock has +// text wrapping enabled. The effect is meant to emulate the visual effects of +// setting a MessageCard.Title field. +func NewTitleTextBlock(title string, wrap bool) Element { + return Element{ + Type: TypeElementTextBlock, + Wrap: wrap, + Text: title, + Style: TextBlockStyleHeading, + Size: SizeLarge, + Weight: WeightBolder, + } +} + +// NewTableCellsWithTextBlock accepts a collection of items that can be converted +// to string values and returns a collection of TableCells, each populated +// with a single TextBlock containing one of the given items. +// +// Example usage: +// +// vals := []int{1, 2, 3} +// items := make([]interface{}, len(vals)) +// +// for i := range vals { +// items[i] = vals[i] +// } +// +// tableCells := NewTextBlockTableCells(items) +func NewTableCellsWithTextBlock(items []interface{}) (TableCells, error) { + if len(items) == 0 { + return TableCells{}, fmt.Errorf("no data provided: %w", ErrMissingValue) + } + + cells := make(TableCells, len(items)) + for i, item := range items { + switch { + // If an input item is nil, insert an empty table cell in its place. + case item == nil: + cell := TableCell{ + Type: TypeTableCell, + } + cells[i] = cell + default: + block := Element{ + Type: TypeElementTextBlock, + Text: fmt.Sprintf("%v", item), + } + cell := TableCell{ + Type: TypeTableCell, + Items: []*Element{&block}, + } + cells[i] = cell + } + } + + return cells, nil +} + +// NewTableRowFromCells accepts a collection of TableCell values and returns a +// TableRow populated with those TableCells. +func NewTableRowFromCells(cells ...TableCell) (TableRow, error) { + if len(cells) == 0 { + return TableRow{}, fmt.Errorf("no data provided: %w", ErrMissingValue) + } + + if err := TableCells(cells).Validate(); err != nil { + return TableRow{}, err + } + + row := TableRow{ + Type: TypeTableRow, + Cells: cells, + } + + return row, nil +} + +// NewTable creates an empty Element of Table type. +func NewTable() Element { + table := Element{ + Type: TypeElementTable, + } + + return table +} + +// NewTableCellFromElement accepts an Element value and returns a TableCell +// populated with that Element. +func NewTableCellFromElement(element Element) (TableCell, error) { + if err := element.Validate(); err != nil { + return TableCell{}, err + } + + cell := TableCell{ + Type: TypeTableCell, + Items: []*Element{&element}, + } + + return cell, nil +} + +// NewTableCellFromElements accepts a collection of Element values and returns +// a TableCell populated with those Elements. +func NewTableCellFromElements(elements ...Element) (TableCell, error) { + if len(elements) == 0 { + return TableCell{}, fmt.Errorf("no data provided: %w", ErrMissingValue) + } + + if err := Elements(elements).Validate(); err != nil { + return TableCell{}, err + } + + cellItems := make([]*Element, len(elements)) + for i := range elements { + cellItems[i] = &elements[i] + } + + cell := TableCell{ + Type: TypeTableCell, + Items: cellItems, + } + + return cell, nil +} + +// NewTableWithGridFromTableCells accepts a collection of TableCell values and +// the number of cells that should be inserted per table row. Header values +// are not inserted. +func NewTableWithGridFromTableCells(cells []TableCell, perRow int) (Element, error) { + switch { + case len(cells) == 0: + return Element{}, fmt.Errorf("no data provided: %w", ErrMissingValue) + + case perRow < 0: + return Element{}, fmt.Errorf("invalid per row value %d provided", perRow) + } + + if err := TableCells(cells).Validate(); err != nil { + return Element{}, err + } + + neededRows := func() int { + // d := float64(len(cells)) / float64(perRow) + // return int(math.Ceil(d)) + + d := len(cells) / perRow + + // Round up if the per row count doesn't divide evenly into the number + // of cells. This will leave us with a ragged, but valid number of + // cells per row. + if len(cells)%perRow > 0 { + d++ + } + return d + } + + table := Element{ + Type: TypeElementTable, + GridStyle: ContainerStyleAccent, + ShowGridLines: func() *bool { hasGridLines := true; return &hasGridLines }(), + FirstRowAsHeaders: func() *bool { hasHeaders := false; return &hasHeaders }(), + } + + // Add columns to table. + for i := 0; i < perRow; i++ { + c := Column{ + Type: TypeTableColumnDefinition, + Width: 1, + HorizontalCellContentAlignment: HorizontalAlignmentCenter, + VerticalCellContentAlignment: VerticalAlignmentCenter, + } + table.Columns = append(table.Columns, c) + } + + tableRows := make(TableRows, 0, neededRows()) + + // cellsChan := make(chan TableCell) + // go func() { + // for _, cell := range cells { + // cellsChan <- cell + // } + // close(cellsChan) + // }() + // + // for i := 0; i < neededRows(); i++ { + // tableCells := make([]TableCell, 0, perRow) + // for j := 0; j < perRow; j++ { + // cell := <-cellsChan + // tableCells = append(tableCells, cell) + // } + // + // tableRow := TableRow{ + // Type: TypeTableRow, + // Cells: tableCells, + // } + // + // tableRows = append(tableRows, tableRow) + // } + + // Opt for non-channel/non-goroutine implementation. + var cellCtr int + for i := 0; i < neededRows(); i++ { + tableCells := make([]TableCell, 0, perRow) + for j := 0; j < perRow; j++ { + cell := cells[cellCtr] + cellCtr++ + + tableCells = append(tableCells, cell) + } + + tableRow := TableRow{ + Type: TypeTableRow, + Cells: tableCells, + } + + tableRows = append(tableRows, tableRow) + } + + table.Rows = tableRows + + return table, nil +} + +// NewTableFromTableCells accepts a multidimensional collection of TableCell +// values, the number of columns that the table should have, a boolean value +// indicating whether the first row should be treated as a header row and +// another boolean value indicating whether grid lines should be displayed for +// the table. +// +// If the specified number of columns is zero then the number of columns will +// be calculated using the number of values in the first row. +// +// The outer slice is the collection of rows and the inner slice is the +// collection of values. The number of cells per row is determined by the +// number of cell values in that row. If a collection of values for a row is +// empty, an empty row is inserted into the generated table. +func NewTableFromTableCells(cells [][]TableCell, numColumns int, firstRowIsHeaders bool, showGridLines bool) (Element, error) { + if len(cells) == 0 { + return Element{}, fmt.Errorf("no data provided: %w", ErrMissingValue) + } + + for _, row := range cells { + if err := TableCells(row).Validate(); err != nil { + return Element{}, err + } + } + + neededRows := len(cells) + neededColumns := func() int { + switch { + case numColumns == 0: + return len(cells[0]) + default: + return numColumns + } + } + + table := Element{ + Type: TypeElementTable, + GridStyle: ContainerStyleAccent, + ShowGridLines: &showGridLines, + FirstRowAsHeaders: &firstRowIsHeaders, + } + + // Add columns to table equal to the number of values in the first row. + for i := 0; i < neededColumns(); i++ { + c := Column{ + Type: TypeTableColumnDefinition, + Width: 1, + HorizontalCellContentAlignment: HorizontalAlignmentCenter, + VerticalCellContentAlignment: VerticalAlignmentCenter, + } + table.Columns = append(table.Columns, c) + } + + tableRows := make(TableRows, 0, neededRows) + for _, row := range cells { + var tableRow TableRow + // If our input row is empty, insert a cell with empty TextBlock in + // its place. + switch { + case len(row) == 0: + block := Element{ + Type: TypeElementTextBlock, + Text: "", + } + cell := TableCell{ + Type: TypeTableCell, + Items: []*Element{&block}, + } + tableRow = TableRow{ + Type: TypeTableRow, + Cells: []TableCell{cell}, + } + default: + tableRow = TableRow{ + Type: TypeTableRow, + Cells: row, + } + } + + tableRows = append(tableRows, tableRow) + } + + table.Rows = tableRows + + return table, nil +} + +// NewFactSet creates an empty FactSet. +func NewFactSet() FactSet { + factSet := FactSet{ + Type: TypeElementFactSet, + } + + return factSet +} + +// AddFact adds one or many Fact values to a FactSet. An error is returned if +// the Fact fails validation or if AddFact is called on an unsupported Element +// type. +func (fs *FactSet) AddFact(facts ...Fact) error { + // Fail early if called on the wrong Element type. + if fs.Type != TypeElementFactSet { + return fmt.Errorf( + "unsupported element type %s; expected %s: %w", + fs.Type, + TypeElementFactSet, + ErrInvalidType, + ) + } + + if len(facts) == 0 { + return fmt.Errorf( + "received empty collection of facts: %w", + ErrMissingValue, + ) + } + + // Validate all Fact values before adding them to the collection. + for _, fact := range facts { + if err := fact.Validate(); err != nil { + return err + } + } + + fs.Facts = append(fs.Facts, facts...) + + return nil +} + +// HasMentionText asserts that a supported Element type contains the required +// Mention text string necessary to link a user mention to a specific Element. +func (e Element) HasMentionText(m Mention) bool { + switch { + case e.Type == TypeElementTextBlock: + if strings.Contains(e.Text, m.Text) { + return true + } + return false + + case e.Type == TypeElementFactSet: + for _, fact := range e.Facts { + if strings.Contains(fact.Title, m.Text) || + strings.Contains(fact.Value, m.Text) { + + return true + } + } + return false + + default: + return false + } +} + +// AddTableRow adds one or many TableRow values to an Element of Table type. +// An error is returned if a TableRow value fails validation or if AddRow is +// called on any Element type other than a Table. +func (e *Element) AddTableRow(rows ...TableRow) error { + if e.Type != TypeElementTable { + return fmt.Errorf( + "unsupported element type %s; expected %s: %w", + e.Type, + TypeElementTable, + ErrInvalidType, + ) + } + + if len(rows) == 0 { + return fmt.Errorf("no data provided: %w", ErrMissingValue) + } + + e.Rows = append(e.Rows, rows...) + + return nil +} + +// NewActionOpenURL creates a new Action.OpenURL value using the provided URL +// and title. An error is returned if invalid values are supplied. +func NewActionOpenURL(url string, title string) (Action, error) { + // Accept the user-specified values as-is, use Validate() method to do the + // heavy lifting. + action := Action{ + Type: TypeActionOpenURL, + Title: title, + URL: url, + } + + err := action.Validate() + if err != nil { + return Action{}, err + } + + return action, nil +} + +// NewActionToggleVisibility creates a new Action.ToggleVisibility value using +// the (optionally) provided title text. +// +// NOTE: The caller is responsible for adding required TargetElement values to +// meet validation requirements. +func NewActionToggleVisibility(title string) Action { + return Action{ + Type: TypeActionToggleVisibility, + Title: title, + } +} + +// NewActionSetsFromActions creates a new ActionSet for every +// TeamsActionsDisplayLimit count of Actions given. An error is returned if +// the specified Actions do not pass validation. +func NewActionSetsFromActions(actions ...Action) ([]Element, error) { + if len(actions) == 0 { + return nil, fmt.Errorf( + "received empty collection of actions to create ActionSet: %w", + ErrMissingValue, + ) + } + + for _, action := range actions { + if err := action.Validate(); err != nil { + return nil, err + } + } + + // Create a new ActionSet for every TeamsActionsDisplayLimit count of + // Actions given. + actionSetsNeeded := int(math.Ceil(float64(len(actions)) / float64(TeamsActionsDisplayLimit))) + actionSets := make([]Element, 0, actionSetsNeeded) + + stride := TeamsActionsDisplayLimit + for i := 0; i < len(actions); i += stride { + // Ensure that we don't stride past the end of the actions slice. + if stride > len(actions)-i { + stride = len(actions) - i + } + + actionSetItems := actions[i : i+stride] + actionSet := Element{ + Type: TypeElementActionSet, + Actions: actionSetItems, + } + + actionSets = append(actionSets, actionSet) + } + + return actionSets, nil +} + +// AddElement adds the given Element to the collection of Element values in +// the container. If specified, the Element is inserted at the beginning of +// the collection, otherwise appended to the end. +func (c *Container) AddElement(prepend bool, element Element) error { + if err := element.Validate(); err != nil { + return err + } + + switch prepend { + case true: + c.Items = append([]Element{element}, c.Items...) + case false: + c.Items = append(c.Items, element) + } + + return nil +} + +// AddAction adds one or more provided Action values to the associated +// Container as one or more new ActionSets. The number of actions in each +// newly created ActionSet is limited to the number specified by +// TeamsActionsDisplayLimit. +// +// If specified, the newly created ActionSets are inserted before other +// Elements in the Container, otherwise appended. +// +// If adding an action to be used when the Container is tapped or selected use +// AddSelectAction() instead. +// +// An error is returned if specified Action values fail validation. +func (c *Container) AddAction(prepend bool, actions ...Action) error { + // Rely on function to apply validation instead of duplicating it here. + actionSets, err := NewActionSetsFromActions(actions...) + if err != nil { + return err + } + + switch prepend { + case true: + c.Items = append(actionSets, c.Items...) + case false: + c.Items = append(c.Items, actionSets...) + } + + return nil +} + +// AddSelectAction adds a given Action or ISelectAction value to the +// associated Container. This action will be invoked when the Container is +// tapped or selected. +// +// An error is returned if the given Action or ISelectAction value fails +// validation or if a value other than an Action or ISelectAction is provided. +func (c *Container) AddSelectAction(action interface{}) error { + switch v := action.(type) { + case Action: + // Perform manual conversion to the supported type. + selectAction := ISelectAction{ + Type: v.Type, + ID: v.ID, + Title: v.Title, + URL: v.URL, + Fallback: v.Fallback, + } + + // Don't touch the new TargetElements field unless the provided Action + // has specified values. + if len(v.TargetElements) > 0 { + selectAction.TargetElements = append( + selectAction.TargetElements, + v.TargetElements..., + ) + } + + c.SelectAction = &selectAction + + case ISelectAction: + c.SelectAction = &v + + // unsupported value provided + default: + return fmt.Errorf( + "error: unsupported value provided; "+ + " only Action or ISelectAction values are supported: %w", + ErrInvalidFieldValue, + ) + } + + return nil +} + +// AddContainer adds the given Container Element to the collection of Element +// values for the Card. If specified, the Container Element is inserted at the +// beginning of the collection, otherwise appended to the end. +func (c *Card) AddContainer(prepend bool, container Container) error { + element := Element(container) + + if err := element.Validate(); err != nil { + return err + } + + switch prepend { + case true: + c.Body = append([]Element{element}, c.Body...) + case false: + c.Body = append(c.Body, element) + } + + return nil +} + +// NewCodeBlock creates a new CodeBlock element with snippet, language, and +// optional firstLine. This is an MSTeams extension element. +// +// Supported languages include: +// +// - Bash +// - C +// - C# +// - C++ +// - CSS +// - DOS +// - Go +// - GraphQL +// - HTML +// - Java +// - JavaScript +// - JSON +// - Perl +// - PHP +// - PlainText +// - PowerShell +// - Python +// - SQL +// - TypeScript +// - Verilog +// - VHDL +// - Visual Basic +// - XML +// +// See +// https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format +// for additional languages that may be supported. +func NewCodeBlock(snippet string, language string, firstLine int) Element { + codeBlock := Element{ + Type: TypeElementMSTeamsCodeBlock, + CodeSnippet: snippet, + Language: language, + StartLineNumber: firstLine, + } + return codeBlock +} + +// cardBodyHasMention indicates whether an Adaptive Card body contains all +// specified Mention values. For every user mention, we require at least one +// match in an applicable Element in the Card Body. +func cardBodyHasMention(body []Element, mentions []Mention) bool { + // If the card body is empty, it cannot contain the required Mention values. + if body == nil { + return false + } + + elementsHaveMention := func(elements []Element, m Mention) bool { + for _, element := range elements { + if element.HasMentionText(m) { + return true + } + } + return false + } + + for _, mention := range mentions { + if !elementsHaveMention(body, mention) { + return false + } + } + + return true +} + +// assertHeightAlignmentFieldsSetWhenRequired asserts verticalContentAlignment +// is set when minHeight is set; while both are optional fields, both have to +// be set when the other is. +func assertHeightAlignmentFieldsSetWhenRequired(minHeight string, verticalContentAlignment string) error { + if minHeight != "" && verticalContentAlignment == "" { + return fmt.Errorf( + "field MinHeight is set, VerticalContentAlignment is not;"+ + " field VerticalContentAlignment is only optional when MinHeight"+ + " is not set: %w", + ErrMissingValue, + ) + } + + return nil +} + +// assertCardBodyHasMention asserts that if there are recorded user mentions, +// then Mention.Text is contained (substring match) within an applicable field +// of a supported Element of the Card Body. +// +// At present, this includes the Text field of a TextBlock Element or +// the Title or Value fields of a Fact from a FactSet. +// +// https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format#mention-support-within-adaptive-cards +func assertCardBodyHasMention(elements []Element, mentions []Mention) error { + // User mentions recorded, but no elements in Card Body to potentially + // contain required text string. + if len(mentions) > 0 && len(elements) == 0 { + return fmt.Errorf( + "user mention text not found in empty Card Body: %w", + ErrMissingValue, + ) + } + + // For every user mention, we require at least one match in an applicable + // Element in the Card Body. + if len(mentions) > 0 && !cardBodyHasMention(elements, mentions) { + return fmt.Errorf( + "user mention text not found in elements of Card Body: %w", + ErrMissingValue, + ) + } + + return nil +} + +func assertColumnWidthValidValues(c Column) error { + switch v := c.Width.(type) { + // Nothing to see here. + case nil: + + // Assert specific fixed keyword values, empty string or valid pixel + // width; all other values are invalid. + case string: + v = strings.TrimSpace(v) + + switch { + case v == ColumnWidthAuto: + case v == ColumnWidthStretch: + default: + if err := assertValidPixelSizeOrEmptyValue(v); err != nil { + return err + } + } + + // Number representing relative width of the column. + case int: + + // Unsupported value. + default: + return fmt.Errorf( + "invalid pixel width %q; "+ + "expected one of keywords %q, int value (e.g., %d) "+ + "or specific pixel width (e.g., %s): %w", + v, + strings.Join([]string{ + ColumnWidthAuto, + ColumnWidthStretch, + }, ","), + 1, + PixelSizeExample, + ErrInvalidFieldValue, + ) + } + + return nil +} + +func assertTableColumnDefinitionWidthValidValues(tcd TableColumnDefinition) error { + switch v := tcd.Width.(type) { + // Nothing to see here. + case nil: + + // Assert valid pixel width or empty string; all other values are invalid. + case string: + if err := assertValidPixelSizeOrEmptyValue(v); err != nil { + return err + } + + // Number representing relative width of the column. + case int: + + // Unsupported value. + default: + return fmt.Errorf( + "invalid pixel width %q; "+ + "expected int value (e.g., %d) "+ + "or specific pixel width (e.g., %s): %w", + v, + 1, + PixelSizeExample, + ErrInvalidFieldValue, + ) + } + + return nil +} + +func assertValidPixelSizeOrEmptyValue(val string) error { + val = strings.TrimSpace(val) + + // An empty string is a special case and is permitted to honor "optional" + // field value requirement. + if val == "" { + return nil + } + + matched, _ := regexp.MatchString(PixelSizeRegex, val) + + if !matched { + return fmt.Errorf( + "invalid pixel width %q; expected value in format %s: %w", + val, + PixelSizeExample, + ErrInvalidFieldValue, + ) + } + + // TODO: Apply validation to ensure that 0 is not given as a pixel size? + + return nil +} + +func assertValidVersionFieldValue(val string) error { + switch { + case strings.TrimSpace(val) == "": + return fmt.Errorf( + "required field Version is empty for top-level Card: %w", + ErrMissingValue, + ) + default: + // Assert that Version value can be converted to the expected format. + versionNum, err := strconv.ParseFloat(val, 64) + if err != nil { + return fmt.Errorf( + "value %q incompatible with Version field: %w", + val, + ErrInvalidFieldValue, + ) + } + + // This is a high confidence validation failure. + if versionNum < AdaptiveCardMinVersion { + return fmt.Errorf( + "unsupported version %q;"+ + " expected minimum value of %0.1f: %w", + val, + AdaptiveCardMinVersion, + ErrInvalidFieldValue, + ) + } + + // This is *NOT* a high confidence validation failure; it is likely + // that Microsoft Teams will gain support for future versions of the + // Adaptive Card greater than the current recorded max configured + // schema version. Because the max value constant is subject to fall + // out of sync (at least briefly), this is a risky assertion to make. + // + // if versionNum < AdaptiveCardMinVersion || versionNum > AdaptiveCardMaxVersion { + // return fmt.Errorf( + // "unsupported version %q;"+ + // " expected value between %0.1f and %0.1f: %w", + // tc.Version, + // AdaptiveCardMinVersion, + // AdaptiveCardMaxVersion, + // ErrInvalidFieldValue, + // ) + // } + } + + return nil +} diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/doc.go b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/doc.go new file mode 100644 index 0000000..1ad6ee1 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/doc.go @@ -0,0 +1,31 @@ +// Copyright 2022 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +/* +Package adaptivecard provides support for generating Microsoft Teams messages +using the Adaptive Card format. + +See the provided examples in this repo, the Godoc generated documentation at +https://pkg.go.dev/github.com/atc0005/go-teams-notify/v2 and the following +resources for more information: + + - https://adaptivecards.io/explorer + - https://docs.microsoft.com/en-us/adaptive-cards/ + - https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/getting-started + - https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features + - https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model + - https://docs.microsoft.com/en-us/adaptive-cards/getting-started/bots + - https://docs.microsoft.com/en-us/adaptive-cards/resources/principles + - https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format + - https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format#mention-support-within-adaptive-cards + - https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#support-for-adaptive-cards + - https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/what-are-cards + - https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using + - https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#send-adaptive-cards-using-an-incoming-webhook + - https://stackoverflow.com/questions/50753072/microsoft-teams-webhook-generating-400-for-adaptive-card +*/ +package adaptivecard diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/format.go b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/format.go new file mode 100644 index 0000000..2a8c696 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/format.go @@ -0,0 +1,73 @@ +// Copyright 2022 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package adaptivecard + +import "strings" + +// - https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format#newlines-for-adaptive-cards +// - https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features + +// Newline and break statement patterns stripped out of text content sent to +// Microsoft Teams (by request). +const ( + // CR LF \r\n (windows) + windowsEOLActual = "\r\n" + windowsEOLEscaped = `\r\n` + + // CF \r (mac) + macEOLActual = "\r" + macEOLEscaped = `\r` + + // LF \n (unix) + unixEOLActual = "\n" + unixEOLEscaped = `\n` + + // Used with MessageCard format to emulate newlines, incompatible with + // Adaptive Card format (displays as literal values). + breakStatement = "
" +) + +// ConvertEOL converts \r\n (windows), \r (mac) and \n (unix) into \n\n. +// +// This function is intended for processing text for use in an Adaptive Card +// TextBlock element. The goal is to provide spacing in rendered text display +// comparable to native display. +// +// NOTE: There are known discrepancies in the way that Microsoft Teams renders +// text in desktop, web and mobile, so even with using this helper function +// some differences are to be expected. +// +// - https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format#newlines-for-adaptive-cards +// - https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features +func ConvertEOL(s string) string { + s = strings.ReplaceAll(s, windowsEOLEscaped, unixEOLActual+unixEOLActual) + s = strings.ReplaceAll(s, windowsEOLActual, unixEOLActual+unixEOLActual) + s = strings.ReplaceAll(s, macEOLActual, unixEOLActual+unixEOLActual) + s = strings.ReplaceAll(s, macEOLEscaped, unixEOLActual+unixEOLActual) + s = strings.ReplaceAll(s, unixEOLEscaped, unixEOLActual+unixEOLActual) + + return s +} + +// ConvertBreakToEOL converts
statements into \n\n to provide comparable +// spacing in Adaptive Card TextBlock elements. +// +// This function is intended for processing text for use in an Adaptive Card +// TextBlock element. The goal is to provide spacing in rendered text display +// comparable to native display. +// +// The primary use case of this function is to process text that was +// previously formatted in preparation for use in a MessageCard; the +// MessageCard format supports
statements for text spacing/formatting +// where the Adaptive Card format does not. +// +// - https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format#newlines-for-adaptive-cards +// - https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features +func ConvertBreakToEOL(s string) string { + return strings.ReplaceAll(s, breakStatement, unixEOLActual+unixEOLActual) +} diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/getters.go b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/getters.go new file mode 100644 index 0000000..dbf56a6 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/getters.go @@ -0,0 +1,340 @@ +// Copyright 2022 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package adaptivecard + +// supportedElementTypes returns a list of valid types for an Adaptive Card +// element used in Microsoft Teams messages. This list is intended to be used +// for validation and display purposes. +func supportedElementTypes() []string { + // TODO: Confirm whether all types are supported. + // + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#support-for-adaptive-cards + // https://adaptivecards.io/explorer/AdaptiveCard.html + return []string{ + TypeElementActionSet, + TypeElementColumnSet, + TypeElementContainer, + TypeElementFactSet, + TypeElementImage, + TypeElementImageSet, + TypeElementInputChoiceSet, + TypeElementInputDate, + TypeElementInputNumber, + TypeElementInputText, + TypeElementInputTime, + TypeElementInputToggle, + TypeElementMedia, // Introduced in version 1.1 (TODO: Is this supported in Teams message?) + TypeElementRichTextBlock, + TypeElementTable, // Introduced in version 1.5 + TypeElementTextBlock, + TypeElementTextRun, + TypeElementMSTeamsCodeBlock, + } +} + +// supportedSizeValues returns a list of valid Size values for applicable +// Element types. This list is intended to be used for validation and display +// purposes. +func supportedSizeValues() []string { + // https://adaptivecards.io/explorer/TextBlock.html + return []string{ + SizeSmall, + SizeDefault, + SizeMedium, + SizeLarge, + SizeExtraLarge, + } +} + +// supportedWeightValues returns a list of valid Weight values for text in +// applicable Element types. This list is intended to be used for validation +// and display purposes. +func supportedWeightValues() []string { + // https://adaptivecards.io/explorer/TextBlock.html + return []string{ + WeightBolder, + WeightLighter, + WeightDefault, + } +} + +// supportedColorValues returns a list of valid Color values for text in +// applicable Element types. This list is intended to be used for validation +// and display purposes. +func supportedColorValues() []string { + // https://adaptivecards.io/explorer/TextBlock.html + return []string{ + ColorDefault, + ColorDark, + ColorLight, + ColorAccent, + ColorGood, + ColorWarning, + ColorAttention, + } +} + +// supportedSpacingValues returns a list of valid Spacing values for Element +// types. This list is intended to be used for validation and display +// purposes. +func supportedSpacingValues() []string { + // https://adaptivecards.io/explorer/TextBlock.html + return []string{ + SpacingDefault, + SpacingNone, + SpacingSmall, + SpacingMedium, + SpacingLarge, + SpacingExtraLarge, + SpacingPadding, + } +} + +// supportedHorizontalAlignmentValues returns a list of valid horizontal +// alignment values for supported container and text types. This list is +// intended to be used for validation and display purposes. +func supportedHorizontalAlignmentValues() []string { + // https://adaptivecards.io/explorer/Table.html + // https://adaptivecards.io/explorer/TextBlock.html + // https://adaptivecards.io/schemas/adaptive-card.json + return []string{ + HorizontalAlignmentLeft, + HorizontalAlignmentCenter, + HorizontalAlignmentRight, + } +} + +// supportedVerticalAlignmentValues returns a list of valid vertical content +// alignment values for supported container types. This list is intended to be +// used for validation and display purposes. +func supportedVerticalContentAlignmentValues() []string { + // https://adaptivecards.io/explorer/Table.html + // https://adaptivecards.io/schemas/adaptive-card.json + return []string{ + VerticalAlignmentTop, + VerticalAlignmentCenter, + VerticalAlignmentBottom, + } +} + +// supportedActionValues accepts a value indicating the maximum Adaptive Card +// schema version supported and returns a list of valid Action types. This +// list is intended to be used for validation and display purposes. +// +// NOTE: See also the supportedISelectActionValues() function. See ref links +// for unsupported Action types. +func supportedActionValues(version float64) []string { + // https://adaptivecards.io/explorer/AdaptiveCard.html + // https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference + supportedValues := []string{ + TypeActionOpenURL, + TypeActionShowCard, + TypeActionToggleVisibility, + + // Action.Submit is not supported for Adaptive Cards in Incoming + // Webhooks. + // + // TypeActionSubmit, + } + + // Version 1.4 is when Action.Execute was introduced. + // + // Per this doc: + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference + // + // the "Action.Execute" action is supported: + // + // "For Adaptive Cards in Incoming Webhooks, all native Adaptive Card + // schema elements, except Action.Submit, are fully supported. The + // supported actions are Action.OpenURL, Action.ShowCard, + // Action.ToggleVisibility, and Action.Execute." + if version >= ActionExecuteMinCardVersionRequired { + supportedValues = append(supportedValues, TypeActionExecute) + } + + return supportedValues +} + +// supportedISelectActionValues accepts a value indicating the maximum +// Adaptive Card schema version supported and returns a list of valid +// ISelectAction types. This list is intended to be used for validation and +// display purposes. +// +// NOTE: See also the supportedActionValues() function. See ref links for +// unsupported Action types. +func supportedISelectActionValues(version float64) []string { + // https://adaptivecards.io/explorer/Column.html + // https://adaptivecards.io/explorer/TableCell.html + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference + supportedValues := []string{ + TypeActionOpenURL, + TypeActionToggleVisibility, + + // Action.Submit is not supported for Adaptive Cards in Incoming + // Webhooks. + // + // TypeActionSubmit, + + // Action.ShowCard is not a supported Action for selectAction fields + // (ISelectAction). + // + // TypeActionShowCard, + } + + // Version 1.4 is when Action.Execute was introduced. + // + // Per this doc: + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference + // + // the "Action.Execute" action is supported: + // + // "For Adaptive Cards in Incoming Webhooks, all native Adaptive Card + // schema elements, except Action.Submit, are fully supported. The + // supported actions are Action.OpenURL, Action.ShowCard, + // Action.ToggleVisibility, and Action.Execute." + if version >= ActionExecuteMinCardVersionRequired { + supportedValues = append(supportedValues, TypeActionExecute) + } + + return supportedValues +} + +// supportedAttachmentLayoutValues returns a list of valid AttachmentLayout +// values for Message type. This list is intended to be used for validation +// and display purposes. +// +// NOTE: See also the supportedActionValues() function. +func supportedAttachmentLayoutValues() []string { + return []string{ + AttachmentLayoutList, + AttachmentLayoutCarousel, + } +} + +// supportedStyleValues returns a list of valid Style field values for the +// specified element type. This list is intended to be used for validation and +// display purposes. +func supportedStyleValues(elementType string) []string { + switch elementType { + case TypeElementColumnSet: + return supportedContainerStyleValues() + case TypeElementContainer: + return supportedContainerStyleValues() + case TypeElementTable: + return supportedContainerStyleValues() + case TypeElementImage: + return supportedImageStyleValues() + case TypeElementInputChoiceSet: + return supportedChoiceInputStyleValues() + case TypeElementInputText: + return supportedTextInputStyleValues() + case TypeElementTextBlock: + return supportedTextBlockStyleValues() + + // Unsupported element types are indicated by an explicit empty list. + default: + return []string{} + } +} + +// supportedImageStyleValues returns a list of valid Style field values for +// the Image element type. This list is intended to be used for validation and +// display purposes. +func supportedImageStyleValues() []string { + return []string{ + ImageStyleDefault, + ImageStylePerson, + } +} + +// supportedChoiceInputStyleValues returns a list of valid Style field values +// for ChoiceInput related element types (e.g., Input.ChoiceSet) This list is +// intended to be used for validation and display purposes. +func supportedChoiceInputStyleValues() []string { + return []string{ + ChoiceInputStyleCompact, + ChoiceInputStyleExpanded, + ChoiceInputStyleFiltered, + } +} + +// supportedTextInputStyleValues returns a list of valid Style field values +// for TextInput related element types (e.g., Input.Text) This list is +// intended to be used for validation and display purposes. +func supportedTextInputStyleValues() []string { + return []string{ + TextInputStyleText, + TextInputStyleTel, + TextInputStyleURL, + TextInputStyleEmail, + TextInputStylePassword, + } +} + +// supportedTextBlockStyleValues returns a list of valid Style field values +// for the TextBlock element type. This list is intended to be used for +// validation and display purposes. +func supportedTextBlockStyleValues() []string { + return []string{ + TextBlockStyleDefault, + TextBlockStyleHeading, + } +} + +// supportedContainerStyleValues returns a list of valid Style field values +// for Container types (e.g., Column, ColumnSet, Container). This list is +// intended to be used for validation and display purposes. +func supportedContainerStyleValues() []string { + return []string{ + ContainerStyleDefault, + ContainerStyleEmphasis, + ContainerStyleGood, + ContainerStyleAttention, + ContainerStyleWarning, + ContainerStyleAccent, + } +} + +// supportedMSTeamsWidthValues returns a list of valid Width field values for +// MSTeams type. This list is intended to be used for validation and display +// purposes. +func supportedMSTeamsWidthValues() []string { + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format#full-width-adaptive-card + return []string{ + MSTeamsWidthFull, + } +} + +// supportedActionFallbackValues accepts a value indicating the maximum +// Adaptive Card schema version supported and returns a list of valid Action +// Fallback types. This list is intended to be used for validation and display +// purposes. +func supportedActionFallbackValues(version float64) []string { + // https://adaptivecards.io/explorer/Action.OpenUrl.html + // https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference + supportedValues := supportedActionValues(version) + supportedValues = append(supportedValues, TypeFallbackOptionDrop) + + return supportedValues +} + +// supportedISelectActionFallbackValues accepts a value indicating the maximum +// Adaptive Card schema version supported and returns a list of valid +// ISelectAction Fallback types. This list is intended to be used for +// validation and display purposes. +func supportedISelectActionFallbackValues(version float64) []string { + // https://adaptivecards.io/explorer/Action.OpenUrl.html + // https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference + supportedValues := supportedISelectActionValues(version) + supportedValues = append(supportedValues, TypeFallbackOptionDrop) + + return supportedValues +} diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/nullstring.go b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/nullstring.go new file mode 100644 index 0000000..57e6449 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/nullstring.go @@ -0,0 +1,63 @@ +// Copyright 2022 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package adaptivecard + +import ( + "encoding/json" + "strings" +) + +// Credit: +// +// These resources were used while developing the json.Marshaler and +// json.Unmarshler interface implementations used in this file: +// +// https://stackoverflow.com/questions/31048557/assigning-null-to-json-fields-instead-of-empty-strings +// https://stackoverflow.com/questions/25087960/json-unmarshal-time-that-isnt-in-rfc-3339-format/ + +// Add an "implements assertion" to fail the build if the json.Unmarshaler +// implementation isn't correct. +// +// This resolves the unparam linter error: +// (*NullString).UnmarshalJSON - result 0 (error) is always nil (unparam) +// +// https://github.com/mvdan/unparam/issues/52 +var _ json.Unmarshaler = (*NullString)(nil) + +// Perform similar "implements assertion" for the json.Marshaler interface. +var _ json.Marshaler = (*NullString)(nil) + +// NullString represents a string value used in component fields that may +// potentially be null in the input JSON feed. +type NullString string + +// MarshalJSON implements the json.Marshaler interface. This compliments the +// custom Unmarshaler implementation to handle potentially null component +// description field value. +func (ns NullString) MarshalJSON() ([]byte, error) { + if len(string(ns)) == 0 { + return []byte("null"), nil + } + + // NOTE: If we fail to convert the type, an infinite loop will occur. + return json.Marshal(string(ns)) +} + +// UnmarshalJSON implements the json.Unmarshaler interface to handle +// potentially null component description field value. +func (ns *NullString) UnmarshalJSON(data []byte) error { + str := string(data) + if str == "null" { + *ns = "" + return nil + } + + *ns = NullString(strings.Trim(str, "\"")) + + return nil +} diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/doc.go b/vendor/github.com/atc0005/go-teams-notify/v2/doc.go new file mode 100644 index 0000000..9d7d204 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/doc.go @@ -0,0 +1,39 @@ +// Copyright 2021 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +/* +Package goteamsnotify is used to send messages to a Microsoft Teams channel. + +# Project Home + +See our GitHub repo (https://github.com/atc0005/go-teams-notify) for the +latest code, to file an issue or submit improvements for review and potential +inclusion into the project. + +# Purpose + +Send messages to a Microsoft Teams channel. + +# Features + + - Submit messages to Microsoft Teams consisting of one or more sections, + Facts (key/value pairs), Actions or images (hosted externally) + - Support for MessageCard and Adaptive Card messages + - Support for Actions, allowing users to take quick actions within Microsoft + Teams + - Support for user mentions + - Configurable validation + - Configurable timeouts + - Configurable retry support + - Support for overriding the default http.Client + - Support for overriding the default project-specific user agent + +# Usage + +See our main README for supported settings and examples. +*/ +package goteamsnotify diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/format.go b/vendor/github.com/atc0005/go-teams-notify/v2/format.go new file mode 100644 index 0000000..d393711 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/format.go @@ -0,0 +1,253 @@ +// Copyright 2021 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package goteamsnotify + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +///////////////////////////////////////////////////////////////////////// +// NOTE: The contents of this file are deprecated. See the Deprecated +// indicators in this file for intended replacements. +// +// Please submit a bug report if you find exported code in this file which +// does *not* already have a replacement elsewhere in this library. +///////////////////////////////////////////////////////////////////////// + +// Newline patterns stripped out of text content sent to Microsoft Teams (by +// request) and replacement break value used to provide equivalent formatting +// for MessageCard payloads in Microsoft Teams. +const ( + + // CR LF \r\n (windows) + windowsEOLActual = "\r\n" + windowsEOLEscaped = `\r\n` + + // CF \r (mac) + macEOLActual = "\r" + macEOLEscaped = `\r` + + // LF \n (unix) + unixEOLActual = "\n" + unixEOLEscaped = `\n` + + // Used by Teams to separate lines + breakStatement = "
" +) + +// Even though Microsoft Teams doesn't show the additional newlines, +// https://messagecardplayground.azurewebsites.net/ DOES show the results +// as a formatted code block. Including the newlines now is an attempt at +// "future proofing" the codeblock support in MessageCard values sent to +// Microsoft Teams. +const ( + + // msTeamsCodeBlockSubmissionPrefix is the prefix appended to text input + // to indicate that the text should be displayed as a codeblock by + // Microsoft Teams for MessageCard payloads. + msTeamsCodeBlockSubmissionPrefix string = "\n```\n" + // msTeamsCodeBlockSubmissionPrefix string = "```" + + // msTeamsCodeBlockSubmissionSuffix is the suffix appended to text input + // to indicate that the text should be displayed as a codeblock by + // Microsoft Teams for MessageCard payloads. + msTeamsCodeBlockSubmissionSuffix string = "```\n" + // msTeamsCodeBlockSubmissionSuffix string = "```" + + // msTeamsCodeSnippetSubmissionPrefix is the prefix appended to text input + // to indicate that the text should be displayed as a code formatted + // string of text by Microsoft Teams for MessageCard payloads. + msTeamsCodeSnippetSubmissionPrefix string = "`" + + // msTeamsCodeSnippetSubmissionSuffix is the suffix appended to text input + // to indicate that the text should be displayed as a code formatted + // string of text by Microsoft Teams for MessageCard payloads. + msTeamsCodeSnippetSubmissionSuffix string = "`" +) + +// TryToFormatAsCodeBlock acts as a wrapper for FormatAsCodeBlock. If an +// error is encountered in the FormatAsCodeBlock function, this function will +// return the original string, otherwise if no errors occur the newly formatted +// string will be returned. +// +// This function is intended for processing text intended for a MessageCard. +// Using this helper function for text intended for an Adaptive Card is +// unsupported and unlikely to produce the desired results. +// +// Deprecated: use messagecard.TryToFormatAsCodeBlock instead. +func TryToFormatAsCodeBlock(input string) string { + result, err := FormatAsCodeBlock(input) + if err != nil { + logger.Printf("TryToFormatAsCodeBlock: error occurred when calling FormatAsCodeBlock: %v\n", err) + logger.Println("TryToFormatAsCodeBlock: returning original string") + return input + } + + logger.Println("TryToFormatAsCodeBlock: no errors occurred when calling FormatAsCodeBlock") + return result +} + +// TryToFormatAsCodeSnippet acts as a wrapper for FormatAsCodeSnippet. If an +// error is encountered in the FormatAsCodeSnippet function, this function +// will return the original string, otherwise if no errors occur the newly +// formatted string will be returned. +// +// This function is intended for processing text intended for a MessageCard. +// Using this helper function for text intended for an Adaptive Card is +// unsupported and unlikely to produce the desired results. +// +// Deprecated: use messagecard.TryToFormatAsCodeSnippet instead. +func TryToFormatAsCodeSnippet(input string) string { + result, err := FormatAsCodeSnippet(input) + if err != nil { + logger.Printf("TryToFormatAsCodeSnippet: error occurred when calling FormatAsCodeBlock: %v\n", err) + logger.Println("TryToFormatAsCodeSnippet: returning original string") + return input + } + + logger.Println("TryToFormatAsCodeSnippet: no errors occurred when calling FormatAsCodeSnippet") + return result +} + +// FormatAsCodeBlock accepts an arbitrary string, quoted or not, and calls a +// helper function which attempts to format as a valid Markdown code block for +// submission to Microsoft Teams. +// +// This function is intended for processing text intended for a MessageCard. +// Using this helper function for text intended for an Adaptive Card is +// unsupported and unlikely to produce the desired results. +// +// Deprecated: use messagecard.FormatAsCodeBlock instead. +func FormatAsCodeBlock(input string) (string, error) { + if input == "" { + return "", errors.New("received empty string, refusing to format") + } + + result, err := formatAsCode( + input, + msTeamsCodeBlockSubmissionPrefix, + msTeamsCodeBlockSubmissionSuffix, + ) + + return result, err +} + +// FormatAsCodeSnippet accepts an arbitrary string, quoted or not, and calls a +// helper function which attempts to format as a single-line valid Markdown +// code snippet for submission to Microsoft Teams. +// +// This function is intended for processing text intended for a MessageCard. +// Using this helper function for text intended for an Adaptive Card is +// unsupported and unlikely to produce the desired results. +// +// Deprecated: use messagecard.FormatAsCodeSnippet instead. +func FormatAsCodeSnippet(input string) (string, error) { + if input == "" { + return "", errors.New("received empty string, refusing to format") + } + + result, err := formatAsCode( + input, + msTeamsCodeSnippetSubmissionPrefix, + msTeamsCodeSnippetSubmissionSuffix, + ) + + return result, err +} + +// formatAsCode is a helper function which accepts an arbitrary string, quoted +// or not, a desired prefix and a suffix for the string and attempts to format +// as a valid Markdown formatted code sample for submission to Microsoft +// Teams. This helper function is intended for processing text intended for a +// MessageCard. +// +// Using this helper function for text intended for an Adaptive Card is +// unsupported and unlikely to produce the desired results. +func formatAsCode(input string, prefix string, suffix string) (string, error) { + var err error + var byteSlice []byte + + switch { + // required; protects against slice out of range panics + case input == "": + return "", errors.New("received empty string, refusing to format as code block") + + // If the input string is already valid JSON, don't double-encode and + // escape the content + case json.Valid([]byte(input)): + logger.Printf("formatAsCode: input string already valid JSON; input: %+v", input) + logger.Printf("formatAsCode: Calling json.RawMessage([]byte(input)); input: %+v", input) + + // FIXME: Is json.RawMessage() really needed if the input string is + // *already* JSON? https://golang.org/pkg/encoding/json/#RawMessage + // seems to imply a different use case. + byteSlice = json.RawMessage([]byte(input)) + // + // From light testing, it appears to not be necessary: + // + // logger.Printf("formatAsCode: Skipping json.RawMessage, converting string directly to byte slice; input: %+v", input) + // byteSlice = []byte(input) + + default: + logger.Printf("formatAsCode: input string not valid JSON; input: %+v", input) + logger.Printf("formatAsCode: Calling json.Marshal(input); input: %+v", input) + byteSlice, err = json.Marshal(input) + if err != nil { + return "", err + } + } + + logger.Println("formatAsCode: byteSlice as string:", string(byteSlice)) + + var prettyJSON bytes.Buffer + + logger.Println("formatAsCode: calling json.Indent") + err = json.Indent(&prettyJSON, byteSlice, "", "\t") + if err != nil { + return "", err + } + formattedJSON := prettyJSON.String() + + logger.Println("formatAsCode: Formatted JSON:", formattedJSON) + + // handle both cases: where the formatted JSON string was not wrapped with + // double-quotes and when it was + codeContentForSubmission := prefix + strings.Trim(formattedJSON, "\"") + suffix + + logger.Printf("formatAsCode: formatted JSON as-is:\n%s\n\n", formattedJSON) + logger.Printf("formatAsCode: formatted JSON wrapped with code prefix/suffix: \n%s\n\n", codeContentForSubmission) + + // err should be nil if everything worked as expected + return codeContentForSubmission, err +} + +// ConvertEOLToBreak converts \r\n (windows), \r (mac) and \n (unix) into
+// statements. +// +// This function is intended for processing text intended for a MessageCard. +// Using this helper function for text intended for an Adaptive Card is +// unsupported and unlikely to produce the desired results. +// +// Deprecated: use messagecard.ConvertEOLToBreak instead. +func ConvertEOLToBreak(s string) string { + logger.Printf("ConvertEOLToBreak: Received %#v", s) + + s = strings.ReplaceAll(s, windowsEOLActual, breakStatement) + s = strings.ReplaceAll(s, windowsEOLEscaped, breakStatement) + s = strings.ReplaceAll(s, macEOLActual, breakStatement) + s = strings.ReplaceAll(s, macEOLEscaped, breakStatement) + s = strings.ReplaceAll(s, unixEOLActual, breakStatement) + s = strings.ReplaceAll(s, unixEOLEscaped, breakStatement) + + logger.Printf("ConvertEOLToBreak: Returning %#v", s) + + return s +} diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/internal/validator/doc.go b/vendor/github.com/atc0005/go-teams-notify/v2/internal/validator/doc.go new file mode 100644 index 0000000..08e5801 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/internal/validator/doc.go @@ -0,0 +1,18 @@ +// Copyright 2022 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +/* +Package validator provides logic to assist with validation tasks. The logic is +designed so that each subsequent validation step short-circuits after the +first validation failure; only the first validation failure is reported. + +Credit to Fabrizio Milo for sharing the original implementation: + +- https://stackoverflow.com/a/23960293/903870 +- https://github.com/Mistobaan +*/ +package validator diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/internal/validator/validator.go b/vendor/github.com/atc0005/go-teams-notify/v2/internal/validator/validator.go new file mode 100644 index 0000000..0ed6f97 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/internal/validator/validator.go @@ -0,0 +1,495 @@ +// Copyright 2022 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package validator + +import ( + "fmt" + + goteamsnotify "github.com/atc0005/go-teams-notify/v2" +) + +// Validater is the interface shared by all supported types which provide +// validation of their fields. +type Validater interface { + Validate() error +} + +// Validator is used to perform validation of given values. Each validation +// method for this type is designed to exit early in order to preserve any +// prior validation failure. If a previous validation check failure occurred, +// the most recent validation check result will +// +// After performing a validation check, the caller is responsible for checking +// the result to determine if further validation checks should be performed. +// +// Heavily inspired by: https://stackoverflow.com/a/23960293/903870 +type Validator struct { + err error +} + +// hasNilValues is a helper function used to determine whether any items in +// the given collection are nil. +func hasNilValues(items []interface{}) bool { + for _, item := range items { + if item == nil { + return true + } + } + return false +} + +// SelfValidate asserts that each given item can self-validate. +// +// A true value is returned if the validation step passed. A false value is +// returned if this or a prior validation step failed. +func (v *Validator) SelfValidate(items ...Validater) bool { + if v.err != nil { + return false + } + for _, item := range items { + if err := item.Validate(); err != nil { + v.err = err + return false + } + } + return true +} + +// SelfValidateIfXEqualsY asserts that each given item can self-validate if +// value x is equal to y. +// +// A true value is returned if the validation step passed. A false value is +// returned false if this or a prior validation step failed. +func (v *Validator) SelfValidateIfXEqualsY(x string, y string, items ...Validater) bool { + if v.err != nil { + return false + } + + if x == y { + v.SelfValidate(items...) + } + + return true +} + +// FieldHasSpecificValue asserts that fieldVal is reqVal. fieldValDesc +// describes the field value being validated (e.g., "Type") and typeDesc +// describes the specific struct or value type whose field we are validating +// (e.g., "Element"). +// +// A true value is returned if the validation step passed. A false value is +// returned if this or a prior validation step failed. +func (v *Validator) FieldHasSpecificValue( + fieldVal string, + fieldValDesc string, + reqVal string, + typeDesc string, + baseErr error, +) bool { + + switch { + case v.err != nil: + return false + + case fieldVal != reqVal: + v.err = fmt.Errorf( + // "required %s is empty for %s: %w", + // "invalid card type %q; expected %q: %w", + "invalid %s %q for %s; expected %q: %w", + fieldValDesc, + fieldVal, + typeDesc, + reqVal, + baseErr, + ) + return false + + default: + return true + } +} + +// FieldHasSpecificValueIfFieldNotEmpty asserts that fieldVal is reqVal unless +// fieldVal is empty. fieldValDesc describes the field value being validated +// (e.g., "Type") and typeDesc describes the specific struct or value type +// whose field we are validating (e.g., "Element"). +// +// A true value is returned if the validation step passed. A false value is +// returned if this or a prior validation step failed. +func (v *Validator) FieldHasSpecificValueIfFieldNotEmpty( + fieldVal string, + fieldValDesc string, + reqVal string, + typeDesc string, + baseErr error, +) bool { + + switch { + case v.err != nil: + return false + + case fieldVal != "": + return v.FieldHasSpecificValue( + fieldVal, + fieldValDesc, + reqVal, + typeDesc, + baseErr, + ) + + default: + return true + } +} + +// NotEmptyValue asserts that fieldVal is not empty. fieldValDesc describes +// the field value being validated (e.g., "Type") and typeDesc describes the +// specific struct or value type whose field we are validating (e.g., +// "Element"). +// +// A true value is returned if the validation step passed. A false value is +// returned if this or a prior validation step failed. +func (v *Validator) NotEmptyValue(fieldVal string, fieldValDesc string, typeDesc string, baseErr error) bool { + if v.err != nil { + return false + } + if fieldVal == "" { + v.err = fmt.Errorf( + "required %s is empty for %s: %w", + fieldValDesc, + typeDesc, + baseErr, + ) + return false + } + return true +} + +// InList reports whether fieldVal is in validVals. fieldValDesc describes the +// field value being validated (e.g., "Type") and typeDesc describes the +// specific struct or value type whose field we are validating (e.g., +// "Element"). +// +// A true value is returned if fieldVal is is in validVals. +// +// A false value is returned if any of: +// - a prior validation step failed +// - fieldVal is empty +// - fieldVal is non-empty and not in validVals +// - the validVals collection to compare against is empty +func (v *Validator) InList(fieldVal string, fieldValDesc string, typeDesc string, validVals []string, baseErr error) bool { + switch { + case v.err != nil: + return false + + case fieldVal == "": + return false + + case !goteamsnotify.InList(fieldVal, validVals, false): + switch { + case len(validVals) == 0 && baseErr != nil: + v.err = fmt.Errorf( + "invalid %s %q for %s; empty list of valid values: %w", + fieldValDesc, + fieldVal, + typeDesc, + baseErr, + ) + case len(validVals) == 0: + v.err = fmt.Errorf( + "invalid %s %q for %s; no known valid values", + fieldValDesc, + fieldVal, + typeDesc, + ) + case baseErr != nil: + v.err = fmt.Errorf( + "invalid %s %q for %s; expected one of %v: %w", + fieldValDesc, + fieldVal, + typeDesc, + validVals, + baseErr, + ) + default: + v.err = fmt.Errorf( + "invalid %s %q for %s; expected one of %v", + fieldValDesc, + fieldVal, + typeDesc, + validVals, + ) + } + + return false + + // Validation is good. + default: + return true + } +} + +// InListIfFieldValNotEmpty reports whether fieldVal is in validVals if +// fieldVal is not empty. fieldValDesc describes the field value being +// validated (e.g., "Type") and typeDesc describes the specific struct or +// value type whose field we are validating (e.g., "Element"). +// +// A true value is returned if fieldVal is empty or is in validVals. +// +// A false value is returned if any of: +// - a prior validation step failed +// - fieldVal is not empty and is not in validVals +// - the validVals collection to compare against is empty +func (v *Validator) InListIfFieldValNotEmpty(fieldVal string, fieldValDesc string, typeDesc string, validVals []string, baseErr error) bool { + switch { + case v.err != nil: + return false + + case fieldVal != "" && !goteamsnotify.InList(fieldVal, validVals, false): + switch { + case len(validVals) == 0 && baseErr != nil: + v.err = fmt.Errorf( + "invalid %s %q for %s; empty list of valid values: %w", + fieldValDesc, + fieldVal, + typeDesc, + baseErr, + ) + case len(validVals) == 0: + v.err = fmt.Errorf( + "invalid %s %q for %s; no known valid values", + fieldValDesc, + fieldVal, + typeDesc, + ) + case baseErr != nil: + v.err = fmt.Errorf( + "invalid %s %q for %s; expected one of %v: %w", + fieldValDesc, + fieldVal, + typeDesc, + validVals, + baseErr, + ) + default: + v.err = fmt.Errorf( + "invalid %s %q for %s; expected one of %v", + fieldValDesc, + fieldVal, + typeDesc, + validVals, + ) + } + + return false + + // Validation is good. + default: + return true + } +} + +// FieldInListIfTypeValIs reports whether fieldVal is in validVals if fieldVal +// is not empty. fieldValDesc describes the field value being validated (e.g., +// "Type") and typeDesc describes the specific struct or value type whose +// field we are validating (e.g., "Element"). +// +// A true value is returned if fieldVal is empty or is in validVals. A false +// value is returned if a prior validation step failed or if fieldVal is not +// empty and is not in validVals. +// func (v *Validator) FieldInListIfTypeValIs( +// fieldVal string, +// fieldDesc string, +// typeVal string, +// typeDesc string, +// validVals []string, +// baseErr error, +// ) bool { +// switch { +// case v.err != nil: +// return false +// +// case fieldVal != "" && !goteamsnotify.InList(fieldVal, validVals, false): +// v.err = fmt.Errorf( +// "invalid %s %q for %s; expected one of %v", +// fieldValDesc, +// fieldVal, +// typeDesc, +// validVals, +// ) +// +// if baseErr != nil { +// v.err = fmt.Errorf( +// "invalid %s %q for %s; expected one of %v: %w", +// fieldValDesc, +// fieldVal, +// typeDesc, +// validVals, +// baseErr, +// ) +// } +// +// return false +// +// // Validation is good. +// default: +// return true +// } +// } + +// NotEmptyCollection asserts that the specified items collection is not +// empty. fieldValueDesc describes the field for this collection being +// validated (e.g., "Facts") and typeDesc describes the specific struct or +// value type whose field we are validating (e.g., "Element"). +// +// A true value is returned if the collection is not empty. A false value is +// returned if a prior validation step failed or if the items collection is +// empty. +func (v *Validator) NotEmptyCollection(fieldValueDesc string, typeDesc string, baseErr error, items ...interface{}) bool { + if v.err != nil { + return false + } + if len(items) == 0 { + switch { + case baseErr != nil: + v.err = fmt.Errorf( + "required %s collection is empty for %s: %w", + fieldValueDesc, + typeDesc, + baseErr, + ) + default: + v.err = fmt.Errorf( + "required %s collection is empty for %s", + fieldValueDesc, + typeDesc, + ) + } + + return false + } + return true +} + +// NoNilValuesInCollection asserts that the specified items collection does +// not contain any nil values. fieldValueDesc describes the field for this +// collection being validated (e.g., "Facts") and typeDesc describes the +// specific struct or value type whose field we are validating (e.g., +// "Element"). +// +// A true value is returned if the collection does not contain any nil values +// (even if the collection itself has no values). A false value is returned if +// a prior validation step failed or if any items in the collection are nil. +func (v *Validator) NoNilValuesInCollection(fieldValueDesc string, typeDesc string, baseErr error, items ...interface{}) bool { + if v.err != nil { + return false + } + + switch { + case hasNilValues(items): + switch { + case baseErr != nil: + v.err = fmt.Errorf( + "required %s collection contains nil values for %s: %w", + fieldValueDesc, + typeDesc, + baseErr, + ) + default: + v.err = fmt.Errorf( + "required %s collection contains nil values for for %s", + fieldValueDesc, + typeDesc, + ) + } + + return false + + default: + return true + } +} + +// NotEmptyCollectionIfFieldValNotEmpty asserts that the specified items +// collection is not empty if fieldVal is not empty. fieldValueDesc describes +// the field for this collection being validated (e.g., "Facts") and typeDesc +// describes the specific struct or value type whose field we are validating +// (e.g., "Element"). +// +// A true value is returned if the collection is not empty. A false value is +// returned if a prior validation step failed or if the items collection is +// empty. +func (v *Validator) NotEmptyCollectionIfFieldValNotEmpty( + fieldVal string, + fieldValueDesc string, + typeDesc string, + baseErr error, + items ...interface{}, +) bool { + + switch { + case v.err != nil: + return false + + case fieldVal != "" && len(items) == 0: + switch { + case baseErr != nil: + v.err = fmt.Errorf( + "required %s collection is empty for %s: %w", + fieldValueDesc, + typeDesc, + baseErr, + ) + default: + v.err = fmt.Errorf( + "required %s collection is empty for %s", + fieldValueDesc, + typeDesc, + ) + } + + return false + + default: + return true + } +} + +// SuccessfulFuncCall accepts fn, a function that returns an error. fn is +// called in order to determine validation results. +// +// A true value is returned if fn was successful. A false value is returned if +// a prior validation step failed or if fn returned an error. +func (v *Validator) SuccessfulFuncCall(fn func() error) bool { + if v.err != nil { + return false + } + + if err := fn(); err != nil { + v.err = err + return false + } + + return true +} + +// IsValid indicates whether validation checks performed thus far have all +// passed. +func (v *Validator) IsValid() bool { + return v.err != nil +} + +// Error returns the error string from the last recorded validation error. +func (v *Validator) Error() string { + return v.err.Error() +} + +// Err returns the last recorded validation error. +func (v *Validator) Err() error { + return v.err +} diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/messagecard.go b/vendor/github.com/atc0005/go-teams-notify/v2/messagecard.go new file mode 100644 index 0000000..52e0feb --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/messagecard.go @@ -0,0 +1,858 @@ +// Copyright 2020 Enrico Hoffmann +// Copyright 2021 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package goteamsnotify + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "strings" +) + +///////////////////////////////////////////////////////////////////////// +// NOTE: The contents of this file are deprecated. See the Deprecated +// indicators in this file for intended replacements. +// +// Please submit a bug report if you find exported code in this file which +// does *not* already have a replacement elsewhere in this library. +///////////////////////////////////////////////////////////////////////// + +const ( + // PotentialActionOpenURIType is the type that must be used for OpenUri + // potential action. + // + // Deprecated: use messagecard.PotentialActionOpenURIType instead. + PotentialActionOpenURIType = "OpenUri" + + // PotentialActionHTTPPostType is the type that must be used for HttpPOST + // potential action. + // + // Deprecated: use messagecard.PotentialActionHTTPPostType instead. + PotentialActionHTTPPostType = "HttpPOST" + + // PotentialActionActionCardType is the type that must be used for + // ActionCard potential action. + // + // Deprecated: use messagecard.PotentialActionActionCardType instead. + PotentialActionActionCardType = "ActionCard" + + // PotentialActionInvokeAddInCommandType is the type that must be used for + // InvokeAddInCommand potential action. + // + // Deprecated: use messagecard.PotentialActionInvokeAddInCommandType + // instead. + PotentialActionInvokeAddInCommandType = "InvokeAddInCommand" + + // PotentialActionActionCardInputTextInputType is the type that must be + // used for ActionCard TextInput type. + // + // Deprecated: use messagecard.PotentialActionActionCardInputTextInputType + // instead. + PotentialActionActionCardInputTextInputType = "TextInput" + + // PotentialActionActionCardInputDateInputType is the type that must be + // used for ActionCard DateInput type. + // + // Deprecated: use messagecard.PotentialActionActionCardInputDateInputType + // instead. + PotentialActionActionCardInputDateInputType = "DateInput" + + // PotentialActionActionCardInputMultichoiceInput is the type that must be + // used for ActionCard MultichoiceInput type. + // + // Deprecated: use + // messagecard.PotentialActionActionCardInputMultichoiceInputType instead. + PotentialActionActionCardInputMultichoiceInput = "MultichoiceInput" +) + +// PotentialActionMaxSupported is the maximum number of actions allowed in a +// MessageCardPotentialAction collection. +// +// https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#actions +// +// Deprecated: use messagecard.PotentialActionMaxSupported instead. +const PotentialActionMaxSupported = 4 + +// ErrPotentialActionsLimitReached indicates that the maximum supported number +// of potentialAction collection values has been reached for either a +// MessageCard or a MessageCardSection. +// +// Deprecated: use messagecard.ErrPotentialActionsLimitReached instead. +var ErrPotentialActionsLimitReached = errors.New("potential actions collection limit reached") + +// MessageCardPotentialAction represents potential actions an user can do in a +// message card. See [Legacy actionable message card reference > Actions] for +// more information. +// +// Deprecated: use messagecard.PotentialAction instead. +// +// [Legacy actionable message card reference > Actions]: https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#actions +type MessageCardPotentialAction struct { + // Type of the potential action. Can be OpenUri, HttpPOST, ActionCard or + // InvokeAddInCommand. + Type string `json:"@type"` + + // Name property defines the text that will be displayed on screen for the + // action. + Name string `json:"name"` + + // MessageCardPotentialActionOpenURI is a set of options for openUri + // potential action. + MessageCardPotentialActionOpenURI + + // MessageCardPotentialActionHTTPPOST is a set of options for httpPOST + // potential action. + MessageCardPotentialActionHTTPPOST + + // MessageCardPotentialActionActionCard is a set of options for actionCard + // potential action. + MessageCardPotentialActionActionCard + + // MessageCardPotentialActionInvokeAddInCommand is a set of options for + // invokeAddInCommand potential action. + MessageCardPotentialActionInvokeAddInCommand +} + +// MessageCardPotentialActionOpenURI represents a OpenUri potential action. +// +// Deprecated: use messagecard.PotentialActionOpenURI instead. +type MessageCardPotentialActionOpenURI struct { + // Targets is a collection of name/value pairs that defines one URI per + // target operating system. Only used for OpenUri action type. + Targets []MessageCardPotentialActionOpenURITarget `json:"targets,omitempty"` +} + +// MessageCardPotentialActionHTTPPOST represents a HttpPOST potential action. +// +// Deprecated: use messagecard.PotentialActionHTTPPOST instead. +type MessageCardPotentialActionHTTPPOST struct { + // Target defines the URL endpoint of the service that implements the + // action. Only used for HttpPOST action type. + Target string `json:"target,omitempty"` + + // Headers is a collection of MessageCardPotentialActionHeader objects + // representing a set of HTTP headers that will be emitted when sending + // the POST request to the target URL. Only used for HttpPOST action type. + Headers []MessageCardPotentialActionHTTPPOSTHeader `json:"headers,omitempty"` + + // Body is the body of the POST request. Only used for HttpPOST action + // type. + Body string `json:"body,omitempty"` + + // BodyContentType is optional and specifies the MIME type of the body in + // the POST request. Only used for HttpPOST action type. + BodyContentType string `json:"bodyContentType,omitempty"` +} + +// MessageCardPotentialActionActionCard represents an actionCard potential +// action. +// +// Deprecated: use messagecard.PotentialActionActionCard instead. +type MessageCardPotentialActionActionCard struct { + // Inputs is a collection of inputs an user can provide before processing + // the actions. Only used for ActionCard action type. Three types of + // inputs are available: TextInput, DateInput and MultichoiceInput + Inputs []MessageCardPotentialActionActionCardInput `json:"inputs,omitempty"` + + // Actions are the available actions. Only used for ActionCard action + // type. + Actions []MessageCardPotentialActionActionCardAction `json:"actions,omitempty"` +} + +// MessageCardPotentialActionActionCardAction is used for configuring +// ActionCard actions. +// +// Deprecated: use messagecard.PotentialActionActionCardAction +// instead. +type MessageCardPotentialActionActionCardAction struct { + // Type of the action. Can be OpenUri, HttpPOST, ActionCard or + // InvokeAddInCommand. + Type string `json:"@type"` + + // Name property defines the text that will be displayed on screen for the + // action. + Name string `json:"name"` + + // MessageCardPotentialActionOpenURI is used to specify a openUri action + // card's action. + MessageCardPotentialActionOpenURI + + // MessageCardPotentialActionHTTPPOST is used to specify a httpPOST action + // card's action. + MessageCardPotentialActionHTTPPOST +} + +// MessageCardPotentialActionInvokeAddInCommand represents an +// invokeAddInCommand potential action. +// +// Deprecated: use messagecard.PotentialActionInvokeAddInCommand +// instead. +type MessageCardPotentialActionInvokeAddInCommand struct { + // AddInID specifies the add-in ID of the required add-in. Only used for + // InvokeAddInCommand action type. + AddInID string `json:"addInId,omitempty"` + + // DesktopCommandID specifies the ID of the add-in command button that + // opens the required task pane. Only used for InvokeAddInCommand action + // type. + DesktopCommandID string `json:"desktopCommandId,omitempty"` + + // InitializationContext is an optional field which provides developers a + // way to specify any valid JSON object. The value is serialized into a + // string and made available to the add-in when the action is executed. + // This allows the action to pass initialization data to the add-in. Only + // used for InvokeAddInCommand action type. + InitializationContext interface{} `json:"initializationContext,omitempty"` +} + +// MessageCardPotentialActionOpenURITarget is used for OpenUri action type. +// It defines one URI per target operating system. +// +// Deprecated: use messagecard.PotentialActionOpenURITarget +// instead. +type MessageCardPotentialActionOpenURITarget struct { + // OS defines the operating system the target uri refers to. Supported + // operating system values are default, windows, iOS and android. The + // default operating system will in most cases simply open the URI in a + // web browser, regardless of the actual operating system. + OS string `json:"os,omitempty"` + + // URI defines the URI being called. + URI string `json:"uri,omitempty"` +} + +// MessageCardPotentialActionHTTPPOSTHeader defines a HTTP header used for +// HttpPOST action type. +// +// Deprecated: use messagecard.PotentialActionHTTPPOSTHeader +// instead. +type MessageCardPotentialActionHTTPPOSTHeader struct { + // Name is the header name. + Name string `json:"name,omitempty"` + + // Value is the header value. + Value string `json:"value,omitempty"` +} + +// MessageCardPotentialActionActionCardInput represents an ActionCard input. +// +// Deprecated: use messagecard.PotentialActionActionCardInput +// instead. +type MessageCardPotentialActionActionCardInput struct { + // Type of the ActionCard input. + // Must be either TextInput, DateInput or MultichoiceInput + Type string `json:"@type"` + + // ID uniquely identifies the input so it is possible to reference it in + // the URL or body of an HttpPOST action. + ID string `json:"id,omitempty"` + + // Title defines a title for the input. + Title string `json:"title,omitempty"` + + // Value defines the initial value of the input. For multi-choice inputs, + // value must be equal to the value property of one of the input's + // choices. + Value string `json:"value,omitempty"` + + // MessageCardPotentialActionInputMultichoiceInput must be defined for + // MultichoiceInput input type. + MessageCardPotentialActionActionCardInputMultichoiceInput + + // MessageCardPotentialActionInputTextInput must be defined for InputText + // input type. + MessageCardPotentialActionActionCardInputTextInput + + // MessageCardPotentialActionInputDateInput must be defined for DateInput + // input type. + MessageCardPotentialActionActionCardInputDateInput + + // IsRequired indicates whether users are required to type a value before + // they are able to take an action that would take the value of the input + // as a parameter. + IsRequired bool `json:"isRequired,omitempty"` +} + +// MessageCardPotentialActionActionCardInputTextInput represents a TextInput +// input used for potential action. +// +// Deprecated: use messagecard.PotentialActionActionCardInputTextInput +// instead. +type MessageCardPotentialActionActionCardInputTextInput struct { + // MaxLength indicates the maximum number of characters that can be + // entered. + MaxLength int `json:"maxLength,omitempty"` + + // IsMultiline indicates whether the text input should accept multiple + // lines of text. + IsMultiline bool `json:"isMultiline,omitempty"` +} + +// MessageCardPotentialActionActionCardInputMultichoiceInput represents a +// MultichoiceInput input used for potential action. +// +// Deprecated: use messagecard.PotentialActionActionCardInputMultichoiceInput +// instead. +type MessageCardPotentialActionActionCardInputMultichoiceInput struct { + // Choices defines the values that can be selected for the multichoice + // input. + Choices []struct { + Display string `json:"display,omitempty"` + Value string `json:"value,omitempty"` + } `json:"choices,omitempty"` + + // Style defines the style of the input. When IsMultiSelect is false, + // setting the style property to expanded will instruct the host + // application to try and display all choices on the screen, typically + // using a set of radio buttons. + Style string `json:"style,omitempty"` + + // IsMultiSelect indicates whether or not the user can select more than + // one choice. The specified choices will be displayed as a list of + // checkboxes. Default value is false. + IsMultiSelect bool `json:"isMultiSelect,omitempty"` +} + +// MessageCardPotentialActionActionCardInputDateInput represents a DateInput +// input used for potential action. +// +// Deprecated: use messagecard.PotentialActionActionCardInputDateInput +// instead. +type MessageCardPotentialActionActionCardInputDateInput struct { + // IncludeTime indicates whether the date input should allow for the + // selection of a time in addition to the date. + IncludeTime bool `json:"includeTime,omitempty"` +} + +// MessageCardSectionFact represents a section fact entry that is usually +// displayed in a two-column key/value format. +// +// Deprecated: use messagecard.SectionFact instead. +type MessageCardSectionFact struct { + + // Name is the key for an associated value in a key/value pair + Name string `json:"name"` + + // Value is the value for an associated key in a key/value pair + Value string `json:"value"` +} + +// MessageCardSectionImage represents an image as used by the heroImage and +// images properties of a section. +// +// Deprecated: use messagecard.SectionImage instead. +type MessageCardSectionImage struct { + + // Image is the URL to the image. + Image string `json:"image"` + + // Title is a short description of the image. Typically, this description + // is displayed in a tooltip as the user hovers their mouse over the + // image. + Title string `json:"title"` +} + +// MessageCardSection represents a section to include in a message card. +// +// Deprecated: use messagecard.Section instead. +type MessageCardSection struct { + // Title is the title property of a section. This property is displayed + // in a font that stands out, while not as prominent as the card's title. + // It is meant to introduce the section and summarize its content, + // similarly to how the card's title property is meant to summarize the + // whole card. + Title string `json:"title,omitempty"` + + // Text is the section's text property. This property is very similar to + // the text property of the card. It can be used for the same purpose. + Text string `json:"text,omitempty"` + + // ActivityImage is a property used to display a picture associated with + // the subject of a message card. For example, this might be the portrait + // of a person who performed an activity that the message card is + // associated with. + ActivityImage string `json:"activityImage,omitempty"` + + // ActivityTitle is a property used to summarize the activity associated + // with a message card. + ActivityTitle string `json:"activityTitle,omitempty"` + + // ActivitySubtitle is a property used to show brief, but extended + // information about an activity associated with a message card. Examples + // include the date and time the associated activity was taken or the + // handle of a person associated with the activity. + ActivitySubtitle string `json:"activitySubtitle,omitempty"` + + // ActivityText is a property used to provide details about the activity. + // For example, if the message card is used to deliver updates about a + // topic, then this property would be used to hold the bulk of the content + // for the update notification. + ActivityText string `json:"activityText,omitempty"` + + // HeroImage is a property that allows for setting an image as the + // centerpiece of a message card. This property can also be used to add a + // banner to the message card. + // Note: heroImage is not currently supported by Microsoft Teams + // https://stackoverflow.com/a/45389789 + // We use a pointer to this type in order to have the json package + // properly omit this field if not explicitly set. + // https://github.com/golang/go/issues/11939 + // https://stackoverflow.com/questions/18088294/how-to-not-marshal-an-empty-struct-into-json-with-go + // https://stackoverflow.com/questions/33447334/golang-json-marshal-how-to-omit-empty-nested-struct + HeroImage *MessageCardSectionImage `json:"heroImage,omitempty"` + + // Facts is a collection of MessageCardSectionFact values. A section entry + // usually is displayed in a two-column key/value format. + Facts []MessageCardSectionFact `json:"facts,omitempty"` + + // Images is a property that allows for the inclusion of a photo gallery + // inside a section. + // We use a slice of pointers to this type in order to have the json + // package properly omit this field if not explicitly set. + // https://github.com/golang/go/issues/11939 + // https://stackoverflow.com/questions/18088294/how-to-not-marshal-an-empty-struct-into-json-with-go + // https://stackoverflow.com/questions/33447334/golang-json-marshal-how-to-omit-empty-nested-struct + Images []*MessageCardSectionImage `json:"images,omitempty"` + + // PotentialActions is a collection of actions for a MessageCardSection. + // This is separate from the actions collection for the MessageCard. + PotentialActions []*MessageCardPotentialAction `json:"potentialAction,omitempty"` + + // Markdown represents a toggle to enable or disable Markdown formatting. + // By default, all text fields in a card and its sections can be formatted + // using basic Markdown. + Markdown bool `json:"markdown,omitempty"` + + // StartGroup is the section's startGroup property. This property marks + // the start of a logical group of information. Typically, sections with + // startGroup set to true will be visually separated from previous card + // elements. + StartGroup bool `json:"startGroup,omitempty"` +} + +// MessageCard represents a legacy actionable message card used via Office 365 +// or Microsoft Teams connectors. +// +// Deprecated: use messagecard.MessageCard instead. +type MessageCard struct { + // Required; must be set to "MessageCard" + Type string `json:"@type"` + + // Required; must be set to "https://schema.org/extensions" + Context string `json:"@context"` + + // Summary is required if the card does not contain a text property, + // otherwise optional. The summary property is typically displayed in the + // list view in Outlook, as a way to quickly determine what the card is + // all about. Summary appears to only be used when there are sections defined + Summary string `json:"summary,omitempty"` + + // Title is the title property of a card. is meant to be rendered in a + // prominent way, at the very top of the card. Use it to introduce the + // content of the card in such a way users will immediately know what to + // expect. + Title string `json:"title,omitempty"` + + // Text is required if the card does not contain a summary property, + // otherwise optional. The text property is meant to be displayed in a + // normal font below the card's title. Use it to display content, such as + // the description of the entity being referenced, or an abstract of a + // news article. + Text string `json:"text,omitempty"` + + // Specifies a custom brand color for the card. The color will be + // displayed in a non-obtrusive manner. + ThemeColor string `json:"themeColor,omitempty"` + + // ValidateFunc is a validation function that validates a MessageCard + ValidateFunc func() error `json:"-"` + + // Sections is a collection of sections to include in the card. + Sections []*MessageCardSection `json:"sections,omitempty"` + + // PotentialActions is a collection of actions for a MessageCard. + PotentialActions []*MessageCardPotentialAction `json:"potentialAction,omitempty"` + + // payload is a prepared MessageCard in JSON format for submission or + // pretty printing. + payload *bytes.Buffer `json:"-"` +} + +// validatePotentialAction inspects the given *MessageCardPotentialAction +// and returns an error if a value is missing or not known. +func validatePotentialAction(pa *MessageCardPotentialAction) error { + if pa == nil { + return fmt.Errorf("nil MessageCardPotentialAction received") + } + + switch pa.Type { + case PotentialActionOpenURIType, + PotentialActionHTTPPostType, + PotentialActionActionCardType, + PotentialActionInvokeAddInCommandType: + + default: + return fmt.Errorf("unknown type %s for potential action %s", pa.Type, pa.Name) + } + + if pa.Name == "" { + return fmt.Errorf("missing name value for MessageCardPotentialAction") + } + + return nil +} + +// addPotentialAction adds one or many MessageCardPotentialAction values to a +// PotentialActions collection. +func addPotentialAction(collection *[]*MessageCardPotentialAction, actions ...*MessageCardPotentialAction) error { + for _, a := range actions { + logger.Printf("addPotentialAction: MessageCardPotentialAction received: %+v\n", a) + + if err := validatePotentialAction(a); err != nil { + logger.Printf("addPotentialAction: validation failed: %v", err) + + return err + } + + if len(*collection) > PotentialActionMaxSupported { + logger.Printf("addPotentialAction: failed to add potential action: %v", ErrPotentialActionsLimitReached.Error()) + + return fmt.Errorf("func addPotentialAction: failed to add potential action: %w", ErrPotentialActionsLimitReached) + } + + *collection = append(*collection, a) + } + + return nil +} + +// AddSection adds one or many additional MessageCardSection values to a +// MessageCard. Validation is performed to reject invalid values with an error +// message. +// +// Deprecated: use (messagecard.MessageCard).AddSection instead. +func (mc *MessageCard) AddSection(section ...*MessageCardSection) error { + for _, s := range section { + logger.Printf("AddSection: MessageCardSection received: %+v\n", s) + + // bail if a completely nil section provided + if s == nil { + return fmt.Errorf("func AddSection: nil MessageCardSection received") + } + + // Perform validation of all MessageCardSection fields in an effort to + // avoid adding a MessageCardSection with zero value fields. This is + // done to avoid generating an empty sections JSON array since the + // Sections slice for the MessageCard type would technically not be at + // a zero value state. Due to this non-zero value state, the + // encoding/json package would end up including the Sections struct + // field in the output JSON. + // See also https://github.com/golang/go/issues/11939 + switch { + // If any of these cases trigger, skip over the `default` case + // statement and add the section. + case s.Images != nil: + case s.Facts != nil: + case s.HeroImage != nil: + case s.StartGroup: + case s.Markdown: + case s.ActivityText != "": + case s.ActivitySubtitle != "": + case s.ActivityTitle != "": + case s.ActivityImage != "": + case s.Text != "": + case s.Title != "": + + default: + logger.Println("AddSection: No cases matched, all fields assumed to be at zero-value, skipping section") + return fmt.Errorf("all fields found to be at zero-value, skipping section") + } + + logger.Println("AddSection: section contains at least one non-zero value, adding section") + mc.Sections = append(mc.Sections, s) + } + + return nil +} + +// AddPotentialAction adds one or many MessageCardPotentialAction values to a +// PotentialActions collection on a MessageCard. +// +// Deprecated: use (messagecard.MessageCard).AddPotentialAction instead. +func (mc *MessageCard) AddPotentialAction(actions ...*MessageCardPotentialAction) error { + return addPotentialAction(&mc.PotentialActions, actions...) +} + +// Validate validates a MessageCard calling ValidateFunc if defined, +// otherwise, a default validation occurs. +// +// Deprecated: use (messagecard.MessageCard).Validate instead. +func (mc *MessageCard) Validate() error { + if mc.ValidateFunc != nil { + return mc.ValidateFunc() + } + + // Falling back to a default implementation + if (mc.Text == "") && (mc.Summary == "") { + // This scenario results in: + // 400 Bad Request + // Summary or Text is required. + return fmt.Errorf("invalid message card: summary or text field is required") + } + + return nil +} + +// Prepare handles tasks needed to construct a payload from a MessageCard for +// delivery to an endpoint. +// +// Deprecated: use (messagecard.MessageCard).Prepare instead. +func (mc *MessageCard) Prepare() error { + jsonMessage, err := json.Marshal(mc) + if err != nil { + return fmt.Errorf( + "error marshalling MessageCard to JSON: %w", + err, + ) + } + + switch { + case mc.payload == nil: + mc.payload = &bytes.Buffer{} + default: + mc.payload.Reset() + } + + _, err = mc.payload.Write(jsonMessage) + if err != nil { + return fmt.Errorf( + "error updating JSON payload for MessageCard: %w", + err, + ) + } + + return nil +} + +// Payload returns the prepared MessageCard payload. The caller should call +// Prepare() prior to calling this method, results are undefined otherwise. +// +// Deprecated: use (messagecard.MessageCard).Payload instead. +func (mc *MessageCard) Payload() io.Reader { + return mc.payload +} + +// PrettyPrint returns a formatted JSON payload of the MessageCard if the +// Prepare() method has been called, or an empty string otherwise. +// +// Deprecated: use (messagecard.MessageCard).PrettyPrint instead. +func (mc *MessageCard) PrettyPrint() string { + if mc.payload != nil { + var prettyJSON bytes.Buffer + _ = json.Indent(&prettyJSON, mc.payload.Bytes(), "", "\t") + + return prettyJSON.String() + } + + return "" +} + +// AddFact adds one or many additional MessageCardSectionFact values to a +// MessageCardSection. +// +// Deprecated: use (messagecard.Section).AddFact instead. +func (mcs *MessageCardSection) AddFact(fact ...MessageCardSectionFact) error { + for _, f := range fact { + logger.Printf("AddFact: MessageCardSectionFact received: %+v\n", f) + + if f.Name == "" { + return fmt.Errorf("empty Name field received for new fact: %+v", f) + } + + if f.Value == "" { + return fmt.Errorf("empty Value field received for new fact: %+v", f) + } + } + + logger.Println("AddFact: section fact contains at least one non-zero value, adding section fact") + mcs.Facts = append(mcs.Facts, fact...) + + return nil +} + +// AddFactFromKeyValue accepts a key and slice of values and converts them to +// MessageCardSectionFact values. +// +// Deprecated: use (messagecard.Section).AddFactFromKeyValue +// instead. +func (mcs *MessageCardSection) AddFactFromKeyValue(key string, values ...string) error { + // validate arguments + + if key == "" { + return errors.New("empty key received for new fact") + } + + if len(values) < 1 { + return errors.New("no values received for new fact") + } + + fact := MessageCardSectionFact{ + Name: key, + Value: strings.Join(values, ", "), + } + // TODO: Explicitly define or use constructor? + // fact := NewMessageCardSectionFact() + // fact.Name = key + // fact.Value = strings.Join(values, ", ") + + mcs.Facts = append(mcs.Facts, fact) + + // if we made it this far then all should be well + return nil +} + +// AddPotentialAction adds one or many MessageCardPotentialAction values to a +// PotentialActions collection on a MessageCardSection. This is separate from +// the actions collection for the MessageCard. +// +// Deprecated: use (messagecard.Section).AddPotentialAction +// instead. +func (mcs *MessageCardSection) AddPotentialAction(actions ...*MessageCardPotentialAction) error { + return addPotentialAction(&mcs.PotentialActions, actions...) +} + +// AddImage adds an image to a MessageCard section. These images are used to +// provide a photo gallery inside a MessageCard section. +// +// Deprecated: use (messagecard.Section).AddImage instead. +func (mcs *MessageCardSection) AddImage(sectionImage ...MessageCardSectionImage) error { + for i := range sectionImage { + if sectionImage[i].Image == "" { + return fmt.Errorf("cannot add empty image URL") + } + + if sectionImage[i].Title == "" { + return fmt.Errorf("cannot add empty image title") + } + + mcs.Images = append(mcs.Images, §ionImage[i]) + } + + return nil +} + +// AddHeroImageStr adds a Hero Image to a MessageCard section using string +// arguments. This image is used as the centerpiece or banner of a message +// card. +// +// Deprecated: use (messagecard.Section).AddHeroImageStr instead. +func (mcs *MessageCardSection) AddHeroImageStr(imageURL string, imageTitle string) error { + if imageURL == "" { + return fmt.Errorf("cannot add empty hero image URL") + } + + if imageTitle == "" { + return fmt.Errorf("cannot add empty hero image title") + } + + heroImage := MessageCardSectionImage{ + Image: imageURL, + Title: imageTitle, + } + // TODO: Explicitly define or use constructor? + // heroImage := NewMessageCardSectionImage() + // heroImage.Image = imageURL + // heroImage.Title = imageTitle + + mcs.HeroImage = &heroImage + + // our validation checks didn't find any problems + return nil +} + +// AddHeroImage adds a Hero Image to a MessageCard section using a +// MessageCardSectionImage argument. This image is used as the centerpiece or +// banner of a message card. +// +// Deprecated: use (messagecard.Section).AddHeroImage instead. +func (mcs *MessageCardSection) AddHeroImage(heroImage MessageCardSectionImage) error { + if heroImage.Image == "" { + return fmt.Errorf("cannot add empty hero image URL") + } + + if heroImage.Title == "" { + return fmt.Errorf("cannot add empty hero image title") + } + + mcs.HeroImage = &heroImage + + // our validation checks didn't find any problems + return nil +} + +// NewMessageCard creates a new message card with fields required by the +// legacy message card format already predefined. +// +// Deprecated: use messagecard.NewMessageCard instead. +func NewMessageCard() MessageCard { + // define expected values to meet Office 365 Connector card requirements + // https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#card-fields + msgCard := MessageCard{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + } + + return msgCard +} + +// NewMessageCardSection creates an empty message card section. +// +// Deprecated: use messagecard.NewMessageCardSection instead. +func NewMessageCardSection() *MessageCardSection { + msgCardSection := MessageCardSection{} + return &msgCardSection +} + +// NewMessageCardSectionFact creates an empty message card section fact. +// +// Deprecated: use messagecard.NewMessageCardSectionFact instead. +func NewMessageCardSectionFact() MessageCardSectionFact { + msgCardSectionFact := MessageCardSectionFact{} + return msgCardSectionFact +} + +// NewMessageCardSectionImage creates an empty image for use with message card +// section. +// +// Deprecated: use messagecard.NewMessageCardSectionImage instead. +func NewMessageCardSectionImage() MessageCardSectionImage { + msgCardSectionImage := MessageCardSectionImage{} + return msgCardSectionImage +} + +// NewMessageCardPotentialAction creates a new MessageCardPotentialAction +// using the provided potential action type and name. The name value defines +// the text that will be displayed on screen for the action. An error is +// returned if invalid values are supplied. +// +// Deprecated: use messagecard.NewMessageCardPotentialAction instead. +func NewMessageCardPotentialAction(potentialActionType string, name string) (*MessageCardPotentialAction, error) { + pa := MessageCardPotentialAction{ + Type: potentialActionType, + Name: name, + } + + if err := validatePotentialAction(&pa); err != nil { + return nil, err + } + + return &pa, nil +} diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/send.go b/vendor/github.com/atc0005/go-teams-notify/v2/send.go new file mode 100644 index 0000000..86f2cd5 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/send.go @@ -0,0 +1,665 @@ +// Copyright 2020 Enrico Hoffmann +// Copyright 2021 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package goteamsnotify + +import ( + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "regexp" + "strings" + "time" +) + +// logger is a package logger that can be enabled from client code to allow +// logging output from this package when desired/needed for troubleshooting +var logger *log.Logger + +// Known webhook URL prefixes for submitting messages to Microsoft Teams +const ( + WebhookURLOfficecomPrefix = "https://outlook.office.com" + WebhookURLOffice365Prefix = "https://outlook.office365.com" + WebhookURLOrgWebhookPrefix = "https://example.webhook.office.com" +) + +// Known Workflow URL patterns for submitting messages to Microsoft Teams. +const ( + WorkflowURLBaseDomain = `^https:\/\/(?:.*)(:?\.azure-api|logic\.azure)\.(?:com|net)` +) + +// DisableWebhookURLValidation is a special keyword used to indicate to +// validation function(s) that webhook URL validation should be disabled. +// +// Deprecated: prefer using API.SkipWebhookURLValidationOnSend(bool) method instead +const DisableWebhookURLValidation string = "DISABLE_WEBHOOK_URL_VALIDATION" + +// Regular Expression related constants that we can use to validate incoming +// webhook URLs provided by the user. +const ( + + // DefaultWebhookURLValidationPattern is a minimal regex for matching known valid + // webhook URL prefix patterns. + DefaultWebhookURLValidationPattern = `^https:\/\/(?:.*\.webhook|outlook)\.office(?:365)?\.com` + + // Note: The regex allows for capital letters in the GUID patterns. This is + // allowed based on light testing which shows that mixed case works and the + // assumption that since Teams and Office 365 are Microsoft products case + // would be ignored (e.g., Windows, IIS do not consider 'A' and 'a' to be + // different). + // webhookURLRegex = `^https:\/\/(?:.*\.webhook|outlook)\.office(?:365)?\.com\/webhook(?:b2)?\/[-a-zA-Z0-9]{36}@[-a-zA-Z0-9]{36}\/IncomingWebhook\/[-a-zA-Z0-9]{32}\/[-a-zA-Z0-9]{36}$` + + // webhookURLSubURIWebhookPrefix = "webhook" + // webhookURLSubURIWebhookb2Prefix = "webhookb2" + // webhookURLOfficialDocsSampleURI = "a1269812-6d10-44b1-abc5-b84f93580ba0@9e7b80c7-d1eb-4b52-8582-76f921e416d9/IncomingWebhook/3fdd6767bae44ac58e5995547d66a4e4/f332c8d9-3397-4ac5-957b-b8e3fc465a8c" +) + +// ExpectedWebhookURLResponseText represents the expected response text +// provided by the remote webhook endpoint when submitting messages. +const ExpectedWebhookURLResponseText string = "1" + +// DefaultWebhookSendTimeout specifies how long the message operation may take +// before it times out and is cancelled. +const DefaultWebhookSendTimeout = 5 * time.Second + +// DefaultUserAgent is the project-specific user agent used when submitting +// messages unless overridden by client code. This replaces the Go default +// user agent value of "Go-http-client/1.1". +// +// The major.minor numbers reflect when this project first diverged from the +// "upstream" or parent project. +const DefaultUserAgent string = "go-teams-notify/2.2" + +// ErrWebhookURLUnexpected is returned when a provided webhook URL does +// not match a set of confirmed webhook URL patterns. +var ErrWebhookURLUnexpected = errors.New("webhook URL does not match one of expected patterns") + +// ErrWebhookURLUnexpectedPrefix is returned when a provided webhook URL does +// not match a set of confirmed webhook URL prefixes. +// +// Deprecated: Use ErrWebhookURLUnexpected instead. +var ErrWebhookURLUnexpectedPrefix = ErrWebhookURLUnexpected + +// ErrInvalidWebhookURLResponseText is returned when the remote webhook +// endpoint indicates via response text that a message submission was +// unsuccessful. +var ErrInvalidWebhookURLResponseText = errors.New("invalid webhook URL response text") + +// API is the legacy interface representing a client used to submit messages +// to a Microsoft Teams channel. +type API interface { + Send(webhookURL string, webhookMessage MessageCard) error + SendWithContext(ctx context.Context, webhookURL string, webhookMessage MessageCard) error + SendWithRetry(ctx context.Context, webhookURL string, webhookMessage MessageCard, retries int, retriesDelay int) error + SkipWebhookURLValidationOnSend(skip bool) API + AddWebhookURLValidationPatterns(patterns ...string) API + ValidateWebhook(webhookURL string) error +} + +// MessageSender describes the behavior of a baseline Microsoft Teams client. +// +// An unexported method is used to prevent client code from implementing this +// interface in order to support future changes (and not violate backwards +// compatibility). +type MessageSender interface { + HTTPClient() *http.Client + UserAgent() string + ValidateWebhook(webhookURL string) error + + // A private method to prevent client code from implementing the interface + // so that any future changes to it will not violate backwards + // compatibility. + private() +} + +// messagePreparer is a message type that supports marshaling its fields +// as preparation for delivery to an endpoint. +type messagePreparer interface { + Prepare() error +} + +// messageValidator is a message type that provides validation of its format. +type messageValidator interface { + Validate() error +} + +// TeamsMessage is the interface shared by all supported message formats for +// submission to a Microsoft Teams channel. +type TeamsMessage interface { + messagePreparer + messageValidator + + Payload() io.Reader +} + +// teamsClient is the legacy client used for submitting messages to a +// Microsoft Teams channel. +type teamsClient struct { + httpClient *http.Client + userAgent string + webhookURLValidationPatterns []string + skipWebhookURLValidation bool +} + +// TeamsClient provides functionality for submitting messages to a Microsoft +// Teams channel. +type TeamsClient struct { + httpClient *http.Client + userAgent string + webhookURLValidationPatterns []string + skipWebhookURLValidation bool +} + +func init() { + // Disable logging output by default unless client code explicitly + // requests it + logger = log.New(os.Stderr, "[goteamsnotify] ", 0) + logger.SetOutput(ioutil.Discard) +} + +// EnableLogging enables logging output from this package. Output is muted by +// default unless explicitly requested (by calling this function). +func EnableLogging() { + logger.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + logger.SetOutput(os.Stderr) +} + +// DisableLogging reapplies default package-level logging settings of muting +// all logging output. +func DisableLogging() { + logger.SetFlags(0) + logger.SetOutput(ioutil.Discard) +} + +// NewClient - create a brand new client for MS Teams notify +// +// Deprecated: use NewTeamsClient() function instead. +func NewClient() API { + client := teamsClient{ + httpClient: &http.Client{ + // We're using a context instead of setting this directly + // Timeout: DefaultWebhookSendTimeout, + }, + skipWebhookURLValidation: false, + } + return &client +} + +// NewTeamsClient constructs a minimal client for submitting messages to a +// Microsoft Teams channel. +func NewTeamsClient() *TeamsClient { + client := TeamsClient{ + httpClient: &http.Client{ + // We're using a context instead of setting this directly + // Timeout: DefaultWebhookSendTimeout, + }, + skipWebhookURLValidation: false, + } + return &client +} + +// private prevents client code from implementing the MessageSender interface +// so that any future changes to it will not violate backwards compatibility. +func (c *teamsClient) private() {} + +// private prevents client code from implementing the MessageSender interface +// so that any future changes to it will not violate backwards compatibility. +func (c *TeamsClient) private() {} + +// SetHTTPClient accepts a custom http.Client value which replaces the +// existing default http.Client. +func (c *TeamsClient) SetHTTPClient(httpClient *http.Client) *TeamsClient { + c.httpClient = httpClient + + return c +} + +// SetUserAgent accepts a custom user agent string. This custom user agent is +// used when submitting messages to Microsoft Teams. +func (c *TeamsClient) SetUserAgent(userAgent string) *TeamsClient { + c.userAgent = userAgent + + return c +} + +// UserAgent returns the configured user agent string for the client. If a +// custom value is not set the default package user agent is returned. +// +// Deprecated: use TeamsClient.UserAgent() method instead. +func (c *teamsClient) UserAgent() string { + switch { + case c.userAgent != "": + return c.userAgent + default: + return DefaultUserAgent + } +} + +// UserAgent returns the configured user agent string for the client. If a +// custom value is not set the default package user agent is returned. +func (c *TeamsClient) UserAgent() string { + switch { + case c.userAgent != "": + return c.userAgent + default: + return DefaultUserAgent + } +} + +// AddWebhookURLValidationPatterns collects given patterns for validation of +// the webhook URL. +// +// Deprecated: use TeamsClient.AddWebhookURLValidationPatterns() method instead. +func (c *teamsClient) AddWebhookURLValidationPatterns(patterns ...string) API { + c.webhookURLValidationPatterns = append(c.webhookURLValidationPatterns, patterns...) + return c +} + +// AddWebhookURLValidationPatterns collects given patterns for validation of +// the webhook URL. +func (c *TeamsClient) AddWebhookURLValidationPatterns(patterns ...string) *TeamsClient { + c.webhookURLValidationPatterns = append(c.webhookURLValidationPatterns, patterns...) + return c +} + +// HTTPClient returns the internal pointer to an http.Client. This can be used +// to further modify specific http.Client field values. +// +// Deprecated: use TeamsClient.HTTPClient() method instead. +func (c *teamsClient) HTTPClient() *http.Client { + return c.httpClient +} + +// HTTPClient returns the internal pointer to an http.Client. This can be used +// to further modify specific http.Client field values. +func (c *TeamsClient) HTTPClient() *http.Client { + return c.httpClient +} + +// Send is a wrapper function around the SendWithContext method in order to +// provide backwards compatibility. +// +// Deprecated: use TeamsClient.Send() method instead. +func (c *teamsClient) Send(webhookURL string, webhookMessage MessageCard) error { + // Create context that can be used to emulate existing timeout behavior. + ctx, cancel := context.WithTimeout(context.Background(), DefaultWebhookSendTimeout) + defer cancel() + + return sendWithContext(ctx, c, webhookURL, &webhookMessage) +} + +// Send is a wrapper function around the SendWithContext method in order to +// provide backwards compatibility. +func (c *TeamsClient) Send(webhookURL string, message TeamsMessage) error { + // Create context that can be used to emulate existing timeout behavior. + ctx, cancel := context.WithTimeout(context.Background(), DefaultWebhookSendTimeout) + defer cancel() + + return sendWithContext(ctx, c, webhookURL, message) +} + +// SendWithContext submits a given message to a Microsoft Teams channel using +// the provided webhook URL. The http client request honors the cancellation +// or timeout of the provided context. +// +// Deprecated: use TeamsClient.SendWithContext() method instead. +func (c *teamsClient) SendWithContext(ctx context.Context, webhookURL string, webhookMessage MessageCard) error { + return sendWithContext(ctx, c, webhookURL, &webhookMessage) +} + +// SendWithContext submits a given message to a Microsoft Teams channel using +// the provided webhook URL. The http client request honors the cancellation +// or timeout of the provided context. +func (c *TeamsClient) SendWithContext(ctx context.Context, webhookURL string, message TeamsMessage) error { + return sendWithContext(ctx, c, webhookURL, message) +} + +// SendWithRetry provides message retry support when submitting messages to a +// Microsoft Teams channel. The caller is responsible for providing the +// desired context timeout, the number of retries and retries delay. +// +// Deprecated: use TeamsClient.SendWithRetry() method instead. +func (c *teamsClient) SendWithRetry(ctx context.Context, webhookURL string, webhookMessage MessageCard, retries int, retriesDelay int) error { + return sendWithRetry(ctx, c, webhookURL, &webhookMessage, retries, retriesDelay) +} + +// SendWithRetry provides message retry support when submitting messages to a +// Microsoft Teams channel. The caller is responsible for providing the +// desired context timeout, the number of retries and retries delay. +func (c *TeamsClient) SendWithRetry(ctx context.Context, webhookURL string, message TeamsMessage, retries int, retriesDelay int) error { + return sendWithRetry(ctx, c, webhookURL, message, retries, retriesDelay) +} + +// SkipWebhookURLValidationOnSend allows the caller to optionally disable +// webhook URL validation. +// +// Deprecated: use TeamsClient.SkipWebhookURLValidationOnSend() method instead. +func (c *teamsClient) SkipWebhookURLValidationOnSend(skip bool) API { + c.skipWebhookURLValidation = skip + return c +} + +// SkipWebhookURLValidationOnSend allows the caller to optionally disable +// webhook URL validation. +func (c *TeamsClient) SkipWebhookURLValidationOnSend(skip bool) *TeamsClient { + c.skipWebhookURLValidation = skip + return c +} + +// prepareRequest is a helper function that prepares a http.Request (including +// all desired headers) in order to submit a given prepared message to an +// endpoint. +func prepareRequest(ctx context.Context, userAgent string, webhookURL string, preparedMessage io.Reader) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, webhookURL, preparedMessage) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/json;charset=utf-8") + req.Header.Set("User-Agent", userAgent) + + return req, nil +} + +// processResponse is a helper function responsible for validating a response +// from an endpoint after submitting a message. +func processResponse(response *http.Response) (string, error) { + // Get the response body, then convert to string for use with extended + // error messages + responseData, err := ioutil.ReadAll(response.Body) + if err != nil { + logger.Println(err) + + return "", err + } + responseString := string(responseData) + + // TODO: Refactor for v3 series once O365 connector support is dropped. + switch { + // 400 Bad Response is likely an indicator that we failed to provide a + // required field in our JSON payload. For example, when leaving out the + // top level MessageCard Summary or Text field, the remote API returns + // "Summary or Text is required." as a text string. We include that + // response text in the error message that we return to the caller. + case response.StatusCode >= 299: + err = fmt.Errorf("error on notification: %v, %q", response.Status, responseString) + + logger.Println(err) + + return "", err + + case response.StatusCode == 202: + // 202 Accepted response is expected for Workflow connector URL + // submissions. + + logger.Println("202 Accepted response received as expected for workflow connector") + + return responseString, nil + + // DEPRECATED + // + // See https://github.com/atc0005/go-teams-notify/issues/262 + // + // Microsoft Teams developers have indicated that receiving a 200 status + // code when submitting payloads to O365 connectors is insufficient to + // confirm that a message was successfully submitted. + // + // Instead, clients should ensure that a specific response string was also + // returned along with a 200 status code to confirm that a message was + // sent successfully. Because there is a chance that unintentional + // whitespace could be included, we explicitly strip it out. + // + // See atc0005/go-teams-notify#59 for more information. + case responseString != strings.TrimSpace(ExpectedWebhookURLResponseText): + logger.Printf( + "StatusCode: %v, Status: %v\n", response.StatusCode, response.Status, + ) + logger.Printf("ResponseString: %v\n", responseString) + + err = fmt.Errorf( + "got %q, expected %q: %w", + responseString, + ExpectedWebhookURLResponseText, + ErrInvalidWebhookURLResponseText, + ) + + logger.Println(err) + + return "", err + + default: + return responseString, nil + } +} + +// validateWebhook applies webhook URL validation unless explicitly disabled. +func validateWebhook(webhookURL string, skipWebhookValidation bool, patterns []string) error { + if skipWebhookValidation || webhookURL == DisableWebhookURLValidation { + logger.Printf("validateWebhook: Webhook URL will not be validated: %#v\n", webhookURL) + + return nil + } + + u, err := url.Parse(webhookURL) + if err != nil { + return fmt.Errorf("unable to parse webhook URL %q: %w", webhookURL, err) + } + + if len(patterns) == 0 { + patterns = []string{ + DefaultWebhookURLValidationPattern, + WorkflowURLBaseDomain, + } + } + + // Indicate passing validation if at least one pattern matches. + for _, pat := range patterns { + matched, err := regexp.MatchString(pat, webhookURL) + if err != nil { + return err + } + if matched { + logger.Printf("Pattern %v matched", pat) + + return nil + } + } + + return fmt.Errorf( + "%w; got: %q, patterns: %s", + ErrWebhookURLUnexpected, + u.String(), + strings.Join(patterns, ","), + ) +} + +// ValidateWebhook applies webhook URL validation unless explicitly disabled. +// +// Deprecated: use TeamsClient.ValidateWebhook() method instead. +func (c *teamsClient) ValidateWebhook(webhookURL string) error { + return validateWebhook(webhookURL, c.skipWebhookURLValidation, c.webhookURLValidationPatterns) +} + +// ValidateWebhook applies webhook URL validation unless explicitly disabled. +func (c *TeamsClient) ValidateWebhook(webhookURL string) error { + return validateWebhook(webhookURL, c.skipWebhookURLValidation, c.webhookURLValidationPatterns) +} + +// sendWithContext submits a given message to a Microsoft Teams channel using +// the provided webhook URL and client. The http client request honors the +// cancellation or timeout of the provided context. +func sendWithContext(ctx context.Context, client MessageSender, webhookURL string, message TeamsMessage) error { + logger.Printf("sendWithContext: Webhook message received: %#v\n", message) + + if err := client.ValidateWebhook(webhookURL); err != nil { + return fmt.Errorf( + "failed to validate webhook URL: %w", + err, + ) + } + + if err := message.Validate(); err != nil { + return fmt.Errorf( + "failed to validate message: %w", + err, + ) + } + + if err := message.Prepare(); err != nil { + return fmt.Errorf( + "failed to prepare message: %w", + err, + ) + } + + req, err := prepareRequest(ctx, client.UserAgent(), webhookURL, message.Payload()) + if err != nil { + return fmt.Errorf( + "failed to prepare request: %w", + err, + ) + } + + // Submit message to endpoint. + res, err := client.HTTPClient().Do(req) + if err != nil { + return fmt.Errorf( + "failed to submit message: %w", + err, + ) + } + + // Make sure that we close the response body once we're done with it + defer func() { + if err := res.Body.Close(); err != nil { + log.Printf("error closing response body: %v", err) + } + }() + + responseText, err := processResponse(res) + if err != nil { + return fmt.Errorf( + "failed to process response: %w", + err, + ) + } + + logger.Printf("sendWithContext: Response string from Microsoft Teams API: %v\n", responseText) + + return nil +} + +// sendWithRetry provides message retry support when submitting messages to a +// Microsoft Teams channel. The caller is responsible for providing the +// desired context timeout, the number of retries and retries delay. +func sendWithRetry(ctx context.Context, client MessageSender, webhookURL string, message TeamsMessage, retries int, retriesDelay int) error { + var result error + + // initial attempt + number of specified retries + attemptsAllowed := 1 + retries + + // attempt to send message to Microsoft Teams, retry specified number of + // times before giving up + for attempt := 1; attempt <= attemptsAllowed; attempt++ { + // the result from the last attempt is returned to the caller + result = sendWithContext(ctx, client, webhookURL, message) + + switch { + case result != nil: + + logger.Printf( + "sendWithRetry: Attempt %d of %d to send message failed: %v", + attempt, + attemptsAllowed, + result, + ) + + if ctx.Err() != nil { + errMsg := fmt.Errorf( + "sendWithRetry: context cancelled or expired: %v; "+ + "aborting message submission after %d of %d attempts: %w", + ctx.Err().Error(), + attempt, + attemptsAllowed, + result, + ) + + logger.Println(errMsg) + + return errMsg + } + + ourRetryDelay := time.Duration(retriesDelay) * time.Second + + logger.Printf( + "sendWithRetry: Context not cancelled yet, applying retry delay of %v", + ourRetryDelay, + ) + time.Sleep(ourRetryDelay) + + default: + logger.Printf( + "sendWithRetry: successfully sent message after %d of %d attempts\n", + attempt, + attemptsAllowed, + ) + + // No further retries needed + return nil + } + } + + return result +} + +// old deprecated helper functions -------------------------------------------------------------------------------------------------------------- + +// IsValidInput is a validation "wrapper" function. This function is intended +// to run current validation checks and offer easy extensibility for future +// validation requirements. +// +// Deprecated: use API.ValidateWebhook() and MessageCard.Validate() +// methods instead. +func IsValidInput(webhookMessage MessageCard, webhookURL string) (bool, error) { + // validate url + if valid, err := IsValidWebhookURL(webhookURL); !valid { + return false, err + } + + // validate message + if valid, err := IsValidMessageCard(webhookMessage); !valid { + return false, err + } + + return true, nil +} + +// IsValidWebhookURL performs validation checks on the webhook URL used to +// submit messages to Microsoft Teams. +// +// Deprecated: use API.ValidateWebhook() method instead. +func IsValidWebhookURL(webhookURL string) (bool, error) { + c := teamsClient{} + err := c.ValidateWebhook(webhookURL) + return err == nil, err +} + +// IsValidMessageCard performs validation/checks for known issues with +// MessardCard values. +// +// Deprecated: use MessageCard.Validate() instead. +func IsValidMessageCard(webhookMessage MessageCard) (bool, error) { + err := webhookMessage.Validate() + return err == nil, err +} diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/textutils.go b/vendor/github.com/atc0005/go-teams-notify/v2/textutils.go new file mode 100644 index 0000000..c872266 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/textutils.go @@ -0,0 +1,29 @@ +// Copyright 2022 Adam Chalkley +// +// https://github.com/atc0005/go-teams-notify +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package goteamsnotify + +import ( + "strings" +) + +// InList is a helper function to emulate Python's `if "x" in list:` +// functionality. The caller can optionally ignore case of compared items. +func InList(needle string, haystack []string, ignoreCase bool) bool { + for _, item := range haystack { + if ignoreCase { + if strings.EqualFold(item, needle) { + return true + } + } + + if item == needle { + return true + } + } + return false +} diff --git a/vendor/github.com/bitrise-io/go-steputils/stepconf/stepconf.go b/vendor/github.com/bitrise-io/go-steputils/stepconf/stepconf.go new file mode 100644 index 0000000..f891923 --- /dev/null +++ b/vendor/github.com/bitrise-io/go-steputils/stepconf/stepconf.go @@ -0,0 +1,514 @@ +package stepconf + +import ( + "errors" + "fmt" + "os" + "reflect" + "regexp" + "strconv" + "strings" + + "github.com/bitrise-io/go-utils/colorstring" + "github.com/bitrise-io/go-utils/parseutil" +) + +// ErrNotStructPtr indicates a type is not a pointer to a struct. +var ErrNotStructPtr = errors.New("must be a pointer to a struct") + +// ParseError occurs when a struct field cannot be set. +type ParseError struct { + Field string + Value string + Err error +} + +const rangeMinimumGroupName = "min" +const rangeMaximumGroupName = "max" +const rangeMinBracketGroupName = "minbr" +const rangeMaxBracketGroupName = "maxbr" +const rangeRegex = `range(?P<` + rangeMinBracketGroupName + `>\[|\])(?P<` + rangeMinimumGroupName + `>.*?)\.\.(?P<` + rangeMaximumGroupName + `>.*?)(?P<` + rangeMaxBracketGroupName + `>\[|\])` + +// Error implements builtin errors.Error. +func (e *ParseError) Error() string { + segments := []string{e.Field} + if e.Value != "" { + segments = append(segments, e.Value) + } + segments = append(segments, e.Err.Error()) + return strings.Join(segments, ": ") +} + +// Secret variables are not shown in the printed output. +type Secret string + +const secret = "*****" + +// String implements fmt.Stringer.String. +// When a Secret is printed, it's masking the underlying string with asterisks. +func (s Secret) String() string { + if s == "" { + return "" + } + return secret +} + +// Print the name of the struct with Title case in blue color with followed by a newline, +// then print all fields formatted as '- field name: field value` separated by newline. +func Print(config interface{}) { + fmt.Print(toString(config)) +} + +func valueString(v reflect.Value) string { + if v.Kind() != reflect.Ptr { + return fmt.Sprintf("%v", v.Interface()) + } + + if !v.IsNil() { + return fmt.Sprintf("%v", v.Elem().Interface()) + } + + return "" +} + +// returns the name of the struct with Title case in blue color followed by a newline, +// then print all fields formatted as '- field name: field value` separated by newline. +func toString(config interface{}) string { + v := reflect.ValueOf(config) + t := reflect.TypeOf(config) + + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + str := fmt.Sprintf(colorstring.Bluef("%s:\n", strings.Title(t.Name()))) + for i := 0; i < t.NumField(); i++ { + str += fmt.Sprintf("- %s: %s\n", t.Field(i).Name, valueString(v.Field(i))) + } + + return str +} + +// parseTag splits a struct field's env tag into its name and option. +func parseTag(tag string) (string, string) { + if idx := strings.Index(tag, ","); idx != -1 { + return tag[:idx], tag[idx+1:] + } + return tag, "" +} + +// Parse populates a struct with the retrieved values from environment variables +// described by struct tags and applies the defined validations. +func Parse(conf interface{}) error { + c := reflect.ValueOf(conf) + if c.Kind() != reflect.Ptr { + return ErrNotStructPtr + } + c = c.Elem() + if c.Kind() != reflect.Struct { + return ErrNotStructPtr + } + t := c.Type() + + var errs []*ParseError + for i := 0; i < c.NumField(); i++ { + tag, ok := t.Field(i).Tag.Lookup("env") + if !ok { + continue + } + key, constraint := parseTag(tag) + value := os.Getenv(key) + + if err := setField(c.Field(i), value, constraint); err != nil { + errs = append(errs, &ParseError{t.Field(i).Name, value, err}) + } + } + if len(errs) > 0 { + errorString := "failed to parse config:" + for _, err := range errs { + errorString += fmt.Sprintf("\n- %s", err) + } + + errorString += fmt.Sprintf("\n\n%s", toString(conf)) + return errors.New(errorString) + } + + return nil +} + +func setField(field reflect.Value, value, constraint string) error { + if err := validateConstraint(value, constraint); err != nil { + return err + } + + if value == "" { + return nil + } + + if field.Kind() == reflect.Ptr { + // If field is a pointer type, then set its value to be a pointer to a new zero value, matching field underlying type. + var dePtrdType = field.Type().Elem() // get the type field can point to + var newPtrType = reflect.New(dePtrdType) // create new ptr address for type with non-nil zero value + field.Set(newPtrType) // assign value to pointer + field = field.Elem() + } + + switch field.Kind() { + case reflect.String: + field.SetString(value) + case reflect.Bool: + b, err := parseutil.ParseBool(value) + if err != nil { + return errors.New("can't convert to bool") + } + field.SetBool(b) + case reflect.Int: + n, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return errors.New("can't convert to int") + } + field.SetInt(n) + case reflect.Float64: + f, err := strconv.ParseFloat(value, 64) + if err != nil { + return errors.New("can't convert to float") + } + field.SetFloat(f) + case reflect.Slice: + field.Set(reflect.ValueOf(strings.Split(value, "|"))) + default: + return fmt.Errorf("type is not supported (%s)", field.Kind()) + } + return nil +} + +func validateConstraint(value, constraint string) error { + switch constraint { + case "": + break + case "required": + if value == "" { + return errors.New("required variable is not present") + } + case "file", "dir": + if err := checkPath(value, constraint == "dir"); err != nil { + return err + } + // TODO: use FindStringSubmatch to distinguish no match and match for empty string. + case regexp.MustCompile(`^opt\[.*\]$`).FindString(constraint): + if !contains(value, constraint) { + // TODO: print only the value options, not the whole string. + return fmt.Errorf("value is not in value options (%s)", constraint) + } + case regexp.MustCompile(rangeRegex).FindString(constraint): + if err := ValidateRangeFields(value, constraint); err != nil { + return err + } + default: + return fmt.Errorf("invalid constraint (%s)", constraint) + } + return nil +} + +//ValidateRangeFields validates if the given range is proper. Ranges are optional, empty values are valid. +func ValidateRangeFields(valueStr, constraint string) error { + if valueStr == "" { + return nil + } + constraintMin, constraintMax, constraintMinBr, constraintMaxBr, err := GetRangeValues(constraint) + if err != nil { + return err + } + min, err := parseValueStr(constraintMin) + if err != nil { + return fmt.Errorf("failed to parse min value %s: %s", constraintMin, err) + } + max, err := parseValueStr(constraintMax) + if err != nil { + return fmt.Errorf("failed to parse max value %s: %s", constraintMax, err) + } + value, err := parseValueStr(valueStr) + if err != nil { + return fmt.Errorf("failed to parse value %s: %s", valueStr, err) + } + isMinInclusiveBool, err := isMinInclusive(constraintMinBr) + if err != nil { + return err + } + isMaxInclusiveBool, err := isMaxInclusive(constraintMaxBr) + if err != nil { + return err + } + + if err := validateRangeFieldValues(min, max, isMinInclusiveBool, isMaxInclusiveBool, value); err != nil { + return err + } + if err := validateRangeFieldTypes(min, max, value); err != nil { + return err + } + + return nil +} + +func isMinInclusive(bracket string) (bool, error) { + switch bracket { + case "[": + return true, nil + case "]": + return false, nil + default: + return false, fmt.Errorf("invalid string found for bracket: %s", bracket) + } +} + +func isMaxInclusive(bracket string) (bool, error) { + switch bracket { + case "[": + return false, nil + case "]": + return true, nil + default: + return false, fmt.Errorf("invalid string found for bracket: %s", bracket) + } +} + +func validateRangeFieldValues(min interface{}, max interface{}, minInclusive bool, maxInclusive bool, value interface{}) error { + if value == nil { + return fmt.Errorf("value is not present") + } + var err error + var valueFloat float64 + if valueFloat, err = getFloatValue(value); err != nil { + return err + } + + var minErr error + var minFloat float64 + if min != nil { + if minFloat, err = getFloatValue(min); err != nil { + return err + } + minErr = validateRangeMinFieldValue(minFloat, valueFloat, minInclusive) + } + + var maxErr error + var maxFloat float64 + if max != nil { + var err error + if maxFloat, err = getFloatValue(max); err != nil { + return err + } + maxErr = validateRangeMaxFieldValue(maxFloat, valueFloat, maxInclusive) + } + + if min != nil && max != nil { + if minFloat > maxFloat { + return fmt.Errorf("constraint logic is wrong, minimum value %f is bigger than maximum %f", minFloat, maxFloat) + } + if minFloat == maxFloat { + return fmt.Errorf("minimum value %f is equal to maximum %f, for this case use optional value", minFloat, maxFloat) + } + } + + if min == nil { + return maxErr + } else if max == nil { + return minErr + } + if minErr != nil || maxErr != nil { + return fmt.Errorf("value %f is out of range %f-%f", value, minFloat, maxFloat) + } + return nil +} + +func validateRangeFieldTypes(min interface{}, max interface{}, value interface{}) error { + if value == nil { + return fmt.Errorf("value cannot be nil") + } + var minType string + var maxType string + var valueType string + var err error + + if valueType, err = getTypeOf(value); err != nil { + return err + } + if min != nil { + if minType, err = getTypeOf(min); err != nil { + return err + } + } + if max != nil { + if maxType, err = getTypeOf(max); err != nil { + return err + } + } + + if maxType != "" && minType != "" && !hasSameContent(minType, maxType, valueType) { + return fmt.Errorf("invalid constraint and value combination, minimum is %s, maximum is %s, value is %s, but they should be the same", minType, maxType, valueType) + } + + if maxType != "" && !hasSameContent(maxType, valueType) { + return fmt.Errorf("invalid constraint and value combination, maximum is %s, value is %s, but they should be the same", maxType, valueType) + } + + if minType != "" && !hasSameContent(minType, valueType) { + return fmt.Errorf("invalid constraint and value combination, minimum is %s, value is %s, but they should be the same", minType, valueType) + } + return nil +} + +func hasSameContent(strs ...string) bool { + length := len(strs) + if length == 1 { + return true + } + firstItem := strs[0] + for i := 1; i < length; i++ { + if strings.Compare(firstItem, strs[i]) != 0 { + return false + } + } + return true +} + +func getTypeOf(v interface{}) (string, error) { + switch v.(type) { + case int64: + return "int64", nil + case float64: + return "float64", nil + default: + return "unknown", fmt.Errorf("could not find type for %v", v) + } +} + +func parseValueStr(value string) (interface{}, error) { + var err error + var parsedInt int64 + var parsedFloat float64 + if parsedInt, err = strconv.ParseInt(value, 10, 64); err != nil { + // Could be float + if parsedFloat, err = strconv.ParseFloat(value, 64); err != nil { + // It is invalid. + return nil, fmt.Errorf("value %s is could not be parsed", value) + } + return parsedFloat, nil + } + return parsedInt, nil +} + +func getFloatValue(value interface{}) (float64, error) { + switch i := value.(type) { + case int64: + return float64(i), nil + case float64: + return i, nil + case string: + var parsedValue interface{} + var err error + if parsedValue, err = parseValueStr(i); err != nil { + return 0, err + } + return getFloatValue(parsedValue) + default: + return 0, fmt.Errorf("not supported type %T", value) + } +} + +func validateRangeMinFieldValue(min float64, value float64, inclusive bool) error { + if inclusive && min > value { + return fmt.Errorf("value %f is out of range, less than minimum %f", value, min) + } else if !inclusive && min >= value { + return fmt.Errorf("value %f is out of range, greater or equal than maximum %f", value, min) + } + return nil +} + +func validateRangeMaxFieldValue(max float64, value float64, inclusive bool) error { + if inclusive && max < value { + return fmt.Errorf("value %f is out of range, greater than maximum %f", value, max) + + } else if !inclusive && max <= value { + return fmt.Errorf("value %f is out of range, greater or equal than maximum %f", value, max) + } + return nil +} + +// GetRangeValues reads up the given range constraint and returns the values, or an error if the constraint is malformed or could not be parsed. +func GetRangeValues(value string) (min string, max string, minBracket string, maxBracket string, err error) { + regex := regexp.MustCompile(rangeRegex) + groups := regex.FindStringSubmatch(value) + if len(groups) < 1 { + return "", "", "", "", fmt.Errorf("value in value options is malformed (%s)", value) + } + + groupMap := getRegexGroupMap(groups, regex) + minStr := groupMap[rangeMinimumGroupName] + maxStr := groupMap[rangeMaximumGroupName] + minBr := groupMap[rangeMinBracketGroupName] + maxBr := groupMap[rangeMaxBracketGroupName] + if minStr == "" && maxStr == "" { + return "", "", "", "", fmt.Errorf("constraint contains no limits") + } + return minStr, maxStr, minBr, maxBr, nil +} + +func getRegexGroupMap(groups []string, regex *regexp.Regexp) map[string]string { + result := make(map[string]string) + for i, value := range regex.SubexpNames() { + if i != 0 && value != "" { + result[value] = groups[i] + } + } + return result +} + +func checkPath(path string, dir bool) error { + file, err := os.Stat(path) + if err != nil { + // TODO: check case when file exist but os.Stat fails. + return os.ErrNotExist + } + if dir && !file.IsDir() { + return errors.New("not a directory") + } + return nil +} + +// contains reports whether s is within the value options, where value options +// are parsed from opt, which format's is opt[item1,item2,item3]. If an option +// contains commas, it should be single quoted (eg. opt[item1,'item2,item3']). +func contains(s, opt string) bool { + opt = strings.TrimSuffix(strings.TrimPrefix(opt, "opt["), "]") + var valueOpts []string + if strings.Contains(opt, "'") { + // The single quotes separate the options with comma and without comma + // Eg. "a,b,'c,d',e" will results "a,b," "c,d" and ",e" strings. + for _, s := range strings.Split(opt, "'") { + switch { + case s == "," || s == "": + case !strings.HasPrefix(s, ",") && !strings.HasSuffix(s, ","): + // If a string doesn't starts nor ends with a comma it means it's an option which + // contains comma, so we just append it to valueOpts as it is. Eg. "c,d" from above. + valueOpts = append(valueOpts, s) + default: + // If a string starts or ends with comma it means that it contains options without comma. + // So we split the string at commas to get the options. Eg. "a,b," and ",e" from above. + valueOpts = append(valueOpts, strings.Split(strings.Trim(s, ","), ",")...) + } + } + } else { + valueOpts = strings.Split(opt, ",") + } + for _, valOpt := range valueOpts { + if valOpt == s { + return true + } + } + return false +} diff --git a/vendor/github.com/bitrise-io/go-utils/log/internal_logger.go b/vendor/github.com/bitrise-io/go-utils/log/internal_logger.go new file mode 100644 index 0000000..245f995 --- /dev/null +++ b/vendor/github.com/bitrise-io/go-utils/log/internal_logger.go @@ -0,0 +1,76 @@ +package log + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +var ( + analyticsServerURL = "https://bitrise-step-analytics.herokuapp.com" + httpClient = http.Client{ + Timeout: time.Second * 5, + } +) + +// Entry represents a line in a log +type Entry struct { + LogLevel string `json:"log_level"` + Message string `json:"message"` + Data map[string]interface{} `json:"data"` +} + +// SetAnalyticsServerURL updates the the analytics server collecting the +// logs. It is intended for use during tests. Warning: current implementation +// is not thread safe, do not call the function during runtime. +func SetAnalyticsServerURL(url string) { + analyticsServerURL = url +} + +// Internal sends the log message to the configured analytics server +func rprintf(logLevel string, stepID string, tag string, data map[string]interface{}, format string, v ...interface{}) { + e := Entry{ + Message: fmt.Sprintf(format, v...), + LogLevel: logLevel, + } + + e.Data = make(map[string]interface{}) + for k, v := range data { + e.Data[k] = v + } + + if v, ok := e.Data["step_id"]; ok { + fmt.Printf("internal logger: data.step_id (%s) will be overriden with (%s) ", v, stepID) + } + if v, ok := e.Data["tag"]; ok { + fmt.Printf("internal logger: data.tag (%s) will be overriden with (%s) ", v, tag) + } + + e.Data["step_id"] = stepID + e.Data["tag"] = tag + + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(e); err != nil { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + req, err := http.NewRequest(http.MethodPost, analyticsServerURL+"/logs", &b) + if err != nil { + // deliberately not writing into users log + return + } + req = req.WithContext(ctx) + req.Header.Add("Content-Type", "application/json") + + if _, err := httpClient.Do(req); err != nil { + // deliberately not writing into users log + return + } + +} diff --git a/vendor/github.com/bitrise-io/go-utils/log/print.go b/vendor/github.com/bitrise-io/go-utils/log/print.go index dd65016..1c817c4 100644 --- a/vendor/github.com/bitrise-io/go-utils/log/print.go +++ b/vendor/github.com/bitrise-io/go-utils/log/print.go @@ -5,15 +5,24 @@ import ( ) func printf(severity Severity, withTime bool, format string, v ...interface{}) { + message := createLogMsg(severity, withTime, format, v...) + if _, err := fmt.Fprintln(outWriter, message); err != nil { + fmt.Printf("failed to print message: %s, error: %s\n", message, err) + } +} + +func createLogMsg(severity Severity, withTime bool, format string, v ...interface{}) string { colorFunc := severityColorFuncMap[severity] message := colorFunc(format, v...) if withTime { - message = fmt.Sprintf("%s %s", timestampField(), message) + message = prefixCurrentTime(message) } - if _, err := fmt.Fprintln(outWriter, message); err != nil { - fmt.Printf("failed to print message: %s, error: %s\n", message, err) - } + return message +} + +func prefixCurrentTime(message string) string { + return fmt.Sprintf("%s %s", timestampField(), message) } // Successf ... @@ -89,3 +98,18 @@ func TWarnf(format string, v ...interface{}) { func TErrorf(format string, v ...interface{}) { printf(errorSeverity, true, format, v...) } + +// RInfof ... +func RInfof(stepID string, tag string, data map[string]interface{}, format string, v ...interface{}) { + rprintf("info", stepID, tag, data, format, v...) +} + +// RWarnf ... +func RWarnf(stepID string, tag string, data map[string]interface{}, format string, v ...interface{}) { + rprintf("warn", stepID, tag, data, format, v...) +} + +// RErrorf ... +func RErrorf(stepID string, tag string, data map[string]interface{}, format string, v ...interface{}) { + rprintf("error", stepID, tag, data, format, v...) +} diff --git a/vendor/github.com/bitrise-io/go-utils/log/severity.go b/vendor/github.com/bitrise-io/go-utils/log/severity.go index a1c4631..4e7786d 100644 --- a/vendor/github.com/bitrise-io/go-utils/log/severity.go +++ b/vendor/github.com/bitrise-io/go-utils/log/severity.go @@ -20,7 +20,7 @@ var ( successSeverityColorFunc severityColorFunc = colorstring.Greenf infoSeverityColorFunc severityColorFunc = colorstring.Bluef normalSeverityColorFunc severityColorFunc = colorstring.NoColorf - debugSeverityColorFunc severityColorFunc = colorstring.NoColorf + debugSeverityColorFunc severityColorFunc = colorstring.Magentaf warnSeverityColorFunc severityColorFunc = colorstring.Yellowf errorSeverityColorFunc severityColorFunc = colorstring.Redf ) diff --git a/vendor/github.com/bitrise-tools/go-steputils/stepconf/stepconf.go b/vendor/github.com/bitrise-tools/go-steputils/stepconf/stepconf.go deleted file mode 100644 index a47b2fe..0000000 --- a/vendor/github.com/bitrise-tools/go-steputils/stepconf/stepconf.go +++ /dev/null @@ -1,217 +0,0 @@ -package stepconf - -import ( - "errors" - "fmt" - "os" - "reflect" - "regexp" - "strconv" - "strings" - - "github.com/bitrise-io/go-utils/colorstring" - "github.com/bitrise-io/go-utils/parseutil" -) - -// ErrNotStructPtr indicates a type is not a pointer to a struct. -var ErrNotStructPtr = errors.New("must be a pointer to a struct") - -// ParseError occurs when a struct field cannot be set. -type ParseError struct { - Field string - Value string - Err error -} - -// Error implements builtin errors.Error. -func (e *ParseError) Error() string { - segments := []string{e.Field} - if e.Value != "" { - segments = append(segments, e.Value) - } - segments = append(segments, e.Err.Error()) - return strings.Join(segments, ": ") -} - -// Secret variables are not shown in the printed output. -type Secret string - -const secret = "*****" - -// String implements fmt.Stringer.String. -// When a Secret is printed, it's masking the underlying string with asterisks. -func (s Secret) String() string { - if s == "" { - return "" - } - return secret -} - -// Print the name of the struct with Title case in blue color with followed by a newline, -// then print all fields formatted as '- field name: field value` separated by newline. -func Print(config interface{}) { - fmt.Printf(toString(config)) -} - -// returns the name of the struct with Title case in blue color followed by a newline, -// then print all fields formatted as '- field name: field value` separated by newline. -func toString(config interface{}) string { - v := reflect.ValueOf(config) - t := reflect.TypeOf(config) - - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - - str := fmt.Sprintf(colorstring.Bluef("%s:\n", strings.Title(t.Name()))) - for i := 0; i < t.NumField(); i++ { - str += fmt.Sprintf("- %s: %v\n", t.Field(i).Name, v.Field(i).Interface()) - } - - return str -} - -// parseTag splits a struct field's env tag into its name and option. -func parseTag(tag string) (string, string) { - if idx := strings.Index(tag, ","); idx != -1 { - return tag[:idx], tag[idx+1:] - } - return tag, "" -} - -// Parse populates a struct with the retrieved values from environment variables -// described by struct tags and applies the defined validations. -func Parse(conf interface{}) error { - c := reflect.ValueOf(conf) - if c.Kind() != reflect.Ptr { - return ErrNotStructPtr - } - c = c.Elem() - if c.Kind() != reflect.Struct { - return ErrNotStructPtr - } - t := c.Type() - - var errs []*ParseError - for i := 0; i < c.NumField(); i++ { - tag, ok := t.Field(i).Tag.Lookup("env") - if !ok { - continue - } - key, constraint := parseTag(tag) - value := os.Getenv(key) - - if err := setField(c.Field(i), value, constraint); err != nil { - errs = append(errs, &ParseError{t.Field(i).Name, value, err}) - } - } - if len(errs) > 0 { - errorString := "failed to parse config:" - for _, err := range errs { - errorString += fmt.Sprintf("\n- %s", err) - } - - errorString += fmt.Sprintf("\n\n%s", toString(conf)) - return errors.New(errorString) - } - - return nil -} - -func setField(field reflect.Value, value, constraint string) error { - switch constraint { - case "": - break - case "required": - if value == "" { - return errors.New("required variable is not present") - } - case "file", "dir": - if err := checkPath(value, constraint == "dir"); err != nil { - return err - } - // TODO: use FindStringSubmatch to distinguish no match and match for empty string. - case regexp.MustCompile(`^opt\[.*\]$`).FindString(constraint): - if !contains(value, constraint) { - // TODO: print only the value options, not the whole string. - return fmt.Errorf("value is not in value options (%s)", constraint) - } - default: - return fmt.Errorf("invalid constraint (%s)", constraint) - } - - if value == "" { - return nil - } - - switch field.Kind() { - case reflect.String: - field.SetString(value) - case reflect.Bool: - b, err := parseutil.ParseBool(value) - if err != nil { - return errors.New("can't convert to bool") - } - field.SetBool(b) - case reflect.Int: - n, err := strconv.ParseInt(value, 10, 32) - if err != nil { - return errors.New("can't convert to int") - } - field.SetInt(n) - case reflect.Slice: - field.Set(reflect.ValueOf(strings.Split(value, "|"))) - default: - return fmt.Errorf("type is not supported (%s)", field.Kind()) - } - return nil -} - -func checkPath(path string, dir bool) error { - file, err := os.Stat(path) - if err != nil { - // TODO: check case when file exist but os.Stat fails. - return os.ErrNotExist - } - if dir && !file.IsDir() { - return errors.New("not a directory") - } - return nil -} - -// contains reports whether s is within the value options, where value options -// are parsed from opt, which format's is opt[item1,item2,item3]. If an option -// contains commas, it should be single quoted (eg. opt[item1,'item2,item3']). -func contains(s, opt string) bool { - opt = strings.TrimSuffix(strings.TrimPrefix(opt, "opt["), "]") - var valueOpts []string - if strings.Contains(opt, "'") { - // The single quotes separate the options with comma and without comma - // Eg. "a,b,'c,d',e" will results "a,b," "c,d" and ",e" strings. - for _, s := range strings.Split(opt, "'") { - switch { - case s == "," || s == "": - case !strings.HasPrefix(s, ",") && !strings.HasSuffix(s, ","): - // If a string doesn't starts nor ends with a comma it means it's an option which - // contains comma, so we just append it to valueOpts as it is. Eg. "c,d" from above. - valueOpts = append(valueOpts, s) - default: - // If a string starts or ends with comma it means that it contains options without comma. - // So we split the string at commas to get the options. Eg. "a,b," and ",e" from above. - valueOpts = append(valueOpts, strings.Split(strings.Trim(s, ","), ",")...) - } - } - } else { - valueOpts = strings.Split(opt, ",") - } - for _, valOpt := range valueOpts { - if valOpt == s { - return true - } - } - return false -} diff --git a/vendor/modules.txt b/vendor/modules.txt new file mode 100644 index 0000000..34c3414 --- /dev/null +++ b/vendor/modules.txt @@ -0,0 +1,14 @@ +# github.com/atc0005/go-teams-notify/v2 v2.13.0 +## explicit; go 1.14 +github.com/atc0005/go-teams-notify/v2 +github.com/atc0005/go-teams-notify/v2/adaptivecard +github.com/atc0005/go-teams-notify/v2/internal/validator +# github.com/bitrise-io/go-steputils v0.0.0-20201016102104-03ae3a6ded35 +## explicit +github.com/bitrise-io/go-steputils/stepconf +# github.com/bitrise-io/go-utils v0.0.0-20201211082830-859032e9adf0 +## explicit; go 1.13 +github.com/bitrise-io/go-utils/colorstring +github.com/bitrise-io/go-utils/log +github.com/bitrise-io/go-utils/parseutil +github.com/bitrise-io/go-utils/pointers