Monday 24 February 2020

Microsoft Bot Framework v4: Send proactive messages to Teams channels and users

What is a Bot Framework Proactive message?

Usually, for starting a conversation with a Microsoft Teams bot, the user has to initiate the conversation either by sending a personal message to the bot, or by mentioning the bot in a Teams channel or by invoking a messaging extension.

With proactive messaging, the bot can start a conversation with a user (or in a Teams channel) without anyone having to invoke the bot first. This conversation can be started based on any custom logic fit for your application e.g. The occurrence of  an external event, or a webhook getting triggered or even on a scheduled periodical basis.

More about Bot Framework proactive messages here: https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-proactive-message?view=azure-bot-service-4.0&tabs=csharp

I should mention that the bot will first need to be installed in the Team, if you want to send a proactive message to a Teams channel, or to the users who are part of that team.



How to send proactive messages?

So in this post, let's look at a few code samples which make it very easy for our Teams Bot to send proactive messages to users or channels in a Team.

These code samples are based on a standalone .NET Core console app. This is mainly to show that as long as you have the necessary information, your code doesn't need to be running under the Bot messaging endpoint. Once you have the information from the Bot messaging endpoint, the proactive messaging code can run from any platform e.g. from an Azure Function.

If you have a look at the Bot Framework code samples published by Microsoft, they all use code which is running under the messaging endpoint. This initially led me to believe that even for proactive messaging, the code should live under the same endpoint. But as we will see in this post, that is not the case.

What are the prerequisites?

As mentioned before, our bot will need to be installed in a Team first. This will allow the bot messaging endpoint to receive the required values from Teams and send it to our proactive messaging code. If the bot is not installed in the Team, you will get a "Forbidden: The bot is not part of the conversation roster" error.

Base URL (serviceUrl)

This is the Url to which our proactive messaging code should send all the requests. This Url is sent by Teams in the Bot payload in the turnContext.Activity.serviceUrl property. For all intents and purposes this url will remain constant but after having a discussion with Microsoft, they have recommended that this url might change (very rarely) and our bot should have the logic for updating the stored base url periodically from the payload sent to the bot. More about the Base Url here: https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference?view=azure-bot-service-4.0#base-uri

Internal Team Id

This is the internal team id which is not the same as the Office 365 Group Id. The internal team id is in the following format: 19:bf2b184e7cbb4f9f9ca1b47f755cd943@thread.skype

You can get the internal team id from the Bot payload in the channelData.team.id property. You can also get this id through the Microsoft Graph API: https://docs.microsoft.com/en-us/graph/api/resources/team?view=graph-rest-1.0#properties

Channel Id

If we want our bot to post to a specific channel in a Team, then we will need the channel id as well. The format for the channel id is exactly the same as the internal team id. Also, you can get the channel id from the bot payload as well as the Microsoft Graph api: https://docs.microsoft.com/en-us/graph/api/resources/channel?view=graph-rest-1.0#properties

Internal Teams User Id

This would only be needed if you want to send a proactive personal message to a specific user. For all users in a team, Teams maintains an encoded user id so that only bots installed in a team are able to message users. To get this user id, our bot needs to call the conversations/{conversationId}/members REST API endpoint. Fortunately for us the Bot Framework wraps this call in a handy SDK method as shown in the third code sample below.

So once we have all the required values from the Bot messaging endpoint, we are able to send proactive messages. For this sample code, I am using the Microsoft.Bot.Builder v4.7.2
https://www.nuget.org/packages/Microsoft.Bot.Builder/

1) Post a proactive message in a Teams channel



(click to zoom)

2) Post a proactive message in a Teams channel and mention a user in it



(click to zoom)


3) Post a proactive personal message to a user


This code has been updated for Bot Framework v4.12.2 NuGet Gallery | Microsoft.Bot.Builder 4.12.2

Also, before running this code, make sure that the user has installed the bot app in the personal scope or is a member of a Team which has the bot installed.

(click to zoom)

Hope you found the post useful!

16 comments:

Unknown said...

Hi Vardhaman, Your blog is nice and very helpful. One main question I have is that

How/where can I get the teams internal id for sending proactive messages to a user on 1 on 1 chat?

Vardhaman Deshpande said...

Hello there,

Teams internal id can be fetched from the Bot Payload or from the Graph API. Have a look the the "Internal Team Id" section in the start of the blog post.

Thanks,
Vardhaman

Z. Saad said...

Brilliant!

Piecing this together with MS docs was an absolute mess. Half way there, found this post and completed the task right away.

Kudos!

Unknown said...

var user = teamMembers
.Select(channelAccount => JObject.FromObject(channelAccount).ToObject())
.First(user => user.UserPrincipalName == mentionUserPrincipalName); [TeamsChannelAccount from namespace Microsoft.Bot.Schema]

var personalMessageActivity = MessageFactory.Text($"Personal message from the Bot!");

var conversationParameters = new ConversationParameters()[These are from namespace Microsoft.Bot.Schema]
{
ChannelData = new TeamsChannelData
{
Tenant = new TenantInfo
{
Id = tenantId,
}
},
Members = new List() { user }
};

var response = await connectorClient.Conversations.CreateConversationAsync(conversationParameters);[CreateConversationAsync method looks for paramters of type Microsoft.Bot.Connector.

Question:-How to Resolve the conflict between Microsoft.Bot.Connector and using Microsoft.Bot.Schema.Teams.

Vardhaman Deshpande said...

Hi Unknown, just checked with Bot Framework 4.9 and CreateConversationAsync does expect conversationParameters from Microsoft.Bot.Schema

Unknown said...

That is true but in the sentence
var connectorClient = new ConnectorClient(new Uri(serviceUrl), new MicrosoftAppCredentials(botClientID, botClientSecret)); we are connecting to Microsoft.bot.connector and in line await connectorClient.Conversations.SendToConversationAsync(response.Id, personalMessageActivity); we are using the same connectorClient to call the SendToConversationAsync method . Hence it is expecting paramters of type Microsoft.Bot.Connector in CreateConversationAsync method

Unknown said...

Hi Vardhaman, Thank you for putting up this nice blog.
However I wanted to achieve same functionality using REST API calls, but the documentation doesn't cover that alot.

My requirement is to start a proactive conversation in a teams channel using REST API.
As in your first example, you have created a ConversationParameters without members.
var conversationParameters = new ConversationParameters without passing members object.
{
IsGroup = true,
ChannelData = new TeamsChannelData
{
Channel = new ChannelInfo(teamsChannelId),
},
Activity = topLevelMessageActivity
};
But Same is not happening with REST API.
{
"channelData": {
"teamsChannelId": "19:5ev2",
"teamsTeamId": "19:55bd39496a@thv2",
"channel": {
"id": "19:5e89d.t2"
},
"team": {
"id": "19:3288e@tv2"
},
"tenant": {
"id": "bd7c20"
}
}
}
Above payload results in {"error":{"code":"BadSyntax","message":"Incorrect conversation creation parameters"}} error

Vardhaman Deshpande said...

Hi Unknown,

Maybe you could create the request with the SDK and use fiddler to check what REST API is made behind the scenes?

Unknown said...

Thanks Vardhaman. Fiddler worked for me. I was able to get the payload

Greg said...

Hi Vardhaman,

First off, thanks for these chunks of code, they have really helped me produce an alerting service for server outages at work!

I do have one question, i'm trying to send an adaptive card as a private message, and i've been trying to build this as part of the proactive personal messaging code, but I'm either getting just a plain text message or when i try and send across the adaptive card json it's just rendering it as plain old text.

I've tried adding this as an Activity and sending this over via the connector (which does nothing, doesn't even return an error), I've tried sending the json through via the personalMessagingActivity variable you use for sending over plain old strings.

Bit of a long shot, but do you happen to have any examples of where you have done something like this yourself?

Vardhaman Deshpande said...

Hi Greg,

Have you seen this post already? https://www.vrdmn.com/2020/07/microsoft-teams-bot-framework-mention.html

In that, I am posting an adaptive card as part of a message in teams. You should be able to combine the code from that post and the personal message sending code from this post to achieve what you want.

Hope that helps
Vardhaman

Unknown said...

Hi Vardhaman, Abhinav here.
I have installed bot in one channel of my teams. And i received the payload at the endpoint as well.
"channelData":{
"teamsChannelId":"19:690d319a53a0416885400dd80795fdac@thread.tacv2",
"teamsTeamId":"19:690d319a53a0416885400dd80795fdac@thread.tacv2",
"channel":{
"id":"19:690d319a53a0416885400dd80795fdac@thread.tacv2"
},
"team":{
"id":"19:690d319a53a0416885400dd80795fdac@thread.tacv2"
},
"tenant":{
"id":"1b212e38-787d-48cb-83bb-5e4302f225e4"
}
},

This is the channel data. What is the rest API that i can use in post man to send messages to that channel ?

Aravind B.S said...

Hi Vardhaman,

I tried same sample code above with valid parameters such as botid, bot secret, service url, tenantid etc. But,I am getting exception "Not Found" error while calling await ((Conversations)connectorClient.Conversations).GetConversationMemberAsync . Could you please help me on the same?

Monika said...

Hello Vardhaman,

I have tried the above code, but i am getting error "Failed to acquire token for client credentials" at this line
var teamMembers = await connectorClient.Conversations.GetConversationMembersAsync(teamInternalId, default);

could you pls help.
Thanks in advance!

Vardhaman Deshpande said...

Hello all, I have made some changes to the last code sample and updated it for the latest version of Bot Framework. Basically replaced GetConversationMembersAsync (which fetched all users) with GetConversationMemberAsync (which gets user by id). Please check it out and see if it works for you. Thanks!

Julian said...

Thanks a bunch, this saved me a lot of time. So few examples out there about sending messages directly to a single user.