Learning to build a Microsoft Teams Bot — Tying it all together and making it proactive
About this Series
I am (as of the date of writing) the Head of Development for several development teams mostly based in three offices — London, Malta and Barcelona. They have varied and wildly different public holiday schedules, and we don’t officially track them. We regularly have a team(s) not in the office/not available when everyone else is. This is where this bot comes in.
I’m going to massively over-engineer a solution to inform the teams of public holidays occurring in any of the other locations.
My goal for this series is to build a Teams Bot to inform the end-user when the public holidays are in their region on an on-demand or a scheduled basis. This is a POC/MVP with a longer-term goal of having the bot do more useful things later, but as mentioned previously is a bit of overkill for now.
Disclaimer: I have built neither a Bot nor a Teams App before so this is very much a “follow along while I learn” article.
3 Part Series:
- Part 1: Learning to build a Microsoft Teams Bot — Getting the Echo Bot up and running
- Part 2: Learning to build a Microsoft Teams Bot — Getting public holiday data into the bot
- Part 3: This article
This article aims to get the bot to make proactive announcements to a Teams Channel on a schedule. I'll also be publishing everything and wrapping it up.
Recap
To date, this is what I have created.
When asked for anything relating to a ‘holiday’, a bot which can query an API, cleanse the data and return a formatted message containing the relevant national holidays for the next eight days.
Logging
Before continuing with the rest of the project, I decided to add logging. Logging is always crucial, but it is particularly so whenever an application or service runs in the Cloud. To understand what success and failure look like, good or bad performance, or even understanding what “normal” looks like, logging is crucial.
Both the Bot Framework and any Web API have excellent out of the box logging and metrics from Application Insights. And, since every interaction with a Bot is triggered through a Web API, our Bot is technically just a Web App. As such, I decided to add Application Insights to see how far I could get with that.
Turns out, quite far.
Adding in application insights was as simple as filling in the familiar field in the appsettings.json file.
"ApplicationInsights": {
"InstrumentationKey": "xxYourKeyHerexx"
}
And, a little bit of code in the ConfigureServices class.
services.AddApplicationInsightsTelemetry();services.AddSingleton<IBotTelemetryClient, BotTelemetryClient>();services.AddSingleton
<ITelemetryInitializer, OperationCorrelationTelemetryInitializer>();services.AddSingleton<ITelemetryInitializer, TelemetryBotIdInitializer>();services.AddSingleton<TelemetryInitializerMiddleware>();initializer) to track conversation events
services.AddSingleton<TelemetryLoggerMiddleware>(sp =>
{
var telemetryClient = sp.GetService<IBotTelemetryClient>();
return new TelemetryLoggerMiddleware(
telemetryClient,
logPersonalInformation: true);
});
Remember that it can take 2–3 minutes in some cases for data to be available in Application Insights, which can make troubleshooting tricky and/or frustrating at first.
In addition to the usual App Insights style information, there is a custom section specifically for Bots which would be of great interest to anyone building a commercial or more heavily trafficked bot.
I was happy enough to see the information I was hoping to see, up to and including the back and forth between the user and the bot.
Proactive Bot
Looking back at this project, I think this portion of it was the toughest to wrap my head around.
Taking a step back and looking at a Bot and how a user interacts with it, the entire flow is designed to be user-initiated. There is nothing built into the framework which turns that on its head and has the bot itself initiating the interaction with the user.
I spent quite a lot of time looking into this as I was attempting to solve my problem in a specific way and now how to fit that into how the framework is designed to be used.
The Microsoft documentation on this use case of a Proactive Bot notably lacks in examples or clarity. It is the only time in this project I have found that to be the case. In particular, the documentation and samples are designed to reference a previous Conversation object which contains the references required to send a message to that Channel/User. You don't have that if you are trying to broadcast a message to a channel.
After spending a long time reading other blog posts (which I have linked below, and found them very helpful), I finally managed to wrap my head around what I needed to do.
To have the Bot perform activities that look proactive, I needed to expose another endpoint that would allow another application/function to contain the logic required and reach out to the Bot to initiate the logic. The simplest way to test this idea seemed to be to add a new Controller that would allow the logic to be invoked in the Bot and use Postman to invoke it.
However, I discovered I didn't have a lot of information along the way, and I didn't know how to get it.
This is the code I needed to get running, which I cobbled together from various sources, which I have linked below. All the values which I have bolded, I didn't have.
MicrosoftAppCredentials.TrustServiceUrl(_serviceUrl);
var connectorClient = new ConnectorClient
(new Uri(_serviceUrl),
new MicrosoftAppCredentials(_appId, _appPassword));var holidayText = await ConstructNationalHolidayMessage();if (!string.IsNullOrEmpty(holidayText))
{
var topLevelMessageActivity = MessageFactory.Text(holidayText);
var conversationParameters = new ConversationParameters
{
IsGroup = true,
ChannelData = new TeamsChannelData
{
Channel = new ChannelInfo(_generalChannelId),
},
Activity = topLevelMessageActivity
};await connectorClient
.Conversations
.CreateConversationAsync(conversationParameters);
}
The service URL is embedded in the information available in the ITurnContext<IMessageActivity> object available when debugging, which can then be printed to screen. However, it seems to be the same for everyone: https://smba.trafficmanager.net/emea/ so I would imagine it is pretty safe to use this as is.
The next value needed is the Channel Id so that the Bot can post to the channel. Again, you can get this value by debugging a conversation between the bot and yourself from the Teams Channel in question. This does present a bit of chicken and egg scenario in which you need to have the bot available in a channel to get the channel's ID so you can post to the channel. Botception?
Or, you can click the three dots to the right of a channel and click the option “Get link to channel.” This will then provide you with a link that looks something like this
https://teams.microsoft.com/l/channel/19%3a0000000000000000000000000%40thread.tacv2/General?groupId=00000000–0000–0000–0000–000000000000&tenantId=00000000–0000–0000–0000–000000000000
The bit between the 19 and the .tacv2 is your Channel ID. Removing the URL encoded characters shows it is of the form:
19:00000000000000000000000000000000@thread.tacv2
This ID could be passed into the API endpoint and thus act more dynamically, but given this bot only needs to send a single message to a channel, I added it to the appsettings.json file along with the Service URL.
It is worth noting the code block above is what I ended up with after spending a large number of hours trying (and failing, miserably) to get it to do anything useful. It was only after I uploaded the new code to Teams that it did anything useful at all. The emulator does not work for this kind of interaction where you are explicitly targeting a channel and not a user. It’s not you, its the emulator!
Azure Function
The final part of the puzzle is to create the Function to trigger the bot.
The actual code is straightforward, but I had to use both the Microsoft documentation and the Cron Calculator to nail down the cron schedule.
The expression to run every Monday at 09:30 is: 0 30 9 * * Mon
The expression I used for testing, to run every 30 seconds: 10,40 * * * * *
[FunctionName("NationalHolidayTriggerFunction")]
public static async Task Run(
[TimerTrigger("10,40 * * * * *")]TimerInfo myTimer,
ILogger log)
{
try
{
var client = new HttpClient();
client.BaseAddress = new
Uri("https://bd21ba1ec549.ngrok.io/");
var httpResponseMessage =
await client.GetAsync("api/notify"); httpResponseMessage.EnsureSuccessStatusCode(); log.LogInformation(
$"Successfully called the notify API at: {DateTime.Now}");
}
catch (Exception e)
{
log.LogError(
$"Error attempting to call the Notify API. {e.Message}",
e);
throw;
} log.LogInformation(
$"C# Timer trigger function executed at: {DateTime.Now}");
}
Which then looks like this when run.
And in Teams
All that remains is to publish to a production Resource Group in Azure and add it to the intended Team.
Publishing
There are a fair few things which need to be tweaked, published and settings moved around.
Firstly the Bot itself needs to be published. As I've mentioned previously, it is a Web App, so can be deployed into an App Service Plan. As the Bot is still in a Dev/Test phase, I'm happy with a Free plan for now. I'm also happy to do this from Visual Studio.
Once the release is complete, you should be presented with the standard Bot landing page in your browser, but hosted in Azure.
The next step is to take this new URL, *.azurewebsites.net, and transfer it to the Messaging Endpoint Setting in the Bot Channel Registration.
Again, the local Bot Emulator doesn't seem to be able to deal with the AppId and BotPassword but, by clicking on “Test in Web Chat” in the Bot Channel, it is possible to interact with the Bot in the Azure Portal.
I was then able to point my locally running Function at the new endpoint and test the proactive notifications. I do need to secure this endpoint, but it’s outside the scope of this article.
Finally, I deployed my Azure Function and initially had it run every 30 seconds in my test Team Channel, just to confirm that it was working as expected.
This is the deployed architecture.
Having finished the project and let it run for a period, I was pleasantly surprised to see how much information Application Insights had been able to gather from the interactions with the Bot.
Given that I did nothing custom, other than the code shown in this article already, I'm quite impressed with the level of tracking being shown here, particularly the calls out to Calendarific.
Conclusion
I hope you have enjoyed learning along with me as much as I have enjoyed this learning.
I would class building a Teams Bot as a non-trivial. It's not hard per se; there is just a lot of it. It's unlikely to be something achievable in a single afternoon or day, especially if it’s the first one. Due to the layers of interconnected and interdependent technologies, there is a lot to get one’s head around and a lot of learning to do.
What I have produced is certainly over-engineered for the value it provides. However, as mentioned in the first article, this is something of an MVP, and I have plans for the next few months to extend this into something more useful for myself and my teams.
References
All the information needed to add the telemetry/logging to the bot came from this link.
This link is the official documentation but entirely assumes you will have a Conversation from a user to continue. This is entirely the wrong track for posting a message to a Channel.
The next two links were invaluable for understanding the different approach needed to achieve my goal.
Finally, this is Part 2 of an impressive 8 part series going through a bot learning exercise similar to my own. This one was beneficial for identifying various variable values such as the ServiceUrl and ChannelId.