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