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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.Bot.Builder; | |
using Microsoft.Bot.Connector; | |
using Microsoft.Bot.Connector.Authentication; | |
using Microsoft.Bot.Schema; | |
using Microsoft.Bot.Schema.Teams; | |
using System; | |
using System.Threading.Tasks; | |
namespace Teams.Bot.Conversations | |
{ | |
class Program | |
{ | |
public static async Task Main(string[] args) | |
{ | |
//Teams channel id in which to create the post. | |
string teamsChannelId = "19:ba9feb7470794dbeab58e3f72ef20a6e@thread.skype"; | |
//The Bot Service Url needs to be dynamically fetched (and stored) from the Team. Recommendation is to capture the serviceUrl from the bot Payload and later re-use it to send proactive messages. | |
string serviceUrl = "https://smba.trafficmanager.net/emea/"; | |
//From the Bot Channel Registration | |
string botClientID = "<client-id>"; | |
string botClientSecret = "<client-secret>"; | |
MicrosoftAppCredentials.TrustServiceUrl(serviceUrl); | |
var connectorClient = new ConnectorClient(new Uri(serviceUrl), new MicrosoftAppCredentials(botClientID, botClientSecret)); | |
var topLevelMessageActivity = MessageFactory.Text($"I am alive!"); | |
var conversationParameters = new ConversationParameters | |
{ | |
IsGroup = true, | |
ChannelData = new TeamsChannelData | |
{ | |
Channel = new ChannelInfo(teamsChannelId), | |
}, | |
Activity = topLevelMessageActivity | |
}; | |
await connectorClient.Conversations.CreateConversationAsync(conversationParameters); | |
} | |
} | |
} |
(click to zoom)
2) Post a proactive message in a Teams channel and mention a user in it
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.Bot.Builder; | |
using Microsoft.Bot.Connector; | |
using Microsoft.Bot.Connector.Authentication; | |
using Microsoft.Bot.Schema; | |
using Microsoft.Bot.Schema.Teams; | |
using Newtonsoft.Json.Linq; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Threading.Tasks; | |
namespace Teams.Bot.Conversations | |
{ | |
class Program | |
{ | |
public static async Task Main(string[] args) | |
{ | |
//Teams internal id | |
string teamInternalId = "19:96391bb270744e218c04dc8f571d3d8b@thread.skype"; | |
//Teams channel id in which to create the post. | |
string teamsChannelId = "19:86f8774fd88a4d4ba1bae4715dcca821@thread.skype"; | |
//The Bot Service Url needs to be dynamically stored and fetched from the Team. Recommendation is to store the serviceUrl from the bot Payload and later re-use it to send proactive messages. | |
string serviceUrl = "https://smba.trafficmanager.net/emea/"; | |
//the upn of the user who should be mentioned. | |
string mentionUserPrincipalName = "user@tenant.onmicrosoft.com"; | |
//From the Bot Channel Registration | |
string botClientID = "<client-id>"; | |
string botClientSecret = "<client-secret>"; | |
MicrosoftAppCredentials.TrustServiceUrl(serviceUrl); | |
var connectorClient = new ConnectorClient(new Uri(serviceUrl), new MicrosoftAppCredentials(botClientID, botClientSecret)); | |
var teamMembers = await connectorClient.Conversations.GetConversationMembersAsync(teamInternalId, default); | |
var userToMention = teamMembers | |
.Select(channelAccount => JObject.FromObject(channelAccount).ToObject<TeamsChannelAccount>()) | |
.First(user => user.UserPrincipalName == mentionUserPrincipalName); | |
var mention = new Mention | |
{ | |
Mentioned = userToMention, | |
Text = $"<at>{userToMention.Name}</at>", | |
}; | |
var topLevelMessageActivityWithMention = MessageFactory.Text($"Hi {mention.Text}"); | |
topLevelMessageActivityWithMention.Entities = new List<Entity> { mention }; | |
var conversationParameters = new ConversationParameters | |
{ | |
IsGroup = true, | |
ChannelData = new TeamsChannelData | |
{ | |
Channel = new ChannelInfo(teamsChannelId), | |
}, | |
Activity = topLevelMessageActivityWithMention | |
}; | |
await connectorClient.Conversations.CreateConversationAsync(conversationParameters); | |
} | |
} | |
} |
(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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.Bot.Builder; | |
using Microsoft.Bot.Connector; | |
using Microsoft.Bot.Connector.Authentication; | |
using Microsoft.Bot.Schema; | |
using Microsoft.Bot.Schema.Teams; | |
using Newtonsoft.Json.Linq; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Threading.Tasks; | |
namespace Teams.Bot.Conversations | |
{ | |
class Program | |
{ | |
public static async Task Main(string[] args) | |
{ | |
//Teams internal id | |
string teamInternalId = "19:96391bb270744e218c04dc8f571d3d8b@thread.skype"; | |
//The Bot Service Url needs to be dynamically stored and fetched from the Team. Recommendation is to store the serviceUrl from the bot Payload and later re-use it to send proactive messages. | |
string serviceUrl = "https://smba.trafficmanager.net/emea/"; | |
//the upn of the user who should recieve the personal message | |
string mentionUserPrincipalName = "user@tenant.onmicrosoft.com"; | |
//Office 365/Azure AD tenant id for the | |
string tenantId = "<tenant-GUID>"; | |
//From the Bot Channel Registration | |
string botClientID = "<client-id>"; | |
string botClientSecret = "<client-secret>"; | |
var connectorClient = new ConnectorClient(new Uri(serviceUrl), new MicrosoftAppCredentials(botClientID, botClientSecret)); | |
var user = await ((Microsoft.Bot.Connector.Conversations)connectorClient.Conversations).GetConversationMemberAsync(mentionUserPrincipalName, teamInternalId, default); | |
var personalMessageActivity = MessageFactory.Text($"Personal message from the Bot!"); | |
var conversationParameters = new ConversationParameters() | |
{ | |
ChannelData = new TeamsChannelData | |
{ | |
Tenant = new TenantInfo | |
{ | |
Id = tenantId, | |
} | |
}, | |
Members = new List<ChannelAccount>() { user } | |
}; | |
var response = await connectorClient.Conversations.CreateConversationAsync(conversationParameters); | |
await connectorClient.Conversations.SendToConversationAsync(response.Id, personalMessageActivity); | |
} | |
} | |
} |
(click to zoom)
Hope you found the post useful!