Skip to content

Commit

Permalink
Resolve intermittent user mention failure
Browse files Browse the repository at this point in the history
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 `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
  • Loading branch information
atc0005 committed Apr 10, 2022
1 parent 3ed8dc9 commit 296ef6e
Show file tree
Hide file tree
Showing 20 changed files with 3,073 additions and 1,397 deletions.
91 changes: 49 additions & 42 deletions README.md

Large diffs are not rendered by default.

283 changes: 165 additions & 118 deletions cmd/send2teams/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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())
Expand All @@ -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 <br> statements in
// the past, <br> 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:
Expand Down Expand Up @@ -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)
}

}
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ module github.com/atc0005/send2teams

go 1.17

replace github.com/atc0005/go-teams-notify/v2 => ../go-teams-notify
// 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.1
require github.com/atc0005/go-teams-notify/v2 v2.7.0-alpha.2

require github.com/davecgh/go-spew v1.1.1 // indirect
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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=

0 comments on commit 296ef6e

Please sign in to comment.