// Copyright 2020 Enrico Hoffmann // Copyright 2021 Adam Chalkley // // https://github.com/atc0005/go-teams-notify // // Licensed under the MIT License. See LICENSE file in the project root for // full license information. package goteamsnotify import ( "errors" "fmt" "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" // PotentialActionActionCardInputMultichoiceInput is the type that must be // used for ActionCard MultichoiceInput type. PotentialActionActionCardInputMultichoiceInput = "MultichoiceInput" ) // PotentialActionMaxSupported is the maximum number of actions allowed in a // MessageCardPotentialAction collection. // https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#actions const PotentialActionMaxSupported = 4 // ErrPotentialActionsLimitReached indicates that the maximum supported number // of potentialAction collection values has been reached for either a // MessageCard or a MessageCardSection. var ErrPotentialActionsLimitReached = errors.New("potential actions collection limit reached") // MessageCardPotentialAction 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 MessageCardPotentialAction struct { // Type of the potential action. Can be OpenUri, HttpPOST, ActionCard or // InvokeAddInCommand. Type string `json:"@type"` // Name property defines the text that will be displayed on screen for the // action. Name string `json:"name"` // MessageCardPotentialActionOpenURI is a set of options for openUri // potential action. MessageCardPotentialActionOpenURI // MessageCardPotentialActionHTTPPOST is a set of options for httpPOST // potential action. MessageCardPotentialActionHTTPPOST // MessageCardPotentialActionActionCard is a set of options for actionCard // potential action. MessageCardPotentialActionActionCard // MessageCardPotentialActionInvokeAddInCommand is a set of options for // invokeAddInCommand potential action. MessageCardPotentialActionInvokeAddInCommand } // MessageCardPotentialActionOpenURI represents a OpenUri potential action. type MessageCardPotentialActionOpenURI struct { // Targets is a collection of name/value pairs that defines one URI per // target operating system. Only used for OpenUri action type. Targets []MessageCardPotentialActionOpenURITarget `json:"targets,omitempty"` } // MessageCardPotentialActionHTTPPOST represents a HttpPOST potential action. type MessageCardPotentialActionHTTPPOST struct { // Target defines the URL endpoint of the service that implements the // action. Only used for HttpPOST action type. Target string `json:"target,omitempty"` // Headers is a collection of MessageCardPotentialActionHeader objects // representing a set of HTTP headers that will be emitted when sending // the POST request to the target URL. Only used for HttpPOST action type. Headers []MessageCardPotentialActionHTTPPOSTHeader `json:"headers,omitempty"` // Body is the body of the POST request. Only used for HttpPOST action // type. Body string `json:"body,omitempty"` // BodyContentType is optional and specifies the MIME type of the body in // the POST request. Only used for HttpPOST action type. BodyContentType string `json:"bodyContentType,omitempty"` } // MessageCardPotentialActionActionCard represents an actionCard potential // action. type MessageCardPotentialActionActionCard struct { // Inputs is a collection of inputs an user can provide before processing // the actions. Only used for ActionCard action type. Three types of // inputs are available: TextInput, DateInput and MultichoiceInput Inputs []MessageCardPotentialActionActionCardInput `json:"inputs,omitempty"` // Actions are the available actions. Only used for ActionCard action // type. Actions []MessageCardPotentialActionActionCardAction `json:"actions,omitempty"` } // MessageCardPotentialActionActionCardAction is used for configuring ActionCard actions type MessageCardPotentialActionActionCardAction struct { // Type of the action. Can be OpenUri, HttpPOST, ActionCard or // InvokeAddInCommand. Type string `json:"@type"` // Name property defines the text that will be displayed on screen for the // action. Name string `json:"name"` // MessageCardPotentialActionOpenURI is used to specify a openUri action // card's action. MessageCardPotentialActionOpenURI // MessageCardPotentialActionHTTPPOST is used to specify a httpPOST action // card's action. MessageCardPotentialActionHTTPPOST } // MessageCardPotentialActionInvokeAddInCommand represents an // invokeAddInCommand potential action. type MessageCardPotentialActionInvokeAddInCommand struct { // AddInID specifies the add-in ID of the required add-in. Only used for // InvokeAddInCommand action type. AddInID string `json:"addInId,omitempty"` // DesktopCommandID specifies the ID of the add-in command button that // opens the required task pane. Only used for InvokeAddInCommand action // type. DesktopCommandID string `json:"desktopCommandId,omitempty"` // InitializationContext is an optional field which provides developers a // way to specify any valid JSON object. The value is serialized into a // string and made available to the add-in when the action is executed. // This allows the action to pass initialization data to the add-in. Only // used for InvokeAddInCommand action type. InitializationContext interface{} `json:"initializationContext,omitempty"` } // MessageCardPotentialActionOpenURITarget is used for OpenUri action type. // It defines one URI per target operating system. type MessageCardPotentialActionOpenURITarget struct { // OS defines the operating system the target uri refers to. Supported // operating system values are default, windows, iOS and android. The // default operating system will in most cases simply open the URI in a // web browser, regardless of the actual operating system. OS string `json:"os,omitempty"` // URI defines the URI being called. URI string `json:"uri,omitempty"` } // MessageCardPotentialActionHTTPPOSTHeader defines a HTTP header used for HttpPOST action type. type MessageCardPotentialActionHTTPPOSTHeader struct { // Name is the header name. Name string `json:"name,omitempty"` // Value is the header value. Value string `json:"value,omitempty"` } // MessageCardPotentialActionActionCardInput represents an ActionCard input. type MessageCardPotentialActionActionCardInput struct { // Type of the ActionCard input. // Must be either TextInput, DateInput or MultichoiceInput Type string `json:"@type"` // ID uniquely identifies the input so it is possible to reference it in // the URL or body of an HttpPOST action. ID string `json:"id,omitempty"` // Title defines a title for the input. Title string `json:"title,omitempty"` // Value defines the initial value of the input. For multi-choice inputs, // value must be equal to the value property of one of the input's // choices. Value string `json:"value,omitempty"` // MessageCardPotentialActionInputMultichoiceInput must be defined for // MultichoiceInput input type. MessageCardPotentialActionActionCardInputMultichoiceInput // MessageCardPotentialActionInputTextInput must be defined for InputText // input type. MessageCardPotentialActionActionCardInputTextInput // MessageCardPotentialActionInputDateInput must be defined for DateInput // input type. MessageCardPotentialActionActionCardInputDateInput // IsRequired indicates whether users are required to type a value before // they are able to take an action that would take the value of the input // as a parameter. IsRequired bool `json:"isRequired,omitempty"` } // MessageCardPotentialActionActionCardInputTextInput represents a TextInput // input used for potential action. type MessageCardPotentialActionActionCardInputTextInput struct { // MaxLength indicates the maximum number of characters that can be // entered. MaxLength int `json:"maxLength,omitempty"` // IsMultiline indicates whether the text input should accept multiple // lines of text. IsMultiline bool `json:"isMultiline,omitempty"` } // MessageCardPotentialActionActionCardInputMultichoiceInput represents a // MultichoiceInput input used for potential action. type MessageCardPotentialActionActionCardInputMultichoiceInput struct { // Choices defines the values that can be selected for the multichoice // input. Choices []struct { Display string `json:"display,omitempty"` Value string `json:"value,omitempty"` } `json:"choices,omitempty"` // Style defines the style of the input. When IsMultiSelect is false, // setting the style property to expanded will instruct the host // application to try and display all choices on the screen, typically // using a set of radio buttons. Style string `json:"style,omitempty"` // IsMultiSelect indicates whether or not the user can select more than // one choice. The specified choices will be displayed as a list of // checkboxes. Default value is false. IsMultiSelect bool `json:"isMultiSelect,omitempty"` } // MessageCardPotentialActionActionCardInputDateInput represents a DateInput // input used for potential action. type MessageCardPotentialActionActionCardInputDateInput struct { // IncludeTime indicates whether the date input should allow for the // selection of a time in addition to the date. IncludeTime bool `json:"includeTime,omitempty"` } // MessageCardSectionFact represents a section fact entry that is usually // displayed in a two-column key/value format. type MessageCardSectionFact struct { // Name is the key for an associated value in a key/value pair Name string `json:"name"` // Value is the value for an associated key in a key/value pair Value string `json:"value"` } // MessageCardSectionImage represents an image as used by the heroImage and // images properties of a section. type MessageCardSectionImage struct { // Image is the URL to the image. Image string `json:"image"` // Title is a short description of the image. Typically, this description // is displayed in a tooltip as the user hovers their mouse over the // image. Title string `json:"title"` } // MessageCardSection represents a section to include in a message card. type MessageCardSection struct { // Title is the title property of a section. This property is displayed // in a font that stands out, while not as prominent as the card's title. // It is meant to introduce the section and summarize its content, // similarly to how the card's title property is meant to summarize the // whole card. Title string `json:"title,omitempty"` // Text is the section's text property. This property is very similar to // the text property of the card. It can be used for the same purpose. Text string `json:"text,omitempty"` // ActivityImage is a property used to display a picture associated with // the subject of a message card. For example, this might be the portrait // of a person who performed an activity that the message card is // associated with. ActivityImage string `json:"activityImage,omitempty"` // ActivityTitle is a property used to summarize the activity associated // with a message card. ActivityTitle string `json:"activityTitle,omitempty"` // ActivitySubtitle is a property used to show brief, but extended // information about an activity associated with a message card. Examples // include the date and time the associated activity was taken or the // handle of a person associated with the activity. ActivitySubtitle string `json:"activitySubtitle,omitempty"` // ActivityText is a property used to provide details about the activity. // For example, if the message card is used to deliver updates about a // topic, then this property would be used to hold the bulk of the content // for the update notification. ActivityText string `json:"activityText,omitempty"` // HeroImage is a property that allows for setting an image as the // centerpiece of a message card. This property can also be used to add a // banner to the message card. // Note: heroImage is not currently supported by Microsoft Teams // https://stackoverflow.com/a/45389789 // We use a pointer to this type in order to have the json package // properly omit this field if not explicitly set. // https://github.com/golang/go/issues/11939 // https://stackoverflow.com/questions/18088294/how-to-not-marshal-an-empty-struct-into-json-with-go // https://stackoverflow.com/questions/33447334/golang-json-marshal-how-to-omit-empty-nested-struct HeroImage *MessageCardSectionImage `json:"heroImage,omitempty"` // Facts is a collection of MessageCardSectionFact values. A section entry // usually is displayed in a two-column key/value format. Facts []MessageCardSectionFact `json:"facts,omitempty"` // Images is a property that allows for the inclusion of a photo gallery // inside a section. // We use a slice of pointers to this type in order to have the json // package properly omit this field if not explicitly set. // https://github.com/golang/go/issues/11939 // https://stackoverflow.com/questions/18088294/how-to-not-marshal-an-empty-struct-into-json-with-go // https://stackoverflow.com/questions/33447334/golang-json-marshal-how-to-omit-empty-nested-struct Images []*MessageCardSectionImage `json:"images,omitempty"` // PotentialActions is a collection of actions for a MessageCardSection. // This is separate from the actions collection for the MessageCard. PotentialActions []*MessageCardPotentialAction `json:"potentialAction,omitempty"` // Markdown represents a toggle to enable or disable Markdown formatting. // By default, all text fields in a card and its sections can be formatted // using basic Markdown. Markdown bool `json:"markdown,omitempty"` // StartGroup is the section's startGroup property. This property marks // the start of a logical group of information. Typically, sections with // startGroup set to true will be visually separated from previous card // elements. StartGroup bool `json:"startGroup,omitempty"` } // MessageCard represents a legacy actionable message card used via Office 365 // or Microsoft Teams connectors. type MessageCard struct { // Required; must be set to "MessageCard" Type string `json:"@type"` // Required; must be set to "https://schema.org/extensions" Context string `json:"@context"` // Summary is required if the card does not contain a text property, // otherwise optional. The summary property is typically displayed in the // list view in Outlook, as a way to quickly determine what the card is // all about. Summary appears to only be used when there are sections defined Summary string `json:"summary,omitempty"` // Title is the title property of a card. is meant to be rendered in a // prominent way, at the very top of the card. Use it to introduce the // content of the card in such a way users will immediately know what to // expect. Title string `json:"title,omitempty"` // Text is required if the card does not contain a summary property, // otherwise optional. The text property is meant to be displayed in a // normal font below the card's title. Use it to display content, such as // the description of the entity being referenced, or an abstract of a // news article. Text string `json:"text,omitempty"` // Specifies a custom brand color for the card. The color will be // displayed in a non-obtrusive manner. ThemeColor string `json:"themeColor,omitempty"` // ValidateFunc is a validation function that validates a MessageCard ValidateFunc func() error `json:"-"` // Sections is a collection of sections to include in the card. Sections []*MessageCardSection `json:"sections,omitempty"` // PotentialActions is a collection of actions for a MessageCard. PotentialActions []*MessageCardPotentialAction `json:"potentialAction,omitempty"` } // validatePotentialAction inspects the given *MessageCardPotentialAction // and returns an error if a value is missing or not known. func validatePotentialAction(pa *MessageCardPotentialAction) error { if pa == nil { return fmt.Errorf("nil MessageCardPotentialAction received") } switch pa.Type { case PotentialActionOpenURIType, PotentialActionHTTPPostType, PotentialActionActionCardType, PotentialActionInvokeAddInCommandType: default: return fmt.Errorf("unknown type %s for potential action %s", pa.Type, pa.Name) } if pa.Name == "" { return fmt.Errorf("missing name value for MessageCardPotentialAction") } return nil } // addPotentialAction adds one or many MessageCardPotentialAction values to a // PotentialActions collection. func addPotentialAction(collection *[]*MessageCardPotentialAction, actions ...*MessageCardPotentialAction) error { for _, a := range actions { logger.Printf("addPotentialAction: MessageCardPotentialAction received: %+v\n", a) if err := validatePotentialAction(a); err != nil { logger.Printf("addPotentialAction: validation failed: %v", err) return err } if len(*collection) > PotentialActionMaxSupported { logger.Printf("addPotentialAction: failed to add potential action: %v", ErrPotentialActionsLimitReached.Error()) return fmt.Errorf("func addPotentialAction: failed to add potential action: %w", ErrPotentialActionsLimitReached) } *collection = append(*collection, a) } return nil } // AddSection adds one or many additional MessageCardSection values to a // MessageCard. Validation is performed to reject invalid values with an error // message. func (mc *MessageCard) AddSection(section ...*MessageCardSection) error { for _, s := range section { logger.Printf("AddSection: MessageCardSection received: %+v\n", s) // bail if a completely nil section provided if s == nil { return fmt.Errorf("func AddSection: nil MessageCardSection received") } // Perform validation of all MessageCardSection fields in an effort to // avoid adding a MessageCardSection with zero value fields. This is // done to avoid generating an empty sections JSON array since the // Sections slice for the MessageCard type would technically not be at // a zero value state. Due to this non-zero value state, the // encoding/json package would end up including the Sections struct // field in the output JSON. // See also https://github.com/golang/go/issues/11939 switch { // If any of these cases trigger, skip over the `default` case // statement and add the section. case s.Images != nil: case s.Facts != nil: case s.HeroImage != nil: case s.StartGroup: case s.Markdown: case s.ActivityText != "": case s.ActivitySubtitle != "": case s.ActivityTitle != "": case s.ActivityImage != "": case s.Text != "": case s.Title != "": default: logger.Println("AddSection: No cases matched, all fields assumed to be at zero-value, skipping section") return fmt.Errorf("all fields found to be at zero-value, skipping section") } logger.Println("AddSection: section contains at least one non-zero value, adding section") mc.Sections = append(mc.Sections, s) } return nil } // AddPotentialAction adds one or many MessageCardPotentialAction values to a // PotentialActions collection on a MessageCard. func (mc *MessageCard) AddPotentialAction(actions ...*MessageCardPotentialAction) error { return addPotentialAction(&mc.PotentialActions, actions...) } // Validate validates a MessageCard calling ValidateFunc if defined, // otherwise, a default validation occurs 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 } // AddFact adds one or many additional MessageCardSectionFact values to a // MessageCardSection func (mcs *MessageCardSection) AddFact(fact ...MessageCardSectionFact) error { for _, f := range fact { logger.Printf("AddFact: MessageCardSectionFact received: %+v\n", f) if f.Name == "" { return fmt.Errorf("empty Name field received for new fact: %+v", f) } if f.Value == "" { return fmt.Errorf("empty Name field received for new fact: %+v", f) } } logger.Println("AddFact: section fact contains at least one non-zero value, adding section fact") mcs.Facts = append(mcs.Facts, fact...) return nil } // AddFactFromKeyValue accepts a key and slice of values and converts them to // MessageCardSectionFact values func (mcs *MessageCardSection) AddFactFromKeyValue(key string, values ...string) error { // validate arguments if key == "" { return errors.New("empty key received for new fact") } if len(values) < 1 { return errors.New("no values received for new fact") } fact := MessageCardSectionFact{ Name: key, Value: strings.Join(values, ", "), } // TODO: Explicitly define or use constructor? // fact := NewMessageCardSectionFact() // fact.Name = key // fact.Value = strings.Join(values, ", ") mcs.Facts = append(mcs.Facts, fact) // if we made it this far then all should be well return nil } // AddPotentialAction adds one or many MessageCardPotentialAction values to a // PotentialActions collection on a MessageCardSection. This is separate from // the actions collection for the MessageCard. func (mcs *MessageCardSection) AddPotentialAction(actions ...*MessageCardPotentialAction) error { return addPotentialAction(&mcs.PotentialActions, actions...) } // AddImage adds an image to a MessageCard section. These images are used to // provide a photo gallery inside a MessageCard section. func (mcs *MessageCardSection) AddImage(sectionImage ...MessageCardSectionImage) error { for i := range sectionImage { if sectionImage[i].Image == "" { return fmt.Errorf("cannot add empty image URL") } if sectionImage[i].Title == "" { return fmt.Errorf("cannot add empty image title") } mcs.Images = append(mcs.Images, §ionImage[i]) } return nil } // AddHeroImageStr adds a Hero Image to a MessageCard section using string // arguments. This image is used as the centerpiece or banner of a message // card. func (mcs *MessageCardSection) AddHeroImageStr(imageURL string, imageTitle string) error { if imageURL == "" { return fmt.Errorf("cannot add empty hero image URL") } if imageTitle == "" { return fmt.Errorf("cannot add empty hero image title") } heroImage := MessageCardSectionImage{ Image: imageURL, Title: imageTitle, } // TODO: Explicitly define or use constructor? // heroImage := NewMessageCardSectionImage() // heroImage.Image = imageURL // heroImage.Title = imageTitle mcs.HeroImage = &heroImage // our validation checks didn't find any problems return nil } // AddHeroImage adds a Hero Image to a MessageCard section using a // MessageCardSectionImage argument. This image is used as the centerpiece or // banner of a message card. func (mcs *MessageCardSection) AddHeroImage(heroImage MessageCardSectionImage) error { if heroImage.Image == "" { return fmt.Errorf("cannot add empty hero image URL") } if heroImage.Title == "" { return fmt.Errorf("cannot add empty hero image title") } mcs.HeroImage = &heroImage // our validation checks didn't find any problems return nil } // NewMessageCard creates a new message card with fields required by the // legacy message card format already predefined func NewMessageCard() MessageCard { // define expected values to meet Office 365 Connector card requirements // https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#card-fields msgCard := MessageCard{ Type: "MessageCard", Context: "https://schema.org/extensions", } return msgCard } // NewMessageCardSection creates an empty message card section func NewMessageCardSection() *MessageCardSection { msgCardSection := MessageCardSection{} return &msgCardSection } // NewMessageCardSectionFact creates an empty message card section fact func NewMessageCardSectionFact() MessageCardSectionFact { msgCardSectionFact := MessageCardSectionFact{} return msgCardSectionFact } // NewMessageCardSectionImage creates an empty image for use with message card // section func NewMessageCardSectionImage() MessageCardSectionImage { msgCardSectionImage := MessageCardSectionImage{} return msgCardSectionImage } // NewMessageCardPotentialAction creates a new MessageCardPotentialAction // 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 NewMessageCardPotentialAction(potentialActionType string, name string) (*MessageCardPotentialAction, error) { pa := MessageCardPotentialAction{ Type: potentialActionType, Name: name, } if err := validatePotentialAction(&pa); err != nil { return nil, err } return &pa, nil }