I'm developing a bot using Bot Framework SDK 4 with Node.js as an MS-Teams app. The bot is properly registered in Azure but hosted locally with Ngrok forwarding for testing purposes.
The goal of the bot is to do external requests to an api and then populate an adaptive card template from the response of the api for a more engaging interface than text.
This template was created with the Adaptive Card Designer, where it rendered just fine. Testing the bot with the Emulator works too. Same with the Web Chat channel, see screenshots. However when installed in teams, the card is not shown and an error is thrown.
The Errors I get when chatting in Teams:
All other dialog and messages work just fine, this error just happens when I send the adaptive card as an attachment to an activity.
Next I'll paste some code snippets to give some context to what I tried to do
The template I created:
{
"type": "AdaptiveCard",
"$schema": "https://adaptivecards.io/schemas/1.2.0/adaptive-card.json",
"version": "1.2",
"contentType": "application/vnd.microsoft.card.adaptive",
"speak": "not set",
"body": [
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "auto",
"items": [
{
"type": "TextBlock",
"size": "Large",
"text": "${title}",
"weight": "Bolder"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "${type}",
"color": "Accent",
"height": "stretch",
"fontType": "Default",
"size": "Large",
"weight": "Lighter"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "Private",
"horizontalAlignment": "Right",
"height": "stretch",
"size": "Large",
"color": "Warning",
"$when": "${$root.private == '1'}"
}
]
}
]
},
{
"type": "TextBlock",
"spacing": "None",
"text": "Created: {{DATE(${createdUtc},SHORT)}}",
"isSubtle": true,
"size": "Small",
"weight": "Lighter",
"wrap": true,
"$when": "${$root.createdSet == '1'}"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "auto",
"items": [
{
"type": "TextBlock",
"text": "Creator",
"weight": "Bolder",
"color": "Accent"
},
{
"type": "TextBlock",
"weight": "Bolder",
"text": "${creator.name}",
"wrap": true,
"spacing": "None"
}
],
"spacing": "Medium"
},
{
"type": "Column",
"width": "auto",
"items": [
{
"type": "TextBlock",
"text": "Assignee",
"weight": "Bolder",
"color": "Accent"
},
{
"type": "TextBlock",
"weight": "Bolder",
"wrap": true,
"spacing": "None",
"text": "${$root.assignee.name}"
}
],
"spacing": "Medium"
},
{
"type": "Column",
"width": "auto",
"spacing": "Medium",
"items": [
{
"type": "TextBlock",
"text": "Priority",
"weight": "Bolder",
"color": "Accent"
},
{
"type": "TextBlock",
"weight": "Bolder",
"text": "${priority}",
"wrap": true,
"spacing": "None"
}
]
},
{
"type": "Column",
"width": "auto",
"spacing": "Medium",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"color": "Accent",
"text": "Status"
},
{
"type": "TextBlock",
"weight": "Bolder",
"text": "${status}",
"wrap": true,
"spacing": "None"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "Due Date",
"weight": "Bolder",
"color": "Accent"
},
{
"type": "TextBlock",
"weight": "Bolder",
"text": "{{DATE(${dueUTC},SHORT)}}",
"wrap": true,
"spacing": "None"
}
],
"spacing": "Medium",
"$when": "${$root.status!= 'Done' && ${$root.dueSet == '1'}}"
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "Done Date",
"weight": "Bolder",
"color": "Accent"
},
{
"type": "TextBlock",
"weight": "Bolder",
"text": "{{DATE(${doneUTC},SHORT)}}",
"wrap": true,
"spacing": "None"
}
],
"spacing": "Medium",
"$when": "${$root.status== 'Done' && ${$root.doneSet == '1'}}"
}
],
"spacing": "Small",
"separator": true
},
{
"type": "TextBlock",
"text": "${description}",
"wrap": true,
"separator": true
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "View",
"url": "${viewUrl}"
}
]
}
I populate the template variables like this in my issueCardGenerator class:
("issue" is an object containing information I'd like to send to the user as an adaptive card. "issueCard" is a reference to the json template. The specifics on how I parse the information does not matter too much here. "translate" for example translates some enum's of the issue object to natural language.)
generateIssueCard(issue){
//render card
var template = new Template(issueCard);
var cardPayload = template.expand({
$root: {
title: issue.title==null ? "No Title available" : issue.title,
description: issue.description==null ? "": issue.description,
creator: {
name: issue.creator==null ? "Not Set" : issue.creator.username
},
assignee: {
name: issue.assignee==null ? "Not Set" : issue.assignee.username
},
type: issue.type==null ? "" : this.translate(issue.type.value),
status: issue.status==null ? "No Status" : this.translate(issue.status.value),
priority: issue.priority==null ? "No Priority" : this.translate(issue.priority.value),
//only display times when they are set
dueSet: issue.due_date==null ? "0" : "1",
doneSet: issue.done_date==null ? "0" : "1",
createdSet: issue.created==null ? "0" : "1",
//convert time format
dueUtc: issue.due_date==null ? "No Due Date" : moment(issue.due_date).format('YYYY-MM-DD')+'T06:08:39Z',
createdUtc: issue.created==null ? "No Creation Time" : moment(issue.created).format('YYYY-MM-DD')+'T06:08:39Z',
doneUtc: issue.done_date==null ? "No Done Date" : moment(issue.done_date).format('YYYY-MM-DD')+'T06:08:39Z',
viewUrl: "https://www.some-url.com",
private: issue.private==null ? "0" :issue.private
}
});
var adaptiveCard = new AdaptiveCard();
adaptiveCard.parse(cardPayload);
return(CardFactory.adaptiveCard(adaptiveCard))
}
And then send it like this in my dialog-step, which is part of a waterfall dialog:
(The user inputs an "idInput" to sepcify the id of the issue which is then requested and saved in "issue")
async summaryStep(stepContext) {
const idInput = stepContext.result;
if (idInput) {
var issue;
//do request and set issue variable here. This works fine and shows proper in the logs
issue= await this.doRequest(url,accessToken,"GET");
var cardGenerator=new IssueCardGenerator();
var activity= {
text: 'This is your requested issue with the id ' +idInput +': ',
attachments: [cardGenerator.generateIssueCard(issue)]
}
await stepContext.context.sendActivity(activity);
}
return await stepContext.endDialog();
Solved the "problem"
Here is the solution if anyone stumbles onto the same issue:
Turns out in my CardGenerator...
var adaptiveCard = new AdaptiveCard();
adaptiveCard.parse(cardPayload);
return(CardFactory.adaptiveCard(adaptiveCard))
..needs to be changed to use the payload directly for the cardfactory:
return(CardFactory.adaptiveCard(cardPayload))
Why the other channels handle this properly with the first variant and Teams does not, I don't know and probably never will