Skip to content

Commit

Permalink
Initial support for adaptive cards (#749)
Browse files Browse the repository at this point in the history
* upgrade and placate golangci-lint

* introduce logrus & use for attachments

* extract textblocks from adaptive cards

Fixes: https://mattermost.atlassian.net/browse/MM-60608

* Update server/attachments.go

Co-authored-by: Miguel de la Cruz <[email protected]>

---------

Co-authored-by: Miguel de la Cruz <[email protected]>
  • Loading branch information
lieut-data and mgdelacroix authored Sep 30, 2024
1 parent e6b431b commit 81406a1
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 8 deletions.
76 changes: 70 additions & 6 deletions server/attachments.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"strings"
"time"

"github.com/sirupsen/logrus"

"github.com/mattermost/mattermost-plugin-msteams/server/metrics"
"github.com/mattermost/mattermost-plugin-msteams/server/msteams"
"github.com/mattermost/mattermost-plugin-msteams/server/msteams/clientmodels"
Expand Down Expand Up @@ -95,6 +97,14 @@ func (ah *ActivityHandler) ProcessAndUploadFileToMM(attachmentData []byte, attac
}

func (ah *ActivityHandler) handleAttachments(channelID, userID, text string, msg *clientmodels.Message, chat *clientmodels.Chat, existingFileIDs []string) (string, model.StringArray, string, int, bool) {
var logger logrus.FieldLogger = logrus.StandardLogger()

logger = logger.WithFields(logrus.Fields{
"channel_id": channelID,
"user_id": userID,
"teams_message_id": msg.ID,
})

attachments := []string{}
newText := text
parentID := ""
Expand All @@ -114,7 +124,7 @@ func (ah *ActivityHandler) handleAttachments(channelID, userID, text string, msg

errorFound := false
if client == nil {
ah.plugin.GetAPI().LogWarn("Unable to get the client")
logger.Warn("Unable to get the client to handle attachments")
return "", nil, "", 0, errorFound
}

Expand All @@ -135,6 +145,11 @@ func (ah *ActivityHandler) handleAttachments(channelID, userID, text string, msg

skippedFileAttachments := 0
for _, a := range msg.Attachments {
logger := logger.WithFields(logrus.Fields{
"attachment_id": a.ID,
"attachment_content_type": a.ContentType,
})

// remove the attachment tags from the text
newText = attachRE.ReplaceAllString(newText, "")

Expand All @@ -152,9 +167,16 @@ func (ah *ActivityHandler) handleAttachments(channelID, userID, text string, msg
continue
}

// handle an adaptive card
if a.ContentType == "application/vnd.microsoft.card.adaptive" {
newText = ah.handleCard(logger, a, newText)
countNonFileAttachments++
continue
}

// The rest of the code assumes a (file) reference: ignore other content types until we explicitly support them.
if a.ContentType != "reference" {
ah.plugin.GetAPI().LogWarn("ignored attachment content type", "filename", a.Name, "content_type", a.ContentType)
logger.Warn("ignored attachment content type")
countNonFileAttachments++
continue
}
Expand All @@ -173,23 +195,26 @@ func (ah *ActivityHandler) handleAttachments(channelID, userID, text string, msg
if strings.Contains(a.ContentURL, hostedContentsStr) && strings.HasSuffix(a.ContentURL, "$value") {
attachmentData, err = ah.handleDownloadFile(a.ContentURL, client)
if err != nil {
ah.plugin.GetAPI().LogWarn("failed to download the file", "filename", a.Name, "error", err.Error())
logger.WithError(err).Warn("failed to download the file")
ah.plugin.GetMetrics().ObserveFile(metrics.ActionCreated, metrics.ActionSourceMSTeams, metrics.DiscardedReasonUnableToGetTeamsData, isDirectOrGroupMessage)
skippedFileAttachments++
continue
}
} else {
fileSize, downloadURL, err = client.GetFileSizeAndDownloadURL(a.ContentURL)
if err != nil {
ah.plugin.GetAPI().LogWarn("failed to get file size and download URL", "error", err.Error())
logger.WithError(err).Warn("failed to get file size and download URL")
ah.plugin.GetMetrics().ObserveFile(metrics.ActionCreated, metrics.ActionSourceMSTeams, metrics.DiscardedReasonUnableToGetTeamsData, isDirectOrGroupMessage)
skippedFileAttachments++
continue
}

fileSizeAllowed := *ah.plugin.GetAPI().GetConfig().FileSettings.MaxFileSize
if fileSize > fileSizeAllowed {
ah.plugin.GetAPI().LogWarn("skipping file download from MS Teams because the file size is greater than the allowed size")
logger.WithFields(logrus.Fields{
"file_size": fileSize,
"file_size_allowed": fileSizeAllowed,
}).Warn("skipping file download from MS Teams because the file size is greater than the allowed size")
errorFound = true
ah.plugin.GetMetrics().ObserveFile(metrics.ActionCreated, metrics.ActionSourceMSTeams, metrics.DiscardedReasonMaxFileSizeExceeded, isDirectOrGroupMessage)
skippedFileAttachments++
Expand All @@ -200,7 +225,7 @@ func (ah *ActivityHandler) handleAttachments(channelID, userID, text string, msg
if fileSize <= int64(ah.plugin.GetMaxSizeForCompleteDownload()*1024*1024) {
attachmentData, err = client.GetFileContent(downloadURL)
if err != nil {
ah.plugin.GetAPI().LogWarn("failed to get file content", "error", err.Error())
logger.WithError(err).Warn("failed to get file content")
ah.plugin.GetMetrics().ObserveFile(metrics.ActionCreated, metrics.ActionSourceMSTeams, metrics.DiscardedReasonUnableToGetTeamsData, isDirectOrGroupMessage)
skippedFileAttachments++
continue
Expand Down Expand Up @@ -323,3 +348,42 @@ func (ah *ActivityHandler) handleMessageReference(attach clientmodels.Attachment

return post.Id
}

func (ah *ActivityHandler) handleCard(logger logrus.FieldLogger, attach clientmodels.Attachment, text string) string {
var content struct {
Type string `json:"type"`
Body []struct {
Text string `json:"text"`
Type string `json:"type"`
} `json:"body"`
}
err := json.Unmarshal([]byte(attach.Content), &content)
if err != nil {
logger.WithError(err).Warn("failed to unmarshal card")
return text
}

logger = logger.WithField("card_type", content.Type)

if content.Type != "AdaptiveCard" {
logger.Warn("ignoring unexpected card type")
return text
}

foundContent := false
for _, element := range content.Body {
if element.Type == "TextBlock" {
foundContent = true
text = text + "\n" + element.Text
continue
}

logger.Debug("skipping unsupported element type in card", "element_type", element.Type)
}

if !foundContent {
logger.Warn("failed to find any text to render from card")
}

return text
}
80 changes: 78 additions & 2 deletions server/attachments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/mattermost/mattermost-plugin-msteams/server/msteams/clientmodels"
"github.com/mattermost/mattermost-plugin-msteams/server/store/storemodels"
"github.com/mattermost/mattermost/server/public/model"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -206,6 +207,81 @@ func TestHandleMessageReference(t *testing.T) {
})
}

func TestHandleCard(t *testing.T) {
th := setupTestHelper(t)
logger := logrus.StandardLogger()

t.Run("unable to marshal content", func(t *testing.T) {
th.Reset(t)

originalText := "original text"

attachment := clientmodels.Attachment{
ContentType: "application/vnd.microsoft.card.adaptive",
Content: "Invalid JSON",
}

text := th.p.activityHandler.handleCard(logger, attachment, originalText)
assert.Equal(t, originalText, text)
})

t.Run("unknown card type", func(t *testing.T) {
th.Reset(t)

originalText := "original text"

attachment := clientmodels.Attachment{
ContentType: "application/vnd.microsoft.card.adaptive",
Content: `{"type": "unknown"}`,
}

text := th.p.activityHandler.handleCard(logger, attachment, originalText)
assert.Equal(t, originalText, text)
})

t.Run("no text blocks", func(t *testing.T) {
th.Reset(t)

originalText := "original text"

attachment := clientmodels.Attachment{
ContentType: "application/vnd.microsoft.card.adaptive",
Content: "{\r\n \"type\": \"AdaptiveCard\",\r\n \"body\": [\r\n {\r\n \"items\": [\r\n {\r\n \"text\": \" \",\r\n \"type\": \"TextBlock\"\r\n }\r\n ],\r\n \"backgroundImage\": {\r\n \"url\": \"https://img.huffingtonpost.com/asset/default-entry.jpg?ops=1778_1000\",\r\n \"horizontalAlignment\": \"center\",\r\n \"verticalAlignment\": \"center\"\r\n },\r\n \"bleed\": true,\r\n \"minHeight\": \"180px\",\r\n \"type\": \"Container\"\r\n },\r\n ],\r\n \"$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\",\r\n \"version\": \"1.4\",\r\n \"selectAction\": {\r\n \"url\": \"https://www.huffpost.com/entry/looking-at-cute-animal-pictures-at-work-can-make-you-more-productive_n_1930135\",\r\n \"type\": \"Action.OpenUrl\"\r\n }\r\n}",
}

text := th.p.activityHandler.handleCard(logger, attachment, originalText)
assert.Equal(t, originalText, text)
})

t.Run("sample card 1", func(t *testing.T) {
th.Reset(t)

originalText := "original text"

attachment := clientmodels.Attachment{
ContentType: "application/vnd.microsoft.card.adaptive",
Content: "{\r\n \"type\": \"AdaptiveCard\",\r\n \"body\": [\r\n {\r\n \"items\": [\r\n {\r\n \"text\": \" \",\r\n \"type\": \"TextBlock\"\r\n }\r\n ],\r\n \"backgroundImage\": {\r\n \"url\": \"https://img.huffingtonpost.com/asset/default-entry.jpg?ops=1778_1000\",\r\n \"horizontalAlignment\": \"center\",\r\n \"verticalAlignment\": \"center\"\r\n },\r\n \"bleed\": true,\r\n \"minHeight\": \"180px\",\r\n \"type\": \"Container\"\r\n },\r\n {\r\n \"maxLines\": 2,\r\n \"size\": \"medium\",\r\n \"text\": \"Looking At Cute Animal Pictures At Work Can Make You More Productive, Study Claims\",\r\n \"weight\": \"bolder\",\r\n \"wrap\": true,\r\n \"spacing\": \"Small\",\r\n \"type\": \"TextBlock\"\r\n },\r\n {\r\n \"isSubtle\": true,\r\n \"size\": \"small\",\r\n \"text\": \"The Hufffington Post\",\r\n \"spacing\": \"Small\",\r\n \"type\": \"TextBlock\"\r\n },\r\n {\r\n \"isSubtle\": true,\r\n \"maxLines\": 2,\r\n \"size\": \"small\",\r\n \"text\": \"Best News Ever? Perusing Cute Animal Slideshows May Make You A Better Employee\",\r\n \"wrap\": true,\r\n \"spacing\": \"Small\",\r\n \"type\": \"TextBlock\"\r\n }\r\n ],\r\n \"$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\",\r\n \"version\": \"1.4\",\r\n \"selectAction\": {\r\n \"url\": \"https://www.huffpost.com/entry/looking-at-cute-animal-pictures-at-work-can-make-you-more-productive_n_1930135\",\r\n \"type\": \"Action.OpenUrl\"\r\n }\r\n}",
}

text := th.p.activityHandler.handleCard(logger, attachment, originalText)
assert.Equal(t, "original text\nLooking At Cute Animal Pictures At Work Can Make You More Productive, Study Claims\nThe Hufffington Post\nBest News Ever? Perusing Cute Animal Slideshows May Make You A Better Employee", text)
})

t.Run("sample card 2", func(t *testing.T) {
th.Reset(t)

originalText := "original text"

attachment := clientmodels.Attachment{
ContentType: "application/vnd.microsoft.card.adaptive",
Content: "{\r\n \"type\": \"AdaptiveCard\",\r\n \"body\": [\r\n {\r\n \"text\": \"👋 Hey, are you available now?\",\r\n \"wrap\": true,\r\n \"type\": \"TextBlock\"\r\n },\r\n {\r\n \"actions\": [\r\n {\r\n \"data\": {\r\n \"action\": {\r\n \"id\": \"ab48b33b-740e-43af-9750-51bd8c7d6301:615d26c7-7fa5-474f-b88b-35dcd8105a17:sure\",\r\n \"value\": \"sure\"\r\n },\r\n \"__APP__\": \"__PARROT__\"\r\n },\r\n \"title\": \"👍 Sure, let's go!\",\r\n \"msTeams\": {\r\n \"feedback\": {\r\n \"hide\": true\r\n }\r\n },\r\n \"type\": \"Action.Submit\"\r\n },\r\n {\r\n \"data\": {\r\n \"action\": {\r\n \"id\": \"ab48b33b-740e-43af-9750-51bd8c7d6301:615d26c7-7fa5-474f-b88b-35dcd8105a17:not_now\",\r\n \"value\": \"not_now\"\r\n },\r\n \"__APP__\": \"__PARROT__\"\r\n },\r\n \"title\": \"😴 Not now\",\r\n \"msTeams\": {\r\n \"feedback\": {\r\n \"hide\": true\r\n }\r\n },\r\n \"type\": \"Action.Submit\"\r\n }\r\n ],\r\n \"type\": \"ActionSet\"\r\n }\r\n ],\r\n \"actions\": [],\r\n \"$schema\": \"https://adaptivecards.io/schemas/adaptive-card.json\",\r\n \"version\": \"1.2\"\r\n}",
}

text := th.p.activityHandler.handleCard(logger, attachment, originalText)
assert.Equal(t, "original text\n👋 Hey, are you available now?", text)
})
}

func TestHandleAttachments(t *testing.T) {
th := setupTestHelper(t)
team := th.SetupTeam(t)
Expand Down Expand Up @@ -701,8 +777,8 @@ snippet content
message := &clientmodels.Message{
Attachments: []clientmodels.Attachment{
{
ContentType: "application/vnd.microsoft.card.adaptive",
Content: `{\r\n \"type\": \"AdaptiveCard\",\r\n \"body\": [\r\n {\r\n \"items\": [\r\n {\r\n \"text\": \" \",\r\n \"type\": \"TextBlock\"\r\n }\r\n ],\r\n \"backgroundImage\": {\r\n \"url\": \"https://img.huffingtonpost.com/asset/default-entry.jpg?ops=1778_1000\",\r\n \"horizontalAlignment\": \"center\",\r\n \"verticalAlignment\": \"center\"\r\n },\r\n \"bleed\": true,\r\n \"minHeight\": \"180px\",\r\n \"type\": \"Container\"\r\n },\r\n {\r\n \"maxLines\": 2,\r\n \"size\": \"medium\",\r\n \"text\": \"Looking At Cute Animal Pictures At Work Can Make You More Productive, Study Claims\",\r\n \"weight\": \"bolder\",\r\n \"wrap\": true,\r\n \"spacing\": \"Small\",\r\n \"type\": \"TextBlock\"\r\n },\r\n {\r\n \"isSubtle\": true,\r\n \"size\": \"small\",\r\n \"text\": \"The Hufffington Post\",\r\n \"spacing\": \"Small\",\r\n \"type\": \"TextBlock\"\r\n },\r\n {\r\n \"isSubtle\": true,\r\n \"maxLines\": 2,\r\n \"size\": \"small\",\r\n \"text\": \"Best News Ever? Perusing Cute Animal Slideshows May Make You A Better Employee\",\r\n \"wrap\": true,\r\n \"spacing\": \"Small\",\r\n \"type\": \"TextBlock\"\r\n }\r\n ],\r\n \"$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\",\r\n \"version\": \"1.4\",\r\n \"selectAction\": {\r\n \"url\": \"https://www.huffpost.com/entry/looking-at-cute-animal-pictures-at-work-can-make-you-more-productive_n_1930135\",\r\n \"type\": \"Action.OpenUrl\"\r\n }\r\n}`,
ContentType: "unsupported",
Content: "unsupported",
},
},
ChatID: "",
Expand Down
4 changes: 4 additions & 0 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"

"github.com/mattermost/mattermost-plugin-msteams/assets"
Expand Down Expand Up @@ -417,6 +418,9 @@ func (p *Plugin) onActivate() error {
return err
}

logger := logrus.StandardLogger()
pluginapi.ConfigureLogrus(logger, p.apiClient)

p.botUserID, err = p.apiClient.Bot.EnsureBot(&model.Bot{
Username: botUsername,
DisplayName: botDisplayName,
Expand Down

0 comments on commit 81406a1

Please sign in to comment.