From ec300674eec664349946a198d7e104e36bfef829 Mon Sep 17 00:00:00 2001 From: Adam Chalkley Date: Sun, 10 Apr 2022 06:59:50 -0500 Subject: [PATCH] Resolve intermittent user mention failure OVERVIEW Resolve intermittent user mention submission failure by swapping out the `botapi` implementation of user mention support for the new `adaptivecard` package. This new package generates payloads with user mentions that fully comply with published Microsoft Teams requirements. Temporary restrictions applied in the last release between user mentions and the URL "buttons", title flags have been removed. This app now exclusively uses the `adaptivecard` package. Unfortunately, the `Adaptive Card` format does not (at the time of this writing) support message color theming (border trim color). This flag now is a NOOP; while using this flag does not produce an error, it no longer does anything. CHANGES - replace `botapi` and `messagecard` packages with `adaptivecard` package - exclusively use `adaptivecard` package to generate Microsoft Teams messages - add missing checks for use of `--silent` flag before emitting warning/error output - resolve intermittent user mention submission failure - the `--target-url` flag no longer enforces a set limit of 4 URL "buttons" - documentation - the current Microsoft Teams message size limit of *approximately* 28 KB is explicitly noted - references to the `--color` flag have been removed (aside from the explicit NOOP behavior in the config flag table) - mention that the generated JSON payload is now emitted in verbose ouput (exposed via the `--verbose` flag) REFERENCES - refs GH-225 - refs atc0005/go-teams-notify#162 --- README.md | 91 +- cmd/send2teams/main.go | 283 ++- go.mod | 6 +- go.sum | 8 +- internal/config/config.go | 61 +- .../atc0005/go-teams-notify/v2/README.md | 115 +- .../v2/adaptivecard/adaptivecard.go | 2226 +++++++++++++++++ .../go-teams-notify/v2/adaptivecard/doc.go | 32 + .../go-teams-notify/v2/adaptivecard/format.go | 73 + .../v2/adaptivecard/getters.go | 310 +++ .../v2/adaptivecard/nullstring.go | 63 + .../go-teams-notify/v2/botapi/botapi.go | 350 --- .../atc0005/go-teams-notify/v2/botapi/doc.go | 26 - .../atc0005/go-teams-notify/v2/doc.go | 4 +- .../atc0005/go-teams-notify/v2/format.go | 70 +- .../atc0005/go-teams-notify/v2/messagecard.go | 44 +- .../go-teams-notify/v2/messagecard/doc.go | 5 - .../v2/messagecard/messagecard.go | 714 ------ .../atc0005/go-teams-notify/v2/send.go | 5 +- .../atc0005/go-teams-notify/v2/textutils.go | 29 + vendor/modules.txt | 5 +- 21 files changed, 3127 insertions(+), 1393 deletions(-) create mode 100644 vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/adaptivecard.go create mode 100644 vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/doc.go create mode 100644 vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/format.go create mode 100644 vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/getters.go create mode 100644 vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/nullstring.go delete mode 100644 vendor/github.com/atc0005/go-teams-notify/v2/botapi/botapi.go delete mode 100644 vendor/github.com/atc0005/go-teams-notify/v2/botapi/doc.go delete mode 100644 vendor/github.com/atc0005/go-teams-notify/v2/messagecard/doc.go delete mode 100644 vendor/github.com/atc0005/go-teams-notify/v2/messagecard/messagecard.go create mode 100644 vendor/github.com/atc0005/go-teams-notify/v2/textutils.go diff --git a/README.md b/README.md index 5215fe8..226f14c 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Small CLI tool used to submit messages to Microsoft Teams. - [Expected format](#expected-format) - [How to create a webhook URL (Connector)](#how-to-create-a-webhook-url-connector) - [Command-line](#command-line) +- [Limitations](#limitations) + - [message size](#message-size) - [Examples](#examples) - [One-off](#one-off) - [Using an invalid flag](#using-an-invalid-flag) @@ -67,13 +69,12 @@ project. - minimal configuration - very few build dependencies - optional conversion of messages with Windows, Mac or Linux newlines to - `
` to increase compatibility with Teams formatting + increase compatibility with Teams formatting - message delivery retry support with retry and retry delay values configurable via flag -- Support for user mentions (limited) -- optional branding of delivered messages - - noting this application as the delivery agent - - (also optional) noting a sending application as the source of the message +- support for user mentions +- optional support for noting a sending application as the source of the + message - optional support for specifying target `url`, `description` comma-separated pairs for use as labelled "buttons" within a Microsoft Teams message. @@ -203,26 +204,34 @@ shadabacc3934](https://gist.github.com/chusiang/895f6406fbf9285c58ad0a3ace13d025 Currently `send2teams` only supports command-line configuration flags. Requests for other configuration sources will be considered. -| Flag | Required | Default | Possible | Description | -| ------------------------- | -------- | ------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `h`, `help` | No | N/A | N/A | Display Help; show available flags. | -| `v`, `version` | No | `false` | `true`, `false` | Whether to display application version and then immediately exit application. | -| `channel` | No | `unspecified` | *valid Microsoft Teams channel name* | The target channel where we will send a message. If not specified, defaults to `unspecified`. | -| `color` | No | `#832561` | *valid hex color code with leading `#`* | The hex color code used to set the desired trim color on submitted messages. | -| `message` | Yes | | *valid message string* | The (optionally) Markdown-formatted message to submit. | -| `team` | No | `unspecified` | *valid Microsoft Teams team name* | The name of the Team containing our target channel. If not specified, defaults to `unspecified`. | -| `title` | No | | *valid title string* | The (optional) title for the message to submit. | -| `sender` | No | | *valid application or script name* | The (optional) sending application name or generator of the message this app will attempt to deliver. | -| `url` | Yes | | [*valid Microsoft Office 365 Webhook URL*](#webhook-urls) | The Webhook URL provided by a pre-configured Connector. | -| `target-url` | No | | *valid comma-separated `url`, `description` pair* (limit 4) | The target URL and label (specified as comma separated pair) usually visible as a button towards the bottom of the Microsoft Teams message. | -| `verbose` | No | `false` | `true`, `false` | Whether detailed output should be shown after message submission success or failure | -| `silent` | No | `false` | `true`, `false` | Whether ANY output should be shown after message submission success or failure | -| `convert-eol` | No | `false` | `true`, `false` | Whether messages with Windows, Mac and Linux newlines are updated to use break statements before message submission | -| `disable-url-validation` | No | `false` | `true`, `false` | Whether webhook URL validation should be disabled. Useful when submitting generated JSON payloads to a service like . | -| `ignore-invalid-response` | No | `false` | `true`, `false` | Whether an invalid response from remote endpoint should be ignored. This is expected if submitting a message to a non-standard webhook URL. | -| `retries` | No | `2` | *positive whole number* | The number of attempts that this application will make to deliver messages before giving up. | -| `retries-delay` | No | `2` | *positive whole number* | The number of seconds that this application will wait before making another delivery attempt. | -| `user-mention` | No | | *valid comma-separated `name`, `id` pair* | The DisplayName and ID of the recipient (specified as comma separated pair) for a user mention. Incompatible with `target-url`, `title` and `color` flags. | +| Flag | Required | Default | Possible | Description | +| ------------------------- | -------- | ------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `h`, `help` | No | N/A | N/A | Display Help; show available flags. | +| `v`, `version` | No | `false` | `true`, `false` | Whether to display application version and then immediately exit application. | +| `channel` | No | `unspecified` | *valid Microsoft Teams channel name* | The target channel where we will send a message. If not specified, defaults to `unspecified`. | +| `color` | No | `NotUsed` | N/A | NOOP; this setting is no longer used. Values specified for this flag are ignored. | +| `message` | Yes | | *valid message string* | The (optionally) Markdown-formatted message to submit. | +| `team` | No | `unspecified` | *valid Microsoft Teams team name* | The name of the Team containing our target channel. If not specified, defaults to `unspecified`. | +| `title` | No | | *valid title string* | The (optional) title for the message to submit. | +| `sender` | No | | *valid application or script name* | The (optional) sending application name or generator of the message this app will attempt to deliver. | +| `url` | Yes | | [*valid Microsoft Office 365 Webhook URL*](#webhook-urls) | The Webhook URL provided by a pre-configured Connector. | +| `target-url` | No | | *valid comma-separated `url`, `description` pair* | The target URL and label (specified as comma separated pair) usually visible as a button towards the bottom of the Microsoft Teams message. | +| `verbose` | No | `false` | `true`, `false` | Whether detailed output should be shown after message submission success or failure | +| `silent` | No | `false` | `true`, `false` | Whether ANY output should be shown after message submission success or failure | +| `convert-eol` | No | `false` | `true`, `false` | Whether messages with Windows, Mac and Linux newlines are updated to use break statements before message submission | +| `disable-url-validation` | No | `false` | `true`, `false` | Whether webhook URL validation should be disabled. Useful when submitting generated JSON payloads to a service like . | +| `ignore-invalid-response` | No | `false` | `true`, `false` | Whether an invalid response from remote endpoint should be ignored. This is expected if submitting a message to a non-standard webhook URL. | +| `retries` | No | `2` | *positive whole number* | The number of attempts that this application will make to deliver messages before giving up. | +| `retries-delay` | No | `2` | *positive whole number* | The number of seconds that this application will wait before making another delivery attempt. | +| `user-mention` | No | | *valid comma-separated `name`, `id` pair* | The DisplayName and ID of the recipient (specified as comma separated pair) for a user mention. | + +## Limitations + +### message size + +Per official documentation (see [references](#references)), each message sent +to Microsoft Teams can be approximately 28 KB (including the message itself +(text, image links, etc.), @-mentions, and reactions). ## Examples @@ -244,18 +253,20 @@ script): --message "System XYZ is down!" \ --title "System outage alert" \ --sender "Nagios" \ - --color "#832561" \ --url "https://outlook.office.com/webhook/www@xxx/IncomingWebhook/yyy/zzz" ``` and on a single line (e.g., one-off via terminal or batch file): ```console -./send2teams.exe --silent --channel "Alerts" --team "Support" --message "System XYZ is down!" --title "System outage alert" --sender "Nagios" --color "#832561" --url "https://outlook.office.com/webhook/www@xxx/IncomingWebhook/yyy/zzz" +./send2teams.exe --silent --channel "Alerts" --team "Support" --message "System XYZ is down!" --title "System outage alert" --sender "Nagios" --url "https://outlook.office.com/webhook/www@xxx/IncomingWebhook/yyy/zzz" ``` -Remove the `-silent` flag in order to see pass or failure output, otherwise -look at the exit code (`$?`) or Microsoft Teams to determine results. +Note: + +- remove the `-silent` flag in order to see pass or failure output +- use the `-verbose` flag to see the JSON payload submitted to Microsoft Teams +- check the exit code (`$?`) to determine overall success/failure result ### Using an invalid flag @@ -275,29 +286,21 @@ flag provided but not defined: -fake-flag --message "Useful starting points" \ --title "Learn more about Go" \ --sender "Nagios" \ - --color "#832561" \ --url "https://outlook.office.com/webhook/www@xxx/IncomingWebhook/yyy/zzz" \ - --target-url "https://www.golang.org/, Go Homepage" \ + --target-url "https://go.dev/, Go Homepage" \ --target-url "https://github.com/dariubs/GoBooks, Awesome Go Books" ``` and on a single line (e.g., one-off via terminal or batch file): ```console -./send2teams.exe --silent --channel "Alerts" --team "Support" --message "Useful starting points" --title "Learn more about Go" --sender "Nagios" --color "#832561" --url "https://outlook.office.com/webhook/www@xxx/IncomingWebhook/yyy/zzz" --target-url "https://www.golang.org/, Go Homepage" --target-url "https://github.com/dariubs/GoBooks, Awesome Go Books" +./send2teams.exe --silent --channel "Alerts" --team "Support" --message "Useful starting points" --title "Learn more about Go" --sender "Nagios" --url "https://outlook.office.com/webhook/www@xxx/IncomingWebhook/yyy/zzz" --target-url "https://go.dev/, Go Homepage" --target-url "https://github.com/dariubs/GoBooks, Awesome Go Books" ``` ### User mention This example illustrates mentioning a user along with providing a brief -message. The specific format is limited and subject to change in future -releases. - -Incompatible with: - -- message title flag -- target url flag -- theme color flag +message. The example, shown split over multiple lines for readability (e.g., shell script): @@ -319,8 +322,11 @@ and on a single line (e.g., one-off via terminal or batch file): ./send2teams --silent --channel "Alerts" --team "Support" --message "System XYZ is down!" --user-mention "John Doe,jdoe@example.com" --sender "Nagios" --url "https://outlook.office.com/webhook/www@xxx/IncomingWebhook/yyy/zzz" ``` -Remove the `-silent` flag in order to see pass or failure output, otherwise -look at the exit code (`$?`) or Microsoft Teams to determine results. +Note: + +- remove the `-silent` flag in order to see pass or failure output +- use the `-verbose` flag to see the JSON payload submitted to Microsoft Teams +- check the exit code (`$?`) to determine overall success/failure result ## License @@ -364,6 +370,7 @@ SOFTWARE. - - - + - - General Go topics of interest - diff --git a/cmd/send2teams/main.go b/cmd/send2teams/main.go index 3e98066..0b8f938 100644 --- a/cmd/send2teams/main.go +++ b/cmd/send2teams/main.go @@ -15,8 +15,7 @@ import ( "os" goteamsnotify "github.com/atc0005/go-teams-notify/v2" - "github.com/atc0005/go-teams-notify/v2/botapi" - "github.com/atc0005/go-teams-notify/v2/messagecard" + "github.com/atc0005/go-teams-notify/v2/adaptivecard" "github.com/atc0005/send2teams/internal/config" ) @@ -57,17 +56,14 @@ func main() { os.Exit(exitCode) }(&appExitCode) - // Convert EOL if user requested it (useful for converting script output) - if cfg.ConvertEOL { - cfg.MessageText = goteamsnotify.ConvertEOLToBreak(cfg.MessageText) - } - // This should only trigger if user specifies large retry values. if cfg.TeamsSubmissionTimeout() > config.DefaultNagiosNotificationTimeout { - log.Printf( - "WARNING: app cancellation timeout value of %v greater than default Nagios command timeout value!", - cfg.TeamsSubmissionTimeout(), - ) + if !cfg.SilentOutput { + log.Printf( + "WARNING: app cancellation timeout value of %v greater than default Nagios command timeout value!", + cfg.TeamsSubmissionTimeout(), + ) + } } ctxSubmissionTimeout, cancel := context.WithTimeout(context.Background(), cfg.TeamsSubmissionTimeout()) @@ -82,155 +78,206 @@ func main() { // Disable webhook URL validation if requested by user. mstClient.SkipWebhookURLValidationOnSend(cfg.DisableWebhookURLValidation) - var sendErr error - var rawMsg string - switch { - case len(cfg.UserMentions) > 0: + // Convert EOL (useful for output from scripts) in the incoming text if + // user requested it. + if cfg.ConvertEOL { + cfg.MessageText = adaptivecard.ConvertEOL(cfg.MessageText) + + // Not 100% safe to apply across the board. + // + // It is unlikely, but not impossible that someone would submit raw + // text with break statements. When you consider that the flag is + // named "convert-eol", it is entirely reasonable that the user would + // expect break statements to remain untouched. + // + // cfg.MessageText = adaptivecard.ConvertBreakToEOL(cfg.MessageText) + } - botapiMsg := botapi.NewMessage().AddText(cfg.MessageText) + card, err := adaptivecard.NewTextBlockCard(cfg.MessageText, cfg.MessageTitle, true) + if err != nil { + if !cfg.SilentOutput { + log.Printf( + "\n\nERROR: Failed to create new card using specified text/title values for %q channel in the %q team: %v\n\n", + cfg.Channel, + cfg.Team, + err, + ) + } + // Regardless of silent flag, explicitly note unsuccessful results + appExitCode = 1 + return + } + card.SetFullWidth() + if len(cfg.UserMentions) > 0 { + // Process user mention details specified by user, create user mention + // values that we can attach to the card. + userMentions := make([]adaptivecard.Mention, 0, len(cfg.UserMentions)) for _, mention := range cfg.UserMentions { - if err := botapiMsg.Mention(mention.Name, mention.ID, true); err != nil { + userMention, err := adaptivecard.NewMention(mention.Name, mention.ID) + if err != nil { if !cfg.SilentOutput { - log.Printf("\n\nERROR: Failed to add user mention to message for %q channel in the %q team: %v\n\n", + log.Printf("\n\nERROR: Failed to process user mention for %q channel in the %q team: %v\n\n", cfg.Channel, cfg.Team, err) } // Regardless of silent flag, explicitly note unsuccessful results appExitCode = 1 return } + userMentions = append(userMentions, userMention) + } + + // Add user mention collection to card. + if err := card.AddMention(true, userMentions...); err != nil { + if !cfg.SilentOutput { + log.Printf("\n\nERROR: Failed to add user mentions to message for %q channel in the %q team: %v\n\n", + cfg.Channel, cfg.Team, err) + } + // Regardless of silent flag, explicitly note unsuccessful results + appExitCode = 1 + return } + } + + // If provided, use target URLs and their descriptions to add labelled + // URL "buttons" to Microsoft Teams message. + if len(cfg.TargetURLs) > 0 { - // Add branding trailer content - trailer := fmt.Sprintf( - "\n\n%s", - config.MessageTrailer(cfg.Sender), - ) + // Create dedicated container for all action items. + actionsContainer := adaptivecard.NewContainer() + actionsContainer.Separator = false + actionsContainer.Style = adaptivecard.ContainerStyleEmphasis + actionsContainer.Spacing = adaptivecard.SpacingExtraLarge - // We don't check cfg.ConvertEOL here because we're processing a value - // we just created, not one passed in by the user (and potentially - // expected to remain untouched/unmodified). - trailer = goteamsnotify.ConvertEOLToBreak(trailer) + actions := make([]adaptivecard.Action, 0, len(cfg.TargetURLs)) - botapiMsg.AddText(trailer) + for i := range cfg.TargetURLs { - if cfg.VerboseOutput { - if err := botapiMsg.Prepare(false); err != nil { + urlAction, err := adaptivecard.NewActionOpenURL( + cfg.TargetURLs[i].URL.String(), + cfg.TargetURLs[i].Description, + ) + if err != nil { if !cfg.SilentOutput { - log.Printf("\n\nERROR: Failed to prepare message for %q channel in the %q team: %v\n\n", - cfg.Channel, cfg.Team, err) + log.Printf( + "\n\nERROR: Failed to process openURL action for %q channel in the %q team: %v\n\n", + cfg.Channel, + cfg.Team, + err, + ) } // Regardless of silent flag, explicitly note unsuccessful results appExitCode = 1 return } - - // Emitted at app exit - rawMsg = fmt.Sprintf("botapi Message values sent: %#v\n", botapiMsg) - - fmt.Println(botapiMsg.PrettyPrint()) + actions = append(actions, urlAction) } - // Submit message card using Microsoft Teams client, retry submission if - // needed up to specified number of retry attempts. - sendErr = mstClient.SendWithRetry(ctxSubmissionTimeout, cfg.WebhookURL, botapiMsg, cfg.Retries, cfg.RetriesDelay) - - default: - - // Setup base message card - msgCard := messagecard.NewMessageCard() - msgCard.Title = cfg.MessageTitle - msgCard.Text = cfg.MessageText - msgCard.ThemeColor = cfg.ThemeColor - - // If provided, use target URLs and their descriptions to add labelled URL - // "buttons" to Microsoft Teams message. - if len(cfg.TargetURLs) > 0 { - - // Create dedicated section for all potentialAction items - actionSection := messagecard.NewSection() - actionSection.StartGroup = true - - for i := range cfg.TargetURLs { - - pa, err := messagecard.NewPotentialAction( - messagecard.PotentialActionOpenURIType, - cfg.TargetURLs[i].Description, + if err := actionsContainer.AddAction(true, actions...); err != nil { + if !cfg.SilentOutput { + log.Printf( + "\n\nERROR: Failed to add openURL action to container for %q channel in the %q team: %v\n\n", + cfg.Channel, + cfg.Team, + err, ) - - if err != nil { - log.Println("error encountered when processing target URL:", err) - appExitCode = 1 - - return - } - - pa.PotentialActionOpenURI.Targets = - []messagecard.PotentialActionOpenURITarget{ - { - OS: "default", - URI: cfg.TargetURLs[i].URL.String(), - }, - } - - if err := actionSection.AddPotentialAction(pa); err != nil { - log.Println("error encountered when adding target URL to message:", err) - appExitCode = 1 - - return - } } + // Regardless of silent flag, explicitly note unsuccessful results + appExitCode = 1 + return + } - if err := msgCard.AddSection(actionSection); err != nil { - log.Println("error encountered when adding section value:", err) - appExitCode = 1 - return + if err := card.AddContainer(false, actionsContainer); err != nil { + if !cfg.SilentOutput { + log.Printf("\n\nERROR: Failed to add actions container to card for %q channel in the %q team: %v\n\n", + cfg.Channel, cfg.Team, err) } + // Regardless of silent flag, explicitly note unsuccessful results + appExitCode = 1 + return } + } - // Create branding trailer section - trailerSection := messagecard.NewSection() - trailerSection.Text = config.MessageTrailer(cfg.Sender) - trailerSection.StartGroup = true - - // Add branding trailer section, bail if unexpected error occurs - if err := msgCard.AddSection(trailerSection); err != nil { - log.Println("error encountered when adding section value:", err) + // Process branding trailer content. + // + // NOTE: Unlike MessageCard text which has benefited from \r\n + // (windows), \r (mac) and \n (unix) conversion to
statements in + // the past,
statements in Adaptive Card text remain as-is in the + // final rendered message. This is not useful. + trailerText := fmt.Sprintf( + "\n\n%s", + config.MessageTrailer(cfg.Sender), + ) + + trailerContainer := adaptivecard.NewContainer() + trailerContainer.Separator = true + trailerContainer.Spacing = adaptivecard.SpacingExtraLarge + + trailerTextBlock := adaptivecard.NewTextBlock(trailerText, true) + trailerTextBlock.Size = adaptivecard.SizeSmall + trailerTextBlock.Weight = adaptivecard.WeightLighter + + if err := trailerContainer.AddElement(false, trailerTextBlock); err != nil { + if !cfg.SilentOutput { + log.Printf("\n\nERROR: Failed to add text block to trailer container for card for %q channel in the %q team: %v\n\n", + cfg.Channel, cfg.Team, err) + } + // Regardless of silent flag, explicitly note unsuccessful results + appExitCode = 1 + return + } + if err := card.AddContainer(false, trailerContainer); err != nil { + if !cfg.SilentOutput { + log.Printf("\n\nERROR: Failed to add trailer container to card for %q channel in the %q team: %v\n\n", + cfg.Channel, cfg.Team, err) + } + // Regardless of silent flag, explicitly note unsuccessful results + appExitCode = 1 + return + } + message, err := adaptivecard.NewMessageFromCard(card) + if err != nil { + if !cfg.SilentOutput { + log.Printf( + "\n\nERROR: Failed to create new message from card for %q channel in the %q team: %v\n\n", + cfg.Channel, + cfg.Team, + err, + ) + + // Regardless of silent flag, explicitly note unsuccessful results appExitCode = 1 return } + } - if cfg.VerboseOutput { - if err := msgCard.Prepare(false); err != nil { - if !cfg.SilentOutput { - log.Printf("\n\nERROR: Failed to prepare message for %q channel in the %q team: %v\n\n", - cfg.Channel, cfg.Team, err) - } - // Regardless of silent flag, explicitly note unsuccessful results - appExitCode = 1 - return - } - - // Emitted at app exit - rawMsg = fmt.Sprintf("MessageCard values sent: %#v\n", msgCard) + if cfg.VerboseOutput { + if err := message.Prepare(); err != nil { + log.Printf("\n\nERROR: Failed to prepare message for %q channel in the %q team: %v\n\n", + cfg.Channel, cfg.Team, err) - fmt.Println(msgCard.PrettyPrint()) + // Regardless of silent flag, explicitly note unsuccessful results + appExitCode = 1 + return } - // Submit message card using Microsoft Teams client, retry submission if - // needed up to specified number of retry attempts. - sendErr = mstClient.SendWithRetry(ctxSubmissionTimeout, cfg.WebhookURL, msgCard, cfg.Retries, cfg.RetriesDelay) - + log.Println(message.PrettyPrint()) } + // Submit message card using Microsoft Teams client, retry submission if + // needed up to specified number of retry attempts. + sendErr := mstClient.SendWithRetry(ctxSubmissionTimeout, cfg.WebhookURL, message, cfg.Retries, cfg.RetriesDelay) + switch { case cfg.IgnoreInvalidResponse && errors.Is(sendErr, goteamsnotify.ErrInvalidWebhookURLResponseText): - log.Printf( - "WARNING: invalid response received from %q endpoint", cfg.WebhookURL) - log.Printf("ignoring error response as requested: \n%s", sendErr) + if !cfg.SilentOutput { + log.Printf( + "WARNING: invalid response received from %q endpoint", cfg.WebhookURL) + log.Printf("ignoring error response as requested: \n%s", sendErr) + } // If an error occurred and we were not expecting one. case sendErr != nil: @@ -260,7 +307,7 @@ func main() { if cfg.VerboseOutput { log.Printf("Configuration used: %#v\n", cfg) log.Printf("Webhook URL: %s\n", cfg.WebhookURL) - log.Println(rawMsg) + log.Printf("Message values sent: %#v\n", message) } } diff --git a/go.mod b/go.mod index bd283c3..f476c9f 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,10 @@ module github.com/atc0005/send2teams go 1.17 -require github.com/atc0005/go-teams-notify/v2 v2.7.0-alpha.1 +// Allow for testing local changes before they're published. +// +// replace github.com/atc0005/go-teams-notify/v2 => ../go-teams-notify + +require github.com/atc0005/go-teams-notify/v2 v2.7.0-alpha.2 require github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index fa06bf9..d1f5e05 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ -github.com/atc0005/go-teams-notify/v2 v2.7.0-alpha.1 h1:sMy2AYzVH9u8jNI/LCgsrj8+hL+2qPhLoevjP9EpjTE= -github.com/atc0005/go-teams-notify/v2 v2.7.0-alpha.1/go.mod h1:xo6GejLDHn3tWBA181F8LrllIL0xC1uRsRxq7YNXaaY= +github.com/atc0005/go-teams-notify/v2 v2.7.0-alpha.2 h1:adF/nirZT6nOv9yRViUNc1MDupJlyxk22d3skZOGcgw= +github.com/atc0005/go-teams-notify/v2 v2.7.0-alpha.2/go.mod h1:lbsBxbUisqmvoAncOmM8kupIVTEvGuSenZiaATwU/KU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go index 869decd..b36487e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,7 +19,6 @@ import ( "time" goteamsnotify "github.com/atc0005/go-teams-notify/v2" - "github.com/atc0005/go-teams-notify/v2/messagecard" ) const ( @@ -34,7 +33,7 @@ const ( webhookURLFlagHelp = "The Webhook URL provided by a preconfigured Connector." targetURLFlagHelp = "The target URL and label (specified as comma separated pair) usually visible as a button towards the bottom of the Microsoft Teams message." userMentionFlagHelp = "The DisplayName and ID of the recipient (specified as comma separated pair) for a user mention." - themeColorFlagHelp = "The hex color code used to set the desired trim color on submitted messages." + themeColorFlagHelp = "NOOP; this setting is no longer used. Values specified for this flag are ignored." titleFlagHelp = "The title for the message to submit." messageFlagHelp = "The message to submit. This message may be provided in Markdown format." senderFlagHelp = "The (optional) sending application name or generator of the message this app will attempt to deliver." @@ -44,7 +43,7 @@ const ( // Default flag settings if not overridden by user input const ( - defaultMessageThemeColor string = "#832561" + defaultMessageThemeColor string = "NotUsed" defaultSilentOutput bool = false defaultVerboseOutput bool = false defaultConvertEOL bool = false @@ -155,8 +154,9 @@ type Config struct { // channel that you wish to submit messages to using this application. WebhookURL string - // ThemeColor is a hex color code string representing the desired border - // trim color for our submitted messages. + // ThemeColor is no longer used. Values specified for this flag are + // ignored. If/when the Adaptive Card format adds support for message + // theming (or border color) we can re-enable this setting. ThemeColor string // MessageTitle is the text shown on the top portion of the message "card" @@ -467,54 +467,9 @@ func NewConfig() (*Config, error) { // Validate verifies all struct fields have been provided acceptable values. func (c Config) Validate(disableWebhookURLValidation bool) error { - // Current implementation of user mentions is incompatible with most - // MessageCard settings/values. Future implementation of Adaptive Card - // support in the atc0005/go-teams-notify library is expected to remove - // some/most of these incompatibilities. - switch { - case c.UserMentions != nil: - - if len(c.TargetURLs) > 0 { - return fmt.Errorf("target urls flag is incompatible with user mentions flag") - } - - if c.MessageTitle != "" { - return fmt.Errorf("message title flag is incompatible with user mentions flag") - } - - if c.ThemeColor != defaultMessageThemeColor { - return fmt.Errorf("theme color flag is incompatible with user mentions flag") - } - - default: - // Expected pattern: #832561 - if len(c.ThemeColor) < len(defaultMessageThemeColor) { - - expectedLength := len(defaultMessageThemeColor) - actualLength := len(c.ThemeColor) - return fmt.Errorf("provided message theme color too short; got message %q of length %d, expected length of %d", - c.ThemeColor, actualLength, expectedLength) - } - - // We rely on the Set() method for the flag.Value interface to ensure that - // the required URL and description values are provided for each target - // URL. We verify here that we don't exceed the maximum supported - // potentialActions for the `section` that we will generate. - // - // https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#actions - if len(c.TargetURLs) > messagecard.PotentialActionMaxSupported { - return fmt.Errorf( - "%d target URLs specified, a maximum of %d are supported", - len(c.TargetURLs), - messagecard.PotentialActionMaxSupported, - ) - } - - } - - /* - Shared/common validation checks. - */ + // We rely on the Set() method for the flag.Value interface to ensure that + // the required URL and description values are provided for each target + // URL. if c.SilentOutput && c.VerboseOutput { return fmt.Errorf("unsupported: You cannot have both silent and verbose output") diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/README.md b/vendor/github.com/atc0005/go-teams-notify/v2/README.md index fbe56cf..c291e3e 100644 --- a/vendor/github.com/atc0005/go-teams-notify/v2/README.md +++ b/vendor/github.com/atc0005/go-teams-notify/v2/README.md @@ -1,5 +1,5 @@ -# go-teams-notify +# goteamsnotify A package to send messages to a Microsoft Teams channel. @@ -44,23 +44,33 @@ inclusion into the project. ## Overview The `goteamsnotify` package (aka, `go-teams-notify`) allows sending messages -to a Microsoft Teams channel. +to a Microsoft Teams channel. These messages can be composed of legacy +[`MessageCard`][msgcard-ref] or [`Adaptive Card`][adaptivecard-ref] card +formats. -Simple messages can be composed of only a title and a text body. More complex -messages can be composed of multiple sections, key/value pairs (aka, `Facts`) -and/or externally hosted images. See the [Features](#features) list for more -information. +Simple messages can be created by specifying only a title and a text body. +More complex messages may be composed of multiple sections (`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 consist of one or more sections, key/value pairs (aka, - `Facts`) and/or externally hosted images. or images (hosted externally) -- Support for [`Actions`][msgcard-ref-actions], allowing users to take quick - actions within Microsoft Teams -- Support for [user mentions][botapi-user-mentions] (limited) + - complex messages may consist of multiple sections (`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 + - [`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 @@ -70,6 +80,10 @@ information. - 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 @@ -91,10 +105,18 @@ For more details, see the ## Supported Releases -| Series | Example | Status | -| -------- | -------- | ------------------- | -| `v1.x.x` | `v1.3.1` | Not Supported (EOL) | -| `v2.x.x` | `v2.6.0` | Supported | +| Series | Example | Status | +| -------- | ---------------- | ------------------- | +| `v1.x.x` | `v1.3.1` | Not Supported (EOL) | +| `v2.x.x` | `v2.6.0` | Supported | +| `v3.x.x` | `v3.0.0-alpha.1` | TBD | + +The current plan is to continue extending the v2 branch with new functionality +while retaining backwards compatibility. Any breakage in compatibility for the +v2 series is considered a bug (please report it). + +Long-term, the goal is to learn from missteps made in current releases and +correct as many as possible for a future v3 series. ## Changelog @@ -108,26 +130,6 @@ official release is also provided for further review. ### Add this project as a dependency -Assuming that you're using [Go -Modules](https://blog.golang.org/using-go-modules), add this line to your -imports like so: - -```golang -import ( - // ... - - "github.com/atc0005/go-teams-notify/v2" -) -``` - -Depending on your editor and current settings, your editor may resolve the -import and update your `go.mod` and `go.sum` files accordingly. If not, review -these resources for further information: - -- -- -- - See the [Examples](#examples) section for more details. ### Webhook URLs @@ -185,27 +187,40 @@ shadabacc3934](https://gist.github.com/chusiang/895f6406fbf9285c58ad0a3ace13d025 This is an example of a simple client application which uses this library. -File: [basic](./examples/basic/main.go) +- `Adaptive Card` + - File: [basic](./examples/adaptivecard/basic/main.go) +- `MessageCard` + - File: [basic](./examples/messagecard/basic/main.go) #### User Mention -This example illustrates the use of a user mention. +These examples illustrates the use of one or more user mentions. This feature +is not available in the legacy `MessageCard` card format. -File: [basic](./examples/user-mention/main.go) +- 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 #### Set custom user agent This example illustrates setting a custom user agent. -File: [custom-user-agent](./examples/custom-user-agent/main.go) +- `Adaptive Card` + - File: [custom-user-agent](./examples/adaptivecard/custom-user-agent/main.go) +- `MessageCard` + - File: [custom-user-agent](./examples/messagecard/custom-user-agent/main.go) #### Add an Action -This example illustrates adding an [`OpenUri Action`][msgcard-ref-actions] to -a message card. When used, this action triggers opening a URI in a separate -browser or application. +This example illustrates adding an [`OpenUri`][msgcard-ref-actions] +(`MessageCard`) or [`OpenUrl`][adaptivecard-ref-actions] Action. When used, +this action triggers opening a URL in a separate browser or application. -File: [actions](./examples/actions/main.go) +- `Adaptive Card` + - File: [actions](./examples/adaptivecard/actions/main.go) +- `MessageCard` + - File: [actions](./examples/messagecard/actions/main.go) #### Disable webhook URL prefix validation @@ -213,14 +228,20 @@ 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). -File: [disable-validation](./examples/disable-validation/main.go) +- `Adaptive Card` + - File: [disable-validation](./examples/adaptivecard/disable-validation/main.go) +- `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. -File: [custom-validation](./examples/custom-validation/main.go) +- `Adaptive Card` + - File: [custom-validation](./examples/adaptivecard/custom-validation/main.go) +- `MessageCard` + - File: [custom-validation](./examples/messagecard/custom-validation/main.go) ## Used by @@ -257,4 +278,6 @@ using either this library or the original project. [msgcard-ref]: [msgcard-ref-actions]: -[botapi-user-mentions]: +[adaptivecard-ref]: +[adaptivecard-ref-actions]: +[adaptivecard-user-mentions]: 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..065f933 --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/adaptivecard.go @@ -0,0 +1,2226 @@ +// 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" +) + +// General constants. +const ( + // TypeMessage is the type for an Adaptive Card Message. + TypeMessage string = "message" +) + +// 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. + // + // Version 1.3 is the highest supported for user-generated cards. + // https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#support-for-adaptive-cards + // https://adaptivecards.io/designer + // + // 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#support-for-adaptive-cards + // + // 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." + // + // Per this doc, Teams MAY support the Action.Execute action: + // + // https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model#schema + // + // AdaptiveCardMaxVersion float64 = 1.4 + AdaptiveCardMaxVersion float64 = 1.3 + 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 contants. +// 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" + + // ColumnWidthPixelRegex is a regular expression pattern intended to match + // specific pixel width values (e.g., 50px). + ColumnWidthPixelRegex string = "^[0-9]+px$" + + // ColumnWidthPixelWidthExample is an example of a valid pixel width for a + // Column. + ColumnWidthPixelWidthExample string = "50px" +) + +// 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 contants. +const ( + ChoiceInputStyleCompact string = "compact" + ChoiceInputStyleExpanded string = "expanded" + ChoiceInputStyleFiltered string = "filtered" // Introduced in version 1.5 +) + +// TextInput specific contants. +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 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. +// +// https://adaptivecards.io/explorer/AdaptiveCard.html +// +// TODO: Confirm whether all types are supported. +// NOTE: Based on current docs, version 1.4 is the latest supported at this +// time. +// 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 + TypeElementTextBlock string = "TextBlock" + TypeElementTextRun string = "TextRun" // Introduced in version 1.2 +) + +// 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 Attachments `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. +// +// TODO: Creating a custom type in order to "hang" methods off of it. May not +// need this if we expose bulk of functionality from Message type. +// +// TODO: Use slice of pointers? +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"` +} + +// 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:"uri,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"` + + // 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 the Container element type. 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 by a ColumnSet element type. + Columns []Column `json:"columns,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"` + + // 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"` + + // Separator, when true, indicates that a separating line shown should + // drawn at the top of the element. + Separator bool `json:"separator,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 + +// Column is a container used by a ColumnSet 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". + Type string `json:"type"` + + // 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. Valid + // values consist of fixed strings OR a number representing the relative + // width. + // + // "auto", "stretch", a number representing relative width of the column + // in the column group, or in version 1.1 and higher, a specific 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"` +} + +// 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"` +} + +// 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"` +} + +// 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"` +} + +// 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"` +} + +// 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() + } + + if m.Type != TypeMessage { + return fmt.Errorf( + "invalid message type %q; expected %q: %w", + m.Type, + TypeMessage, + ErrInvalidType, + ) + } + + // We need an attachment (containing one or more Adaptive Cards) in order + // to generate a valid Message for Microsoft Teams delivery. + if len(m.Attachments) == 0 { + return fmt.Errorf( + "required field Attachments is empty for Message: %w", + ErrMissingValue, + ) + } + + for _, attachment := range m.Attachments { + if err := attachment.Validate(); err != nil { + return err + } + } + + // Optional field, but only specific values permitted if set. + if m.AttachmentLayout != "" { + supportedValues := supportedAttachmentLayoutValues() + if !goteamsnotify.InList(m.AttachmentLayout, supportedValues, false) { + return fmt.Errorf( + "invalid %s %q for Message; expected one of %v: %w", + "AttachmentLayout", + m.AttachmentLayout, + supportedValues, + ErrInvalidFieldValue, + ) + } + } + + return nil +} + +// Validate asserts that fields have valid values. +func (a Attachment) Validate() error { + if a.ContentType != AttachmentContentType { + return fmt.Errorf( + "invalid attachment type %q; expected %q: %w", + a.ContentType, + AttachmentContentType, + ErrInvalidType, + ) + } + + return nil +} + +// Validate asserts that fields have valid values. +func (c Card) Validate() error { + if c.Type != TypeAdaptiveCard { + return fmt.Errorf( + "invalid card type %q; expected %q: %w", + c.Type, + TypeAdaptiveCard, + ErrInvalidType, + ) + } + + if c.Schema != "" { + if c.Schema != AdaptiveCardSchema { + return fmt.Errorf( + "invalid Schema value %q; expected %q: %w", + c.Schema, + AdaptiveCardSchema, + ErrInvalidFieldValue, + ) + } + } + + // The Version field is required for top-level cards, optional for + // Cards nested within an Action.ShowCard. + + for _, element := range c.Body { + if err := element.Validate(); err != nil { + return err + } + } + + for _, action := range c.Actions { + if err := action.Validate(); err != nil { + return err + } + } + + // Both are optional fields, unless MinHeight is set in which case + // VerticalContentAlignment is required. + if c.MinHeight != "" && c.VerticalContentAlignment == "" { + return fmt.Errorf( + "field MinHeight is set, VerticalContentAlignment is not;"+ + " field VerticalContentAlignment is only optional when MinHeight"+ + " is not set: %w", + ErrMissingValue, + ) + } + + // If there are recorded user mentions, we need to assert that + // 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 + if len(c.MSTeams.Entities) > 0 { + hasMentionText := func(elements []Element, m Mention) bool { + for _, element := range elements { + if element.HasMentionText(m) { + return true + } + } + return false + } + + // User mentions recorded, but no elements in Card Body to potentially + // contain required text string. + if len(c.Body) == 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. + for _, mention := range c.MSTeams.Entities { + if !hasMentionText(c.Body, mention) { + // Card Body contains no applicable elements with required + // Mention text string. + return fmt.Errorf( + "user mention text not found in elements of Card Body: %w", + ErrMissingValue, + ) + } + } + } + + return nil +} + +// Validate asserts that fields have valid values. +func (tc TopLevelCard) Validate() error { + // Validate embedded Card first as those validation requirements apply + // here also. + if err := tc.Card.Validate(); err != nil { + return err + } + + // The Version field is required for top-level cards (this one), optional + // for Cards nested within an Action.ShowCard. + switch { + case strings.TrimSpace(tc.Version) == "": + 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(tc.Version, 64) + if err != nil { + return fmt.Errorf( + "value %q incompatible with Version field: %w", + tc.Version, + ErrInvalidFieldValue, + ) + } + + // This is a high confidence validation failure. + if versionNum < AdaptiveCardMinVersion { + return fmt.Errorf( + "unsupported version %q;"+ + " expected minimum value of %0.1f: %w", + tc.Version, + 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 +} + +// Validate asserts that fields have valid values. +func (e Element) Validate() error { + supportedElementTypes := supportedElementTypes() + if !goteamsnotify.InList(e.Type, supportedElementTypes, false) { + return fmt.Errorf( + "invalid %s %q for element; expected one of %v: %w", + "Type", + e.Type, + supportedElementTypes, + ErrInvalidType, + ) + } + + // The Text field is required by TextBlock and TextRun elements, but an + // empty string appears to be permitted. Because of this, we do not have + // to assert that a value is present for the field. + + if e.Type == TypeElementImage { + // URL is required for Image element type. + // https://adaptivecards.io/explorer/Image.html + if e.URL == "" { + return fmt.Errorf( + "required URL is empty for %s: %w", + e.Type, + ErrMissingValue, + ) + } + } + + if e.Size != "" { + supportedSizeValues := supportedSizeValues() + if !goteamsnotify.InList(e.Size, supportedSizeValues, false) { + return fmt.Errorf( + "invalid %s %q for element; expected one of %v: %w", + "Size", + e.Size, + supportedSizeValues, + ErrInvalidFieldValue, + ) + } + } + + if e.Weight != "" { + supportedWeightValues := supportedWeightValues() + if !goteamsnotify.InList(e.Weight, supportedWeightValues, false) { + return fmt.Errorf( + "invalid %s %q for element; expected one of %v: %w", + "Weight", + e.Weight, + supportedWeightValues, + ErrInvalidFieldValue, + ) + } + } + + if e.Color != "" { + supportedColorValues := supportedColorValues() + if !goteamsnotify.InList(e.Color, supportedColorValues, false) { + return fmt.Errorf( + "invalid %s %q for element; expected one of %v: %w", + "Color", + e.Color, + supportedColorValues, + ErrInvalidFieldValue, + ) + } + } + + if e.Spacing != "" { + supportedSpacingValues := supportedSpacingValues() + if !goteamsnotify.InList(e.Spacing, supportedSpacingValues, false) { + return fmt.Errorf( + "invalid %s %q for element; expected one of %v: %w", + "Spacing", + e.Spacing, + supportedSpacingValues, + ErrInvalidFieldValue, + ) + } + } + + if e.Style != "" { + // 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) + + switch { + case len(supportedStyleValues) == 0: + return fmt.Errorf( + "invalid %s %q for element; %s values not supported for element: %w", + "Style", + e.Style, + "Style", + ErrInvalidFieldValue, + ) + + case !goteamsnotify.InList(e.Style, supportedStyleValues, false): + return fmt.Errorf( + "invalid %s %q for element; expected one of %v: %w", + "Style", + e.Style, + supportedStyleValues, + ErrInvalidFieldValue, + ) + } + } + + if e.Type == TypeElementContainer { + // Items collection is required for Container element type. + // https://adaptivecards.io/explorer/Container.html + if len(e.Items) == 0 { + return fmt.Errorf( + "required Items collection is empty for %s: %w", + e.Type, + ErrMissingValue, + ) + } + + for _, item := range e.Items { + if err := item.Validate(); err != nil { + return err + } + } + } + + // Used by ColumnSet type, but not required. + for _, column := range e.Columns { + if err := column.Validate(); err != nil { + return err + } + } + + if e.Type == TypeElementActionSet { + // Actions collection is required for ActionSet element type. + // https://adaptivecards.io/explorer/ActionSet.html + if len(e.Actions) == 0 { + return fmt.Errorf( + "required Actions collection is empty for %s: %w", + e.Type, + ErrMissingValue, + ) + } + + for _, action := range e.Actions { + if err := action.Validate(); err != nil { + return err + } + } + } + + if e.Type == TypeElementFactSet { + // Facts collection is required for FactSet element type. + // https://adaptivecards.io/explorer/FactSet.html + if len(e.Facts) == 0 { + return fmt.Errorf( + "required Facts collection is empty for %s: %w", + e.Type, + ErrMissingValue, + ) + } + + for _, fact := range e.Facts { + if err := fact.Validate(); err != nil { + return err + } + } + } + + return nil +} + +// Validate asserts that fields have valid values. +func (c Column) Validate() error { + if c.Type != TypeColumn { + return fmt.Errorf( + "invalid column type %q; expected %q: %w", + c.Type, + TypeColumn, + ErrInvalidType, + ) + } + + if c.Width != nil { + switch v := c.Width.(type) { + // Assert fixed keyword values or valid pixel width. + case string: + v = strings.TrimSpace(v) + + switch v { + case ColumnWidthAuto: + case ColumnWidthStretch: + default: + matched, _ := regexp.MatchString(ColumnWidthPixelRegex, v) + if !matched { + return fmt.Errorf( + "invalid pixel width %q; expected value in format %s: %w", + v, + ColumnWidthPixelWidthExample, + ErrInvalidFieldValue, + ) + } + } + + // 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, + ColumnWidthPixelWidthExample, + ErrInvalidFieldValue, + ) + } + } + + for _, element := range c.Items { + if err := element.Validate(); err != nil { + return err + } + } + + if c.SelectAction != nil { + return c.SelectAction.Validate() + } + + return nil +} + +// Validate asserts that fields have valid values. +func (f Fact) Validate() error { + if f.Title == "" { + return fmt.Errorf( + "required field Title is empty for Fact: %w", + ErrMissingValue, + ) + } + + if f.Value == "" { + return fmt.Errorf( + "required field Value is empty for Fact: %w", + ErrMissingValue, + ) + } + + return nil +} + +// Validate asserts that fields have valid values. +func (m MSTeams) Validate() error { + // If an optional width value is set, assert that it is a valid value. + if m.Width != "" { + supportedValues := supportedMSTeamsWidthValues() + if !goteamsnotify.InList(m.Width, supportedValues, false) { + return fmt.Errorf( + "invalid %s %q for Action; expected one of %v: %w", + "Width", + m.Width, + supportedValues, + ErrInvalidFieldValue, + ) + } + } + + for _, mention := range m.Entities { + if err := mention.Validate(); err != nil { + return err + } + } + + return nil +} + +// Validate asserts that fields have valid values. +func (i ISelectAction) Validate() error { + // Some supportedISelectActionValues are restricted to later Adaptive Card + // schema versions. + supportedValues := supportedISelectActionValues(AdaptiveCardMaxVersion) + if !goteamsnotify.InList(i.Type, supportedValues, false) { + return fmt.Errorf( + "invalid %s %q for ISelectAction; expected one of %v: %w", + "Type", + i.Type, + supportedValues, + ErrInvalidType, + ) + } + + if i.Fallback != "" { + supportedValues := supportedISelectActionFallbackValues(AdaptiveCardMaxVersion) + if !goteamsnotify.InList(i.Fallback, supportedValues, false) { + return fmt.Errorf( + "invalid %s %q for ISelectAction; expected one of %v: %w", + "Fallback", + i.Fallback, + supportedValues, + ErrInvalidFieldValue, + ) + } + } + + if i.Type == TypeActionOpenURL { + if i.URL == "" { + return fmt.Errorf( + "invalid URL for Action: %w", + ErrMissingValue, + ) + } + } + + return nil +} + +// Validate asserts that fields have valid values. +func (a Action) Validate() error { + + // Some Actions are restricted to later Adaptive Card schema versions. + supportedValues := supportedActionValues(AdaptiveCardMaxVersion) + if !goteamsnotify.InList(a.Type, supportedValues, false) { + return fmt.Errorf( + "invalid %s %q for Action; expected one of %v: %w", + "Type", + a.Type, + supportedValues, + ErrInvalidType, + ) + } + + if a.Type == TypeActionOpenURL { + if a.URL == "" { + return fmt.Errorf( + "invalid URL for Action: %w", + ErrMissingValue, + ) + } + } + + if a.Fallback != "" { + supportedValues := supportedActionFallbackValues(AdaptiveCardMaxVersion) + if !goteamsnotify.InList(a.Fallback, supportedValues, false) { + return fmt.Errorf( + "invalid %s %q for Action; expected one of %v: %w", + "Fallback", + a.Fallback, + supportedValues, + ErrInvalidFieldValue, + ) + } + } + + // Optional, but only supported by the Action.ShowCard type. + if a.Type != TypeActionShowCard && a.Card != nil { + return fmt.Errorf( + "error: specifying a Card is unsupported for Action type %q: %w", + a.Type, + ErrInvalidFieldValue, + ) + } + + 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 +} + +// 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 +} + +// 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, + } +} + +// 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 + } +} + +// 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 +} + +// 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. +// +// 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 +} + +// 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 +} 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..3162a7b --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/doc.go @@ -0,0 +1,32 @@ +// 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..0b5818f --- /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..a7cfd1e --- /dev/null +++ b/vendor/github.com/atc0005/go-teams-notify/v2/adaptivecard/getters.go @@ -0,0 +1,310 @@ +// 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. + // NOTE: Based on current docs, version 1.4 is the latest supported at this + // time. + // 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, + TypeElementTextBlock, + TypeElementTextRun, + } +} + +// 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, + } +} + +// 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 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/botapi/botapi.go b/vendor/github.com/atc0005/go-teams-notify/v2/botapi/botapi.go deleted file mode 100644 index 6c971c6..0000000 --- a/vendor/github.com/atc0005/go-teams-notify/v2/botapi/botapi.go +++ /dev/null @@ -1,350 +0,0 @@ -// 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 botapi - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "strings" -) - -const ( - // MessageType is the type for a BotAPI Message. - MessageType string = "message" - - // MentionType is the type for a user mention for a BotAPI Message. - MentionType string = "mention" - - // MentionTextFormatTemplate is the expected format of the Mention.Text - // field value. - MentionTextFormatTemplate string = "%s" -) - -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") -) - -// Message is a minimal representation of the object used to mention one or -// more users in a Teams channel. -// -// https://docs.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/channel-and-group-conversations?tabs=json#add-mentions-to-your-messages -type Message struct { - // Type is required; must be set to "message". - Type string `json:"type"` - - // Text is required; mostly freeform content, but testing shows that the - // "Some User" string (composed of Display Name value) is - // required by Microsoft Teams for each Mention in the entities - // collection. - Text string `json:"text"` - - // Entities is required; a collection of Mention values, one per mentioned - // individual. - Entities []Mention `json:"entities"` - - // payload is a prepared Message in JSON format for submission or pretty - // printing. - payload *bytes.Buffer `json:"-"` -} - -// 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: MessageType, - } -} - -// NewMessage creates a new Message using provided text with required fields -// predefined. -// func NewMessage(text string) *Message { -// return &Message{ -// Type: MessageType, -// Text: text, -// } -// } - -// AddText adds given text to the message for delivery. If specified, this -// method prepends given text instead of appending it. -// -// The caller may directly write to the exported Message Text field in order -// to overwrite existing Message text. The caller then takes responsibility -// for ensuring that any user mention placeholders are explicitly provided for -// the Message Text field in order to comply with API requirements. -// func (m *Message) AddText(text string, prepend bool) *Message { -// switch { -// case strings.TrimSpace(text) == "": -// // Passing an empty text string is effectively a NOOP. -// case prepend: -// m.Text = text + " " + m.Text -// default: -// m.Text += text -// } -// -// return m -// } - -// AddText appends given text to the message for delivery. -// -// As an alternative to using this method, the caller may directly write to -// the exported Message Text field. If opting to use this approach, care -// should be taken by the caller to retain any previously added mention -// placeholders. -func (m *Message) AddText(text string) *Message { - switch { - case strings.TrimSpace(text) == "": - // Passing an empty text string is effectively a NOOP. - default: - m.Text += text - } - - 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 - - // Validation is handled by the Message.Prepare() method. - _ = json.Indent(&prettyJSON, m.payload.Bytes(), "", "\t") - - return prettyJSON.String() - } - - return "" -} - -// Validate performs basic validation of required field values. -func (m Message) Validate() error { - if m.Text == "" { - return fmt.Errorf( - "required Text field is empty: %w", - ErrInvalidFieldValue, - ) - } - - if m.Type != MessageType { - return fmt.Errorf( - "got %s; wanted %s: %w", - m.Type, - MessageType, - ErrInvalidType, - ) - } - - // If we have any recorded user mentions, check each of them. - if len(m.Entities) > 0 { - for _, mention := range m.Entities { - if err := mention.Validate(); err != nil { - return err - } - } - } - - return nil -} - -// Validate performs basic validation of required field values. -func (m Mention) Validate() error { - if m.Type != MentionType { - return fmt.Errorf( - "got %s; wanted %s: %w", - m.Type, - MentionType, - ErrInvalidType, - ) - } - - if m.Text == "" { - return fmt.Errorf( - "required Text field is empty: %w", - ErrInvalidFieldValue, - ) - } - - if m.Mentioned.ID == "" { - return fmt.Errorf( - "required ID field is empty: %w", - ErrInvalidFieldValue, - ) - } - - if m.Mentioned.Name == "" { - return fmt.Errorf( - "required Name field is empty: %w", - ErrInvalidFieldValue, - ) - } - - return nil -} - -// AddMention adds one or many Mention values to a Message. -// -// If specified, the Text field from each given Mention is prepended to the -// Text field of the Message in order to satisfy the API Message format -// requirements. If specified, the given separator is used, otherwise a space -// is assumed. -// -// If the caller opts to not update the Message Text field when adding a -// Mention, the caller is then responsible for ensuring that the Message Text -// field contains a valid match for each mentioned user. -// -// NOTE: Testing indicates that the expected format matches the DisplayName -// field for the user (e.g., "John Doe" instead of "John" or "Doe" or a custom -// format). -func (m *Message) AddMention(prependToText bool, separator string, mentions ...Mention) error { - if len(mentions) == 0 { - return fmt.Errorf( - "func AddMention: missing value: %w", - ErrMissingValue, - ) - } - - for _, mention := range mentions { - if err := mention.Validate(); err != nil { - return fmt.Errorf( - "func AddMention: validation failed: %w", - err, - ) - } - - m.Entities = append(m.Entities, mention) - - // Fallback to single space separator if user didn't specify one. - if separator == "" { - separator = " " - } - - if prependToText { - m.Text = mention.Text + separator + m.Text - } - } - - return nil -} - -// Mention creates a new user Mention to be included in the Message entities -// collection. -// -// This method receives a user's DisplayName, ID and a boolean value used to -// indicate whether a leading text string of the format "John Doe" -// (i.e., a user "mention") should be prepended to the Message Text field. -// -// If the caller opts to not have this method update the Message Text field, -// then the caller will need to ensure that the Message Text field is updated -// to include a matching pattern for every Mention that is included in the -// entities collection for the Message. -// -// NOTE: Brief testing suggests that the user's display name (e.g., "John -// Doe") is required instead of a firstname (e.g., "John"), lastname ("Doe") -// or custom value (e.g., "JD") is required. -// -// The ID value can be an object ID (e.g., -// 5e8b0f4d-2cd4-4e17-9467-b0f6a5c0c4d0) or a UserPrincipalName (e.g., -// NewUser@contoso.onmicrosoft.com). -func (m *Message) Mention(displayName string, id string, prependToText bool) error { - switch { - case displayName == "": - return fmt.Errorf( - "func Mention: required name argument is empty: %w", - ErrMissingValue, - ) - - case id == "": - return fmt.Errorf( - "func Mention: required id argument is empty: %w", - ErrMissingValue, - ) - - default: - mention := Mention{ - Type: MentionType, - // Text: textVal, - Text: fmt.Sprintf(MentionTextFormatTemplate, displayName), - Mentioned: Mentioned{ - ID: id, - Name: displayName, - }, - } - - m.Entities = append(m.Entities, mention) - - if prependToText { - m.Text = mention.Text + " " + m.Text - } - } - - return nil -} - -// Prepare handles tasks needed to prepare a given Message for delivery to an -// endpoint. If specified, tasks are repeated regardless of whether a previous -// Prepare call was made. Validation should be performed by the caller prior -// to calling this method. -func (m *Message) Prepare(recreate bool) error { - if m.payload != nil && !recreate { - return nil - } - - jsonMessage, err := json.Marshal(m) - if err != nil { - return fmt.Errorf( - "failed to prepare message: %w", - err, - ) - } - - m.payload = bytes.NewBuffer(jsonMessage) - - 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 -} diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/botapi/doc.go b/vendor/github.com/atc0005/go-teams-notify/v2/botapi/doc.go deleted file mode 100644 index 8774e3d..0000000 --- a/vendor/github.com/atc0005/go-teams-notify/v2/botapi/doc.go +++ /dev/null @@ -1,26 +0,0 @@ -/* -Package botapi is intended to provide limited support for adding user mention -functionality to messages sent to a Microsoft Teams channel. - -This package is currently a work-in-progress; when complete, this package will -provide support for generating a message equivalent to the example below, -contributed by @ghokun via -https://github.com/atc0005/go-teams-notify/issues/127. - -curl -X POST -H "Content-type: application/json" -d '{ - "type": "message", - "text": "Hey Some User check out this message", - "entities": [ - { - "type":"mention", - "mentioned":{ - "id":"some.user@company.com", - "name":"Some User" - }, - "text": "Some User" - } - ] -}' - -*/ -package botapi diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/doc.go b/vendor/github.com/atc0005/go-teams-notify/v2/doc.go index a48e63a..1535943 100644 --- a/vendor/github.com/atc0005/go-teams-notify/v2/doc.go +++ b/vendor/github.com/atc0005/go-teams-notify/v2/doc.go @@ -25,9 +25,11 @@ 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 (limited) +• Support for user mentions • Configurable validation diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/format.go b/vendor/github.com/atc0005/go-teams-notify/v2/format.go index 9690485..d393711 100644 --- a/vendor/github.com/atc0005/go-teams-notify/v2/format.go +++ b/vendor/github.com/atc0005/go-teams-notify/v2/format.go @@ -14,8 +14,17 @@ import ( "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. +// request) and replacement break value used to provide equivalent formatting +// for MessageCard payloads in Microsoft Teams. const ( // CR LF \r\n (windows) @@ -43,24 +52,24 @@ const ( // msTeamsCodeBlockSubmissionPrefix is the prefix appended to text input // to indicate that the text should be displayed as a codeblock by - // Microsoft Teams. + // 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. + // 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. + // 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. + // string of text by Microsoft Teams for MessageCard payloads. msTeamsCodeSnippetSubmissionSuffix string = "`" ) @@ -68,6 +77,12 @@ const ( // 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 { @@ -80,10 +95,16 @@ func TryToFormatAsCodeBlock(input string) string { 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. +// 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 { @@ -98,7 +119,13 @@ func TryToFormatAsCodeSnippet(input string) string { // 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 +// 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") @@ -115,7 +142,13 @@ func FormatAsCodeBlock(input string) (string, error) { // 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 +// 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") @@ -132,7 +165,12 @@ func FormatAsCodeSnippet(input string) (string, error) { // 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 +// 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 @@ -192,7 +230,13 @@ func formatAsCode(input string, prefix string, suffix string) (string, error) { } // ConvertEOLToBreak converts \r\n (windows), \r (mac) and \n (unix) into
-// HTML/Markdown break statements. +// 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) diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/messagecard.go b/vendor/github.com/atc0005/go-teams-notify/v2/messagecard.go index 62033c0..dcabd19 100644 --- a/vendor/github.com/atc0005/go-teams-notify/v2/messagecard.go +++ b/vendor/github.com/atc0005/go-teams-notify/v2/messagecard.go @@ -17,6 +17,14 @@ import ( "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. @@ -601,23 +609,33 @@ func (mc *MessageCard) Validate() error { return nil } -// Prepare handles tasks needed to prepare a MessageCard for delivery to an -// endpoint. If specified, tasks are repeated regardless of whether a previous -// Prepare call was made. Validation should be performed by the caller prior -// to calling this method. +// 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(recreate bool) error { - if mc.payload != nil && !recreate { - return nil - } - +func (mc *MessageCard) Prepare() error { jsonMessage, err := json.Marshal(mc) if err != nil { - return err + return fmt.Errorf( + "error marshalling MessageCard to JSON: %w", + err, + ) + } + + switch { + case mc.payload == nil: + mc.payload = &bytes.Buffer{} + default: + mc.payload.Reset() } - mc.payload = bytes.NewBuffer(jsonMessage) + _, err = mc.payload.Write(jsonMessage) + if err != nil { + return fmt.Errorf( + "error updating JSON payload for MessageCard: %w", + err, + ) + } return nil } @@ -637,8 +655,6 @@ func (mc *MessageCard) Payload() io.Reader { func (mc *MessageCard) PrettyPrint() string { if mc.payload != nil { var prettyJSON bytes.Buffer - - // Validation is handled by the MessageCard.Prepare() method. _ = json.Indent(&prettyJSON, mc.payload.Bytes(), "", "\t") return prettyJSON.String() @@ -821,7 +837,7 @@ func NewMessageCardSectionImage() MessageCardSectionImage { } // NewMessageCardPotentialAction creates a new MessageCardPotentialAction -// using the provided potential action type and name. The name values defines +// 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. // diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/messagecard/doc.go b/vendor/github.com/atc0005/go-teams-notify/v2/messagecard/doc.go deleted file mode 100644 index 2c22158..0000000 --- a/vendor/github.com/atc0005/go-teams-notify/v2/messagecard/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -/* -Package messagecard provides support for the legacy MessageCard format in -order to generate Microsoft Teams messages. -*/ -package messagecard diff --git a/vendor/github.com/atc0005/go-teams-notify/v2/messagecard/messagecard.go b/vendor/github.com/atc0005/go-teams-notify/v2/messagecard/messagecard.go deleted file mode 100644 index 2dc918c..0000000 --- a/vendor/github.com/atc0005/go-teams-notify/v2/messagecard/messagecard.go +++ /dev/null @@ -1,714 +0,0 @@ -// 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 messagecard - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "strings" -) - -const ( - // PotentialActionOpenURIType is the type that must be used for OpenUri - // potential action. - PotentialActionOpenURIType = "OpenUri" - - // PotentialActionHTTPPostType is the type that must be used for HttpPOST - // potential action. - PotentialActionHTTPPostType = "HttpPOST" - - // PotentialActionActionCardType is the type that must be used for - // ActionCard potential action. - PotentialActionActionCardType = "ActionCard" - - // PotentialActionInvokeAddInCommandType is the type that must be used for - // InvokeAddInCommand potential action. - PotentialActionInvokeAddInCommandType = "InvokeAddInCommand" - - // PotentialActionActionCardInputTextInputType is the type that must be - // used for ActionCard TextInput type. - PotentialActionActionCardInputTextInputType = "TextInput" - - // PotentialActionActionCardInputDateInputType is the type that must be - // used for ActionCard DateInput type. - PotentialActionActionCardInputDateInputType = "DateInput" - - // PotentialActionActionCardInputMultichoiceInputType is the type that - // must be used for ActionCard MultichoiceInput type. - PotentialActionActionCardInputMultichoiceInputType = "MultichoiceInput" -) - -// PotentialActionMaxSupported is the maximum number of actions allowed in a -// PotentialAction collection. -// https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#actions -const PotentialActionMaxSupported = 4 - -// ErrPotentialActionsLimitReached indicates that the maximum supported number -// of potentialAction collection values has been reached for either a -// MessageCard or a Section. -var ErrPotentialActionsLimitReached = errors.New("potential actions collection limit reached") - -// PotentialAction represents potential actions an user can do in a -// message card. See -// https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#actions -// for more information. -type PotentialAction 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"` - - // PotentialActionOpenURI is a set of options for openUri - // potential action. - PotentialActionOpenURI - - // PotentialActionHTTPPOST is a set of options for httpPOST - // potential action. - PotentialActionHTTPPOST - - // PotentialActionActionCard is a set of options for actionCard - // potential action. - PotentialActionActionCard - - // PotentialActionInvokeAddInCommand is a set of options for - // invokeAddInCommand potential action. - PotentialActionInvokeAddInCommand -} - -// PotentialActionOpenURI represents a OpenUri potential action. -type PotentialActionOpenURI struct { - // Targets is a collection of name/value pairs that defines one URI per - // target operating system. Only used for OpenUri action type. - Targets []PotentialActionOpenURITarget `json:"targets,omitempty"` -} - -// PotentialActionHTTPPOST represents a HttpPOST potential action. -type PotentialActionHTTPPOST 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 []PotentialActionHTTPPOSTHeader `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"` -} - -// PotentialActionActionCard represents an actionCard potential -// action. -type PotentialActionActionCard 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 []PotentialActionActionCardInput `json:"inputs,omitempty"` - - // Actions are the available actions. Only used for ActionCard action - // type. - Actions []PotentialActionActionCardAction `json:"actions,omitempty"` -} - -// PotentialActionActionCardAction is used for configuring ActionCard actions -type PotentialActionActionCardAction 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"` - - // PotentialActionOpenURI is used to specify a openUri action - // card's action. - PotentialActionOpenURI - - // PotentialActionHTTPPOST is used to specify a httpPOST action - // card's action. - PotentialActionHTTPPOST -} - -// PotentialActionInvokeAddInCommand represents an invokeAddInCommand -// potential action. -type PotentialActionInvokeAddInCommand 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"` -} - -// PotentialActionOpenURITarget is used for OpenUri action type. -// It defines one URI per target operating system. -type PotentialActionOpenURITarget 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"` -} - -// PotentialActionHTTPPOSTHeader defines a HTTP header used for HttpPOST action type. -type PotentialActionHTTPPOSTHeader struct { - // Name is the header name. - Name string `json:"name,omitempty"` - - // Value is the header value. - Value string `json:"value,omitempty"` -} - -// PotentialActionActionCardInput represents an ActionCard input. -type PotentialActionActionCardInput 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. - PotentialActionActionCardInputMultichoiceInput - - // MessageCardPotentialActionInputTextInput must be defined for InputText - // input type. - PotentialActionActionCardInputTextInput - - // MessageCardPotentialActionInputDateInput must be defined for DateInput - // input type. - PotentialActionActionCardInputDateInput - - // 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"` -} - -// PotentialActionActionCardInputTextInput represents a TextInput -// input used for potential action. -type PotentialActionActionCardInputTextInput 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"` -} - -// PotentialActionActionCardInputMultichoiceInput represents a -// MultichoiceInput input used for potential action. -type PotentialActionActionCardInputMultichoiceInput 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"` -} - -// PotentialActionActionCardInputDateInput represents a DateInput -// input used for potential action. -type PotentialActionActionCardInputDateInput 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"` -} - -// SectionFact represents a section fact entry that is usually displayed in a -// two-column key/value format. -type SectionFact 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"` -} - -// SectionImage represents an image as used by the heroImage and images -// properties of a section. -type SectionImage 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"` -} - -// Section represents a section to include in a message card. -type Section 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 *SectionImage `json:"heroImage,omitempty"` - - // Facts is a collection of SectionFact values. A section entry - // usually is displayed in a two-column key/value format. - Facts []SectionFact `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 []*SectionImage `json:"images,omitempty"` - - // PotentialActions is a collection of actions for a Section. - // This is separate from the actions collection for the MessageCard. - PotentialActions []*PotentialAction `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. -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 []*Section `json:"sections,omitempty"` - - // PotentialActions is a collection of actions for a MessageCard. - PotentialActions []*PotentialAction `json:"potentialAction,omitempty"` - - // payload is a prepared MessageCard in JSON format for submission or - // pretty printing. - payload *bytes.Buffer `json:"-"` -} - -// validatePotentialAction inspects the given *PotentialAction -// and returns an error if a value is missing or not known. -func validatePotentialAction(pa *PotentialAction) error { - if pa == nil { - return fmt.Errorf("nil PotentialAction 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 PotentialAction") - } - - return nil -} - -// addPotentialAction adds one or many PotentialAction values to a -// PotentialActions collection. -func addPotentialAction(collection *[]*PotentialAction, actions ...*PotentialAction) error { - for _, a := range actions { - if err := validatePotentialAction(a); err != nil { - return err - } - - if len(*collection) > PotentialActionMaxSupported { - return fmt.Errorf("func addPotentialAction: failed to add potential action: %w", ErrPotentialActionsLimitReached) - } - - *collection = append(*collection, a) - } - - return nil -} - -// AddSection adds one or many additional Section values to a MessageCard. -// Validation is performed to reject invalid values with an error message. -func (mc *MessageCard) AddSection(section ...*Section) error { - for _, s := range section { - // bail if a completely nil section provided - if s == nil { - return fmt.Errorf("func AddSection: nil Section received") - } - - // Perform validation of all Section fields in an effort to - // avoid adding a Section 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: - return fmt.Errorf("all fields found to be at zero-value, skipping section") - } - - mc.Sections = append(mc.Sections, s) - } - - return nil -} - -// AddPotentialAction adds one or many PotentialAction values to a -// PotentialActions collection on a MessageCard. -func (mc *MessageCard) AddPotentialAction(actions ...*PotentialAction) error { - return addPotentialAction(&mc.PotentialActions, actions...) -} - -// Validate validates a MessageCard calling ValidateFunc if defined, -// otherwise, a default validation occurs. -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 prepare a MessageCard for delivery to an -// endpoint. If specified, tasks are repeated regardless of whether a previous -// Prepare call was made. Validation should be performed by the caller prior -// to calling this method. -func (mc *MessageCard) Prepare(recreate bool) error { - if mc.payload != nil && !recreate { - return nil - } - - jsonMessage, err := json.Marshal(mc) - if err != nil { - return err - } - - mc.payload = bytes.NewBuffer(jsonMessage) - - return nil -} - -// Payload returns the prepared MessageCard payload. The caller should call -// Prepare() prior to calling this method, results are undefined otherwise. -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. -func (mc *MessageCard) PrettyPrint() string { - if mc.payload != nil { - var prettyJSON bytes.Buffer - - // Validation is handled by the MessageCard.Prepare() method. - _ = json.Indent(&prettyJSON, mc.payload.Bytes(), "", "\t") - - return prettyJSON.String() - } - - return "" -} - -// AddFact adds one or many additional SectionFact values to a -// Section -func (mcs *Section) AddFact(fact ...SectionFact) error { - for _, f := range fact { - 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) - } - } - - mcs.Facts = append(mcs.Facts, fact...) - - return nil -} - -// AddFactFromKeyValue accepts a key and slice of values and converts them to -// SectionFact values -func (mcs *Section) 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 := SectionFact{ - Name: key, - 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 PotentialAction values to a -// PotentialActions collection on a Section. This is separate from -// the actions collection for the MessageCard. -func (mcs *Section) AddPotentialAction(actions ...*PotentialAction) 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. -func (mcs *Section) AddImage(sectionImage ...SectionImage) 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. -func (mcs *Section) 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 := SectionImage{ - Image: imageURL, - 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 -// SectionImage argument. This image is used as the centerpiece or -// banner of a message card. -func (mcs *Section) AddHeroImage(heroImage SectionImage) 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 legacy MessageCard with required fields -// predefined. -// -// https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#card-fields -func NewMessageCard() *MessageCard { - return &MessageCard{ - Type: "MessageCard", - Context: "https://schema.org/extensions", - } -} - -// NewSection creates an empty message card section -func NewSection() *Section { - msgCardSection := Section{} - return &msgCardSection -} - -// NewSectionFact creates an empty message card section fact -func NewSectionFact() *SectionFact { - return &SectionFact{} -} - -// NewSectionImage creates an empty image for use with message card -// section -func NewSectionImage() *SectionImage { - return &SectionImage{} -} - -// NewPotentialAction creates a new PotentialAction -// using the provided potential action type and name. The name values defines -// the text that will be displayed on screen for the action. An error is -// returned if invalid values are supplied. -func NewPotentialAction(potentialActionType string, name string) (*PotentialAction, error) { - pa := PotentialAction{ - 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 index 928587e..3348059 100644 --- a/vendor/github.com/atc0005/go-teams-notify/v2/send.go +++ b/vendor/github.com/atc0005/go-teams-notify/v2/send.go @@ -108,7 +108,6 @@ type API interface { // interface in order to support future changes (and not violate backwards // compatibility). type MessageSender interface { - // validateInput(message MessageValidator, webhookURL string) error HTTPClient() *http.Client UserAgent() string ValidateWebhook(webhookURL string) error @@ -122,7 +121,7 @@ type MessageSender interface { // messagePreparer is a message type that supports marshaling its fields // as preparation for delivery to an endpoint. type messagePreparer interface { - Prepare(recreate bool) error + Prepare() error } // messageValidator is a message type that provides validation of its format. @@ -487,7 +486,7 @@ func sendWithContext(ctx context.Context, client MessageSender, webhookURL strin ) } - if err := message.Prepare(false); err != nil { + if err := message.Prepare(); err != nil { return fmt.Errorf( "failed to prepare message: %w", 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/modules.txt b/vendor/modules.txt index cc2787e..82150ef 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,7 +1,6 @@ -# github.com/atc0005/go-teams-notify/v2 v2.7.0-alpha.1 +# github.com/atc0005/go-teams-notify/v2 v2.7.0-alpha.2 ## explicit; go 1.14 github.com/atc0005/go-teams-notify/v2 -github.com/atc0005/go-teams-notify/v2/botapi -github.com/atc0005/go-teams-notify/v2/messagecard +github.com/atc0005/go-teams-notify/v2/adaptivecard # github.com/davecgh/go-spew v1.1.1 ## explicit