diff --git a/README.md b/README.md index 302c371..043ab88 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ A package to send messages to a Microsoft Teams channel. - [Tables](#tables) - [Set custom user agent](#set-custom-user-agent) - [Add an Action](#add-an-action) + - [Toggle visibility](#toggle-visibility) - [Disable webhook URL prefix validation](#disable-webhook-url-prefix-validation) - [Enable custom patterns' validation](#enable-custom-patterns-validation) - [Used by](#used-by) @@ -242,6 +243,17 @@ this action triggers opening a URL in a separate browser or application. - `MessageCard` - File: [actions](./examples/messagecard/actions/main.go) +#### Toggle visibility + +These examples illustrates using +[`ToggleVisibility`][adaptivecard-ref-actions] Actions to control the +visibility of various Elements of an `Adaptive Card` message. + +- File: [toggle-visibility-single-button](./examples/adaptivecard/toggle-visibility-single-button/main.go) +- File: [toggle-visibility-multiple-buttons](./examples/adaptivecard/toggle-visibility-multiple-buttons/main.go) +- File: [toggle-visibility-column-action](./examples/adaptivecard/toggle-visibility-column-action/main.go) +- File: [toggle-visibility-container-action](./examples/adaptivecard/toggle-visibility-container-action/main.go) + #### Disable webhook URL prefix validation This example disables the validation webhook URLs, including the validation of diff --git a/adaptivecard/adaptivecard.go b/adaptivecard/adaptivecard.go index 3d32d70..94d8e9b 100644 --- a/adaptivecard/adaptivecard.go +++ b/adaptivecard/adaptivecard.go @@ -537,6 +537,17 @@ type Element struct { // "defaults to true" behavior as defined by the schema. FirstRowAsHeaders *bool `json:"firstRowAsHeaders,omitempty"` + // Visible specifies whether this element will be removed from the visual + // tree. + // + // If not specified defaults to true. + // + // NOTE: We define this field as a pointer type so that omitting a value + // for the pointer leaves the field out of the generated JSON payload (due + // to 'omitempty' behavior of the JSON encoder and results in the + // "defaults to true" behavior as defined by the schema. + Visible *bool `json:"isVisible,omitempty"` + // ShowGridLines specified whether grid lines should be displayed. This // field is used by a Table element type. // @@ -554,6 +565,14 @@ type Element struct { // TODO: Should this be a pointer? Actions []Action `json:"actions,omitempty"` + // SelectAction is an Action that will be invoked when the Container + // element is tapped or selected. Action.ShowCard is not supported. + // + // This field is used by supported Container element types (Column, + // ColumnSet, Container). + // + SelectAction *ISelectAction `json:"selectAction,omitempty"` + // Facts is required for the FactSet element type. Actions is a collection // of Fact values that are part of a FactSet element type. Each Fact value // is a key/value pair displayed in tabular form. @@ -777,8 +796,79 @@ type Action struct { // // refs https://github.com/matthidinger/ContosoScubaBot/blob/master/Cards/SubscriberNotification.JSON Card *Card `json:"card,omitempty"` + + // TargetElements is the collection of TargetElement values. + // + // It is not recommended to include Input elements with validation due to + // confusion that can arise from invalid inputs that are not currently + // visible. + // + // https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/input-validation + TargetElements []TargetElement `json:"targetElements,omitempty"` } +// TargetElement represents an entry for Action.ToggleVisibility's +// targetElements property. +// +// - https://adaptivecards.io/explorer/TargetElement.html +// - https://adaptivecards.io/explorer/Action.ToggleVisibility.html +type TargetElement struct { + // ElementID is the ID value of the element to toggle. + ElementID string `json:"elementId"` + + // Visible provides display or visibility control for a target Element. + // + // - If true, always show target element. + // - If false, always hide target element. + // - If not supplied, toggle target element's visibility. + // + // NOTE: We define this field as a pointer type so that omitting a value + // for the pointer leaves the field out of the generated JSON payload (due + // to 'omitempty' behavior of the JSON encoder. If leaving this field out, + // visibility can be toggled for target Elements. + Visible *bool `json:"isVisible,omitempty"` +} + +/* + +General scratch notes for https://github.com/atc0005/go-teams-notify/issues/243 +=============================================================================== + +https://adaptivecards.io/explorer/Action.ToggleVisibility.html +https://adaptivecards.io/explorer/TargetElement.html + +While the targetElements array (JSON) supports raw text strings OR +TargetElement values, we will opt to only support TargetElement values. +Otherwise, we end up needing to use more complicated logic. + +Instead of trying to support this: + + "targetElements": [ + "textToToggle", + "imageToToggle", + "imageToToggle2" + ] + +we support this instead: + + "targetElements": [ + { + "elementId": "textToToggle" + }, + { + "elementId": "imageToToggle" + }, + { + "elementId": "imageToToggle2" + } + ] + + +A Container type has a selectAction field. That slice contains TargetElement +entries. + +*/ + // ISelectAction represents an Action that will be invoked when a container // type (e.g., Column, ColumnSet, Container) is tapped or selected. // Action.ShowCard is not supported. @@ -811,6 +901,17 @@ type ISelectAction struct { // Fallback describes what to do when an unknown element is encountered or // the requirements of this or any children can't be met. Fallback string `json:"fallback,omitempty"` + + // TargetElements is the collection of TargetElement values. + // + // This field is specific to the Action.ToggleVisibility Action type. + // + // It is not recommended to include Input elements with validation due to + // confusion that can arise from invalid inputs that are not currently + // visible. + // + // https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/input-validation + TargetElements []TargetElement `json:"targetElements,omitempty"` } // MSTeams represents a container for properties specific to Microsoft Teams @@ -1214,6 +1315,10 @@ func (e Element) Validate() error { case e.Type == TypeElementColumnSet: v.SelfValidate(Columns(e.Columns)) + if e.SelectAction != nil { + v.SelfValidate(e.SelectAction) + } + // Actions collection is required for ActionSet element type. // https://adaptivecards.io/explorer/ActionSet.html case e.Type == TypeElementActionSet: @@ -1226,6 +1331,10 @@ func (e Element) Validate() error { v.NotEmptyCollection("Items", e.Type, ErrMissingValue, e.Items) v.SelfValidate(Elements(e.Items)) + if e.SelectAction != nil { + v.SelfValidate(e.SelectAction) + } + // URL is required for Image element type. // https://adaptivecards.io/explorer/Image.html case e.Type == TypeElementImage: @@ -1523,6 +1632,50 @@ func (tr TableCell) Validate() error { return v.Err() } +// AddSelectAction adds a given Action or ISelectAction value to the +// associated Column. This action will be invoked when the Column is +// tapped or selected. +// +// An error is returned if the given Action or ISelectAction value fails +// validation or if a value other than an Action or ISelectAction is provided. +func (c *Column) AddSelectAction(action interface{}) error { + switch v := action.(type) { + case Action: + // Perform manual conversion to the supported type. + selectAction := ISelectAction{ + Type: v.Type, + ID: v.ID, + Title: v.Title, + URL: v.URL, + Fallback: v.Fallback, + } + + // Don't touch the new TargetElements field unless the provided Action + // has specified values. + if len(v.TargetElements) > 0 { + selectAction.TargetElements = append( + selectAction.TargetElements, + v.TargetElements..., + ) + } + + c.SelectAction = &selectAction + + case ISelectAction: + c.SelectAction = &v + + // unsupported value provided + default: + return fmt.Errorf( + "error: unsupported value provided; "+ + " only Action or ISelectAction values are supported: %w", + ErrInvalidFieldValue, + ) + } + + return nil +} + // Validate asserts that fields have valid values. func (c Column) Validate() error { v := validator.Validator{} @@ -1595,6 +1748,9 @@ func (m MSTeams) Validate() error { // Validate asserts that fields have valid values. func (i ISelectAction) Validate() error { + supportedISelectActionValues := supportedISelectActionValues(AdaptiveCardMaxVersion) + fallbackValues := supportedActionFallbackValues(AdaptiveCardMaxVersion) + v := validator.Validator{} // Some supportedISelectActionValues are restricted to later Adaptive Card @@ -1603,7 +1759,7 @@ func (i ISelectAction) Validate() error { i.Type, "Type", "ISelectAction", - supportedISelectActionValues(AdaptiveCardMaxVersion), + supportedISelectActionValues, ErrInvalidType, ) @@ -1615,8 +1771,16 @@ func (i ISelectAction) Validate() error { ErrInvalidFieldValue, ) - if i.Type == TypeActionOpenURL { + // See also: Action.Validate() logic. + switch { + case i.Type == TypeActionOpenURL: v.NotEmptyValue(i.URL, "URL", i.Type, ErrMissingValue) + + case i.Fallback != "": + v.InList(i.Fallback, "Fallback", "action", fallbackValues, ErrInvalidFieldValue) + + case i.Type == TypeActionToggleVisibility: + v.NotEmptyCollection("TargetElements", i.Type, ErrMissingValue, i.TargetElements) } return v.Err() @@ -1633,45 +1797,138 @@ func (a Actions) Validate() error { return nil } +// AddTargetElement records the IDs from the given Elements in new +// TargetElement values. The specified visibility setting is used for the new +// TargetElement values. +// +// - If true, always show target Element. +// - If false, always hide target Element. +// - If nil, allow toggling target Element's visibility. +// +// If the given visibility setting is nil, then the visibility setting for the +// TargetElement values is omitted. This enables toggling visibility for the +// target Elements (e.g., toggle button behavior). +func (a *Action) AddTargetElement(visible *bool, elements ...Element) error { + elementIDs := make([]string, 0, len(elements)) + for _, e := range elements { + if strings.TrimSpace(e.ID) == "" { + return fmt.Errorf( + "given Element has empty ID value: %w", + ErrInvalidFieldValue, + ) + } + + elementIDs = append(elementIDs, e.ID) + } + + return a.AddTargetElementID(visible, elementIDs...) +} + +// AddVisibleTargetElement records the Element IDs from the given Elements in +// new TargetElement values. All new TargetElement values are explicitly set +// as visible. +func (a *Action) AddVisibleTargetElement(elements ...Element) error { + visible := true + + return a.AddTargetElement(&visible, elements...) +} + +// AddHiddenTargetElement records the Element IDs from the given Elements in +// new TargetElement values. All new TargetElement values are explicitly set +// as not visible. +func (a *Action) AddHiddenTargetElement(elements ...Element) error { + visible := false + + return a.AddTargetElement(&visible, elements...) +} + +// AddTargetElementID records the given Element ID values in the TargetElements +// collection. A non-empty ID value is required, but the Adaptive Card "tree" +// is not searched for a valid match; it is up to the caller to ensure that +// the given ID value is valid. +// +// The specified visibility setting is used for the new TargetElement values. +// +// - If true, always show target Element. +// - If false, always hide target Element. +// - If nil, allow toggling target Element's visibility. +// +// If the given visibility setting is nil, then the visibility setting for the +// TargetElement values is omitted. This enables toggling visibility for the +// target Elements (e.g., toggle button behavior). +func (a *Action) AddTargetElementID(visible *bool, elementIDs ...string) error { + for _, id := range elementIDs { + if strings.TrimSpace(id) == "" { + return fmt.Errorf( + "received empty Element ID value: %w", + ErrMissingValue, + ) + } + + existingElementIDs := func() []string { + ids := make([]string, 0, len(a.TargetElements)) + for _, targetElement := range a.TargetElements { + ids = append(ids, targetElement.ElementID) + } + + return ids + }() + + // Assert that the ID is not already in the collection. + if goteamsnotify.InList(id, existingElementIDs, false) { + return fmt.Errorf( + "received duplicate Element ID value %q: %w", + id, + ErrInvalidFieldValue, + ) + } + + a.TargetElements = append( + a.TargetElements, + TargetElement{ + ElementID: id, + Visible: visible, + }, + ) + } + + return nil +} + // Validate asserts that fields have valid values. func (a Action) Validate() error { actionValues := supportedActionValues(AdaptiveCardMaxVersion) fallbackValues := supportedActionFallbackValues(AdaptiveCardMaxVersion) - switch { + v := validator.Validator{} + // Some Actions are restricted to later Adaptive Card schema versions. - case !goteamsnotify.InList(a.Type, actionValues, false): - return fmt.Errorf( - "invalid %s %q for Action; expected one of %v: %w", - "Type", - a.Type, - actionValues, - ErrInvalidType, - ) + v.InList(a.Type, "Type", "action", actionValues, ErrInvalidType) - case a.Type == TypeActionOpenURL && a.URL == "": - return fmt.Errorf("invalid URL for Action: %w", ErrMissingValue) + switch { + case a.Type == TypeActionOpenURL: + v.NotEmptyValue(a.URL, "URL", a.Type, ErrMissingValue) - case a.Fallback != "" && - !goteamsnotify.InList(a.Fallback, fallbackValues, false): - return fmt.Errorf( - "invalid %s %q for Action; expected one of %v: %w", - "Fallback", - a.Fallback, - fallbackValues, - ErrInvalidFieldValue, - ) + case a.Fallback != "": + v.InList(a.Fallback, "Fallback", "action", fallbackValues, ErrInvalidFieldValue) + + case a.Type == TypeActionToggleVisibility: + v.NotEmptyCollection("TargetElements", a.Type, ErrMissingValue, a.TargetElements) // Optional, but only supported by the Action.ShowCard type. - case a.Type != TypeActionShowCard && a.Card != nil: + case a.Card != nil: + v.FieldHasSpecificValue(a.Type, "type", TypeActionShowCard, "type", ErrInvalidType) + return fmt.Errorf( "error: specifying a Card is unsupported for Action type %q: %w", a.Type, ErrInvalidFieldValue, ) - default: - return nil } + + // Return the last recorded validation error, or nil if no validation + // errors occurred. + return v.Err() } // Validate asserts that the collection of Mention values are all valid. @@ -2223,6 +2480,36 @@ func NewContainer() Container { return container } +// NewHiddenContainer creates an empty Container whose initial state is +// set as hidden from view. +func NewHiddenContainer() Container { + visible := false + container := Container{ + Type: TypeElementContainer, + Visible: &visible, + } + + return container +} + +// NewColumn creates an empty Column. +func NewColumn() Column { + column := Column{ + Type: TypeColumn, + } + + return column +} + +// NewColumnSet creates an empty Element of type ColumnSet. +func NewColumnSet() Element { + columnSet := Element{ + Type: TypeElementColumnSet, + } + + return columnSet +} + // NewActionSet creates an empty ActionSet. // // TODO: Should we create a type alias for ActionSet, or keep it as a "base" @@ -2247,6 +2534,25 @@ func NewTextBlock(text string, wrap bool) Element { return textBlock } +// NewHiddenTextBlock creates a new TextBlock element using the optional user +// specified Text. If specified, text wrapping is enabled. +// +// The new TextBlock is explicitly hidden from view. To view this Element, the +// caller should set an ID value and then allow toggling visibility by +// referencing this TextBlock's ID from a TargetElement associated with a +// ToggleVisibility Action. +func NewHiddenTextBlock(text string, wrap bool) Element { + isVisible := false + textBlock := Element{ + Type: TypeElementTextBlock, + Wrap: wrap, + Text: text, + Visible: &isVisible, + } + + return textBlock +} + // NewTitleTextBlock uses the specified text to create a new TextBlock // formatted as a "header" or "title" element. If specified, the TextBlock has // text wrapping enabled. The effect is meant to emulate the visual effects of @@ -2665,6 +2971,18 @@ func NewActionOpenURL(url string, title string) (Action, error) { return action, nil } +// NewActionToggleVisibility creates a new Action.ToggleVisibility value using +// the (optionally) provided title text. +// +// NOTE: The caller is responsible for adding required TargetElement values to +// meet validation requirements. +func NewActionToggleVisibility(title string) Action { + return Action{ + Type: TypeActionToggleVisibility, + Title: title, + } +} + // NewActionSetsFromActions creates a new ActionSet for every // TeamsActionsDisplayLimit count of Actions given. An error is returned if // the specified Actions do not pass validation. @@ -2732,6 +3050,9 @@ func (c *Container) AddElement(prepend bool, element Element) error { // If specified, the newly created ActionSets are inserted before other // Elements in the Container, otherwise appended. // +// If adding an action to be used when the Container is tapped or selected use +// AddSelectAction() instead. +// // An error is returned if specified Action values fail validation. func (c *Container) AddAction(prepend bool, actions ...Action) error { // Rely on function to apply validation instead of duplicating it here. @@ -2750,6 +3071,50 @@ func (c *Container) AddAction(prepend bool, actions ...Action) error { return nil } +// AddSelectAction adds a given Action or ISelectAction value to the +// associated Container. This action will be invoked when the Container is +// tapped or selected. +// +// An error is returned if the given Action or ISelectAction value fails +// validation or if a value other than an Action or ISelectAction is provided. +func (c *Container) AddSelectAction(action interface{}) error { + switch v := action.(type) { + case Action: + // Perform manual conversion to the supported type. + selectAction := ISelectAction{ + Type: v.Type, + ID: v.ID, + Title: v.Title, + URL: v.URL, + Fallback: v.Fallback, + } + + // Don't touch the new TargetElements field unless the provided Action + // has specified values. + if len(v.TargetElements) > 0 { + selectAction.TargetElements = append( + selectAction.TargetElements, + v.TargetElements..., + ) + } + + c.SelectAction = &selectAction + + case ISelectAction: + c.SelectAction = &v + + // unsupported value provided + default: + return fmt.Errorf( + "error: unsupported value provided; "+ + " only Action or ISelectAction values are supported: %w", + ErrInvalidFieldValue, + ) + } + + return nil +} + // AddContainer adds the given Container Element to the collection of Element // values for the Card. If specified, the Container Element is inserted at the // beginning of the collection, otherwise appended to the end. diff --git a/examples/adaptivecard/toggle-visibility-column-action/main.go b/examples/adaptivecard/toggle-visibility-column-action/main.go new file mode 100644 index 0000000..822b948 --- /dev/null +++ b/examples/adaptivecard/toggle-visibility-column-action/main.go @@ -0,0 +1,193 @@ +// Copyright 2023 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. + +/* +This example illustrates how to toggle visibility for a container using a +column's select action. + +Of note: + + - default timeout + - package-level logging is disabled by default + - validation of known webhook URL prefixes is *enabled* + +See https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features +for the list of supported Adaptive Card text formatting options. +*/ +package main + +import ( + "log" + "os" + + goteamsnotify "github.com/atc0005/go-teams-notify/v2" + "github.com/atc0005/go-teams-notify/v2/adaptivecard" +) + +func main() { + + // Initialize a new Microsoft Teams client. + mstClient := goteamsnotify.NewTeamsClient() + + // Set webhook url. + // + // NOTE: This is for illustration purposes only. Best practice is to NOT + // hardcode credentials of any kind. + webhookUrl := "https://outlook.office.com/webhook/YOUR_WEBHOOK_URL_OF_TEAMS_CHANNEL" + + // Allow specifying webhook URL via environment variable, fall-back to + // hard-coded value in this example file. + expectedEnvVar := "WEBHOOK_URL" + envWebhookURL := os.Getenv(expectedEnvVar) + switch { + case envWebhookURL != "": + log.Printf( + "Using webhook URL %q from environment variable %q\n\n", + envWebhookURL, + expectedEnvVar, + ) + webhookUrl = envWebhookURL + default: + log.Println(expectedEnvVar, "environment variable not set.") + log.Printf("Using hardcoded value %q as fallback\n\n", webhookUrl) + } + + // Create blank card that we'll manually fill in. + card := adaptivecard.NewCard() + + headerTextBlock := adaptivecard.NewTitleTextBlock("Column SelectAction demo", false) + + if err := card.AddElement(true, headerTextBlock); err != nil { + log.Printf( + "failed to add text block to card body: %v", + err, + ) + os.Exit(1) + } + + showHistoryTextBlock := adaptivecard.NewTextBlock("Show history", false) + showHistoryTextBlock.ID = "showHistory" + + hideHistoryTextBlock := adaptivecard.NewHiddenTextBlock("Hide history", false) + hideHistoryTextBlock.ID = "hideHistory" + + historyDisplayControlColumn := adaptivecard.NewColumn() + historyDisplayControlColumn.Width = 1 + historyDisplayControlColumn.VerticalCellContentAlignment = adaptivecard.VerticalAlignmentCenter + + historyDisplayControlColumn.Items = append( + historyDisplayControlColumn.Items, + &showHistoryTextBlock, + &hideHistoryTextBlock, + ) + + historyItem1TextBlock := adaptivecard.NewTextBlock( + "Event submitted by John Doe on Wed, Dec 6, 2023", + false, + ) + + historyItem2TextBlock := adaptivecard.NewTextBlock( + "Event submitted by Harry Dresden on Wed, Dec 6, 2023", + false, + ) + + historyContainer := adaptivecard.NewHiddenContainer() + historyContainer.ID = "historyContainer" + + if err := historyContainer.AddElement(true, historyItem1TextBlock); err != nil { + log.Printf( + "failed to add text block to container: %v", + err, + ) + os.Exit(1) + } + + if err := historyContainer.AddElement(false, historyItem2TextBlock); err != nil { + log.Printf( + "failed to add text block to container: %v", + err, + ) + os.Exit(1) + } + + historyDisplayColumnSet := adaptivecard.NewColumnSet() + + toggleTargetIDs := []string{ + showHistoryTextBlock.ID, + hideHistoryTextBlock.ID, + historyContainer.ID, + } + + historyDisplayAction := adaptivecard.NewActionToggleVisibility("") + if err := historyDisplayAction.AddTargetElementID(nil, toggleTargetIDs...); err != nil { + log.Printf( + "failed to add element IDs to toggle action: %v", + err, + ) + os.Exit(1) + } + + if err := historyDisplayControlColumn.AddSelectAction(historyDisplayAction); err != nil { + log.Printf( + "failed to add action to column: %v", + err, + ) + os.Exit(1) + } + + historyDisplayColumnSet.Columns = append( + historyDisplayColumnSet.Columns, + historyDisplayControlColumn, + ) + + if err := card.AddElement(false, historyDisplayColumnSet); err != nil { + log.Printf( + "failed to add column set to card body: %v", + err, + ) + os.Exit(1) + } + + if err := card.AddContainer(false, historyContainer); err != nil { + log.Printf( + "failed to add button container to card body: %v", + err, + ) + os.Exit(1) + } + + // Create new Message using Card as input. + msg, err := adaptivecard.NewMessageFromCard(card) + if err != nil { + log.Printf("failed to create message from card: %v", err) + os.Exit(1) + } + + // We explicitly prepare the message for transmission ahead of calling + // mstClient.Send so that we can print the JSON payload in human readable + // format for review. If we do not explicitly prepare the message then the + // mstClient.Send call will handle that for us (which is how this is + // usually handled). + { + if err := msg.Prepare(); err != nil { + log.Printf( + "failed to prepare message: %v", + err, + ) + os.Exit(1) + } + + log.Println(msg.PrettyPrint()) + } + + // Send the message with default timeout/retry settings. + if err := mstClient.Send(webhookUrl, msg); err != nil { + log.Printf("failed to send message: %v", err) + os.Exit(1) + } + +} diff --git a/examples/adaptivecard/toggle-visibility-container-action/main.go b/examples/adaptivecard/toggle-visibility-container-action/main.go new file mode 100644 index 0000000..b6edac2 --- /dev/null +++ b/examples/adaptivecard/toggle-visibility-container-action/main.go @@ -0,0 +1,166 @@ +// Copyright 2023 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. + +/* +This example illustrates how to toggle visibility for a text block using a +container's select action. + +Of note: + +- default timeout +- package-level logging is disabled by default +- validation of known webhook URL prefixes is *enabled* + +See https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features +for the list of supported Adaptive Card text formatting options. +*/ +package main + +import ( + "log" + "os" + + goteamsnotify "github.com/atc0005/go-teams-notify/v2" + "github.com/atc0005/go-teams-notify/v2/adaptivecard" +) + +func main() { + + // Initialize a new Microsoft Teams client. + mstClient := goteamsnotify.NewTeamsClient() + + // Set webhook url. + // + // NOTE: This is for illustration purposes only. Best practice is to NOT + // hardcode credentials of any kind. + webhookUrl := "https://outlook.office.com/webhook/YOUR_WEBHOOK_URL_OF_TEAMS_CHANNEL" + + // Allow specifying webhook URL via environment variable, fall-back to + // hard-coded value in this example file. + expectedEnvVar := "WEBHOOK_URL" + envWebhookURL := os.Getenv(expectedEnvVar) + switch { + case envWebhookURL != "": + log.Printf( + "Using webhook URL %q from environment variable %q\n\n", + envWebhookURL, + expectedEnvVar, + ) + webhookUrl = envWebhookURL + default: + log.Println(expectedEnvVar, "environment variable not set.") + log.Printf("Using hardcoded value %q as fallback\n\n", webhookUrl) + } + + // Create blank card that we'll manually fill in. + card := adaptivecard.NewCard() + + headerTextBlock := adaptivecard.NewTitleTextBlock("Press the link text to show details", false) + + // Details that we'll hide by default but allow toggling visibility for. + detailsMessageBlock := adaptivecard.NewHiddenTextBlock("Details text block content here", true) + detailsMessageBlock.ID = "details" + + cardBodyElements := []adaptivecard.Element{ + headerTextBlock, + detailsMessageBlock, + } + + if err := card.AddElement(true, cardBodyElements...); err != nil { + log.Printf( + "failed to add card body text blocks: %v", + err, + ) + os.Exit(1) + } + + showDetailsTextBlock := adaptivecard.NewTextBlock("Show details", false) + showDetailsTextBlock.ID = "showDetails" + + hideDetailsTextBlock := adaptivecard.NewHiddenTextBlock("Hide details", false) + hideDetailsTextBlock.ID = "hideDetails" + + showHideLinkContainer := adaptivecard.NewContainer() + + if err := showHideLinkContainer.AddElement(true, showDetailsTextBlock); err != nil { + log.Printf( + "failed to add text block to container: %v", + err, + ) + os.Exit(1) + } + + if err := showHideLinkContainer.AddElement(false, hideDetailsTextBlock); err != nil { + log.Printf( + "failed to add text block to container: %v", + err, + ) + os.Exit(1) + } + + toggleTargets := []adaptivecard.Element{ + detailsMessageBlock, + showDetailsTextBlock, + hideDetailsTextBlock, + } + + detailsDisplayAction := adaptivecard.NewActionToggleVisibility("") + if err := detailsDisplayAction.AddTargetElement(nil, toggleTargets...); err != nil { + log.Printf( + "failed to add element IDs to toggle action: %v", + err, + ) + os.Exit(1) + } + + if err := showHideLinkContainer.AddSelectAction(detailsDisplayAction); err != nil { + log.Printf( + "failed to add action to container: %v", + err, + ) + os.Exit(1) + } + + if err := card.AddContainer(false, showHideLinkContainer); err != nil { + log.Printf( + "failed to add button container to card body: %v", + err, + ) + os.Exit(1) + } + + // Create new Message using Card as input. + msg, err := adaptivecard.NewMessageFromCard(card) + if err != nil { + log.Printf("failed to create message from card: %v", err) + os.Exit(1) + } + + // We explicitly prepare the message for transmission ahead of calling + // mstClient.Send so that we can print the JSON payload in human readable + // format for review. If we do not explicitly prepare the message then the + // mstClient.Send call will handle that for us (which is how this is + // usually handled). + { + if err := msg.Prepare(); err != nil { + log.Printf( + "failed to prepare message: %v", + err, + ) + os.Exit(1) + } + + log.Println(msg.PrettyPrint()) + } + + // Send the message with default timeout/retry settings. + if err := mstClient.Send(webhookUrl, msg); err != nil { + log.Printf("failed to send message: %v", err) + os.Exit(1) + } + +} diff --git a/examples/adaptivecard/toggle-visibility-multiple-buttons/main.go b/examples/adaptivecard/toggle-visibility-multiple-buttons/main.go new file mode 100644 index 0000000..0e65055 --- /dev/null +++ b/examples/adaptivecard/toggle-visibility-multiple-buttons/main.go @@ -0,0 +1,186 @@ +// Copyright 2023 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. + +/* +This example uses three action "buttons" on the main card body to illustrate +toggling visibility states for multiple elements. + +While this example aims to showcase one or more specific features it may not +illustrate overall best practices. + +Of note: + +- default timeout +- package-level logging is disabled by default +- validation of known webhook URL prefixes is *enabled* + +See https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features +for the list of supported Adaptive Card text formatting options. +*/ +package main + +import ( + "log" + "os" + + goteamsnotify "github.com/atc0005/go-teams-notify/v2" + "github.com/atc0005/go-teams-notify/v2/adaptivecard" +) + +func main() { + + // Initialize a new Microsoft Teams client. + mstClient := goteamsnotify.NewTeamsClient() + + // Set webhook url. + // + // NOTE: This is for illustration purposes only. Best practice is to NOT + // hardcode credentials of any kind. + webhookUrl := "https://outlook.office.com/webhook/YOUR_WEBHOOK_URL_OF_TEAMS_CHANNEL" + + // Allow specifying webhook URL via environment variable, fall-back to + // hard-coded value in this example file. + expectedEnvVar := "WEBHOOK_URL" + envWebhookURL := os.Getenv(expectedEnvVar) + switch { + case envWebhookURL != "": + log.Printf( + "Using webhook URL %q from environment variable %q\n\n", + envWebhookURL, + expectedEnvVar, + ) + webhookUrl = envWebhookURL + default: + log.Println(expectedEnvVar, "environment variable not set.") + log.Printf("Using hardcoded value %q as fallback\n\n", webhookUrl) + } + + // Create blank card that we'll manually fill in. + card := adaptivecard.NewCard() + + // First text block that we'll use as our header. + headerTextBlock := adaptivecard.NewTitleTextBlock("Press the buttons to toggle visibility", false) + + // This element is intended to remain visible so we skip setting an ID + // value. If we did want to change its visibility we would need to set a + // unique ID value as shown below. + // + // headerTextBlock.ID = "headerBlock" + + textBlock1 := adaptivecard.NewHiddenTextBlock("Text Block 1", true) + textBlock1.ID = "textBlock1" + + textBlock2 := adaptivecard.NewHiddenTextBlock("Text Block 2", true) + textBlock2.ID = "textBlock2" + + textBlock3 := adaptivecard.NewHiddenTextBlock("Text Block 3", true) + textBlock3.ID = "textBlock3" + + // This grouping is used for convenience. + allTextBlocks := []adaptivecard.Element{ + headerTextBlock, + textBlock1, + textBlock2, + textBlock3, + } + + toggleTargets := []adaptivecard.Element{ + textBlock1, + textBlock2, + textBlock3, + } + + if err := card.AddElement(true, allTextBlocks...); err != nil { + log.Printf( + "failed to add text blocks to card: %v", + err, + ) + os.Exit(1) + } + + toggleButton := adaptivecard.NewActionToggleVisibility("Toggle!") + if err := toggleButton.AddTargetElement(nil, toggleTargets...); err != nil { + log.Printf( + "failed to add element IDs to toggle button: %v", + err, + ) + os.Exit(1) + } + + showButton := adaptivecard.NewActionToggleVisibility("Show!") + if err := showButton.AddVisibleTargetElement(toggleTargets...); err != nil { + log.Printf( + "failed to add element IDs to show button: %v", + err, + ) + os.Exit(1) + } + + hideButton := adaptivecard.NewActionToggleVisibility("Hide!") + if err := hideButton.AddHiddenTargetElement(toggleTargets...); err != nil { + log.Printf( + "failed to add element IDs to hide button: %v", + err, + ) + os.Exit(1) + } + + if err := card.AddAction(true, toggleButton); err != nil { + log.Printf( + "failed to add toggle button action to card: %v", + err, + ) + os.Exit(1) + } + + if err := card.AddAction(false, showButton); err != nil { + log.Printf( + "failed to add show button action to card: %v", + err, + ) + os.Exit(1) + } + + if err := card.AddAction(false, hideButton); err != nil { + log.Printf( + "failed to add hide button action to card: %v", + err, + ) + os.Exit(1) + } + + // Create new Message using Card as input. + msg, err := adaptivecard.NewMessageFromCard(card) + if err != nil { + log.Printf("failed to create message from card: %v", err) + os.Exit(1) + } + + // We explicitly prepare the message for transmission ahead of calling + // mstClient.Send so that we can print the JSON payload in human readable + // format for review. If we do not explicitly prepare the message then the + // mstClient.Send call will handle that for us (which is how this is + // usually handled). + { + if err := msg.Prepare(); err != nil { + log.Printf( + "failed to prepare message: %v", + err, + ) + os.Exit(1) + } + + log.Println(msg.PrettyPrint()) + } + + // Send the message with default timeout/retry settings. + if err := mstClient.Send(webhookUrl, msg); err != nil { + log.Printf("failed to send message: %v", err) + os.Exit(1) + } + +} diff --git a/examples/adaptivecard/toggle-visibility-single-button/main.go b/examples/adaptivecard/toggle-visibility-single-button/main.go new file mode 100644 index 0000000..1afb74a --- /dev/null +++ b/examples/adaptivecard/toggle-visibility-single-button/main.go @@ -0,0 +1,121 @@ +// Copyright 2023 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. + +/* +This example uses a single "button" to illustrate toggling visibility for a +"details" text block. + +While this example aims to showcase one or more specific features it may not +illustrate overall best practices. + +Of note: + +- default timeout +- package-level logging is disabled by default +- validation of known webhook URL prefixes is *enabled* + +See https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features +for the list of supported Adaptive Card text formatting options. +*/ +package main + +import ( + "log" + "os" + + goteamsnotify "github.com/atc0005/go-teams-notify/v2" + "github.com/atc0005/go-teams-notify/v2/adaptivecard" +) + +func main() { + + // Initialize a new Microsoft Teams client. + mstClient := goteamsnotify.NewTeamsClient() + + // Set webhook url. + // + // NOTE: This is for illustration purposes only. Best practice is to NOT + // hardcode credentials of any kind. + webhookUrl := "https://outlook.office.com/webhook/YOUR_WEBHOOK_URL_OF_TEAMS_CHANNEL" + + // Allow specifying webhook URL via environment variable, fall-back to + // hard-coded value in this example file. + expectedEnvVar := "WEBHOOK_URL" + envWebhookURL := os.Getenv(expectedEnvVar) + switch { + case envWebhookURL != "": + log.Printf( + "Using webhook URL %q from environment variable %q\n\n", + envWebhookURL, + expectedEnvVar, + ) + webhookUrl = envWebhookURL + default: + log.Println(expectedEnvVar, "environment variable not set.") + log.Printf("Using hardcoded value %q as fallback\n\n", webhookUrl) + } + + // Create blank card that we'll manually fill in. + card := adaptivecard.NewCard() + + // First text block that we'll use as our header. + headerTextBlock := adaptivecard.NewTitleTextBlock("Press the button to show details", false) + + // This element is intended to remain visible so we skip setting an ID + // value. If we did want to change its visibility we would need to set a + // unique ID value as shown below. + // + // headerTextBlock.ID = "headerBlock" + + detailsBlock := adaptivecard.NewHiddenTextBlock("Details text block content here", true) + detailsBlock.ID = "detailsBlock" + + // This grouping is used for convenience. + allTextBlocks := []adaptivecard.Element{ + headerTextBlock, + detailsBlock, + } + + if err := card.AddElement(true, allTextBlocks...); err != nil { + log.Printf( + "failed to add text blocks to card: %v", + err, + ) + os.Exit(1) + } + + toggleButton := adaptivecard.NewActionToggleVisibility("Toggle!") + if err := toggleButton.AddTargetElement(nil, detailsBlock); err != nil { + log.Printf( + "failed to add element ID to toggle button: %v", + err, + ) + os.Exit(1) + } + + if err := card.AddAction(true, toggleButton); err != nil { + log.Printf( + "failed to add toggle button action to card: %v", + err, + ) + os.Exit(1) + } + + // Create new Message using Card as input. + msg, err := adaptivecard.NewMessageFromCard(card) + if err != nil { + log.Printf("failed to create message from card: %v", err) + os.Exit(1) + } + + // Send the message with default timeout/retry settings. + if err := mstClient.Send(webhookUrl, msg); err != nil { + log.Printf("failed to send message: %v", err) + os.Exit(1) + } + +}