Proactive Messages in Microsoft Teams

May 22, 2021

Proactive messaging provides your bot the ability to notify the user with messages that can be written and modified by the developer. The ability to use them with any channel can bring confusion due to how each channel handles proactive messages and dialogs.


Preface

As you propably already know, not every piece of code will behave similarly in any supported channel, proactive messages being one of them. There are many implementations out there, but while it might work in the emulator and in the Web Chat, many major channels like Microsoft Teams might need some changes to get them to work correctly. This post aims to give you an implementation that will work in many major channels, including Microsoft Teams and will still allow you to have the full functionality of your bot in the web chat and emulator.

You can find the official proactive messages sample here and although it will work out of the box with Teams, it might not be suitable for every use case. For example you might need to create a dialog bot that has a slightly different structure and that’s what we are going to do in this post. Here is an older post showing proactive messages working in a dialog bot, and while this works fine with the Web Chat, Teams does not support it.

Microsoft Teams and many other channels never call the OnConversationUpdateActivityAsync() function, which is the function used to capture the converstation reference. In order to capture it we need to find a function that is called often enough (at every message) to collect the conversation reference, if we need to use our bot in these channels. That function is OnMessageActivityAsync().


Create

In this post we will be using a Basic Bot created by Azure Bot Service. You can find out how to create one in this post.
This is the controller that will handle the proactive messages. Create a new class named NotifyController.cs and paste in it the following code.

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Configuration;

namespace ProactiveBot.Controllers
{
    [Route("api/notify")]
    [ApiController]
    public class NotifyController : ControllerBase
    {
        private readonly IBotFrameworkHttpAdapter _adapter;
        private readonly string _appId;
        private readonly ConcurrentDictionary<string, ConversationReference> _conversationReferences;

        public NotifyController(IBotFrameworkHttpAdapter adapter, IConfiguration configuration, ConcurrentDictionary<string, ConversationReference> conversationReferences)
        {
            _adapter = adapter;
            _conversationReferences = conversationReferences;
            _appId = configuration["MicrosoftAppId"] ?? string.Empty;

            if (string.IsNullOrEmpty(_appId))
            {
                _appId = Guid.NewGuid().ToString(); //if no AppId, use a random Guid
            }
        }

        public async Task<IActionResult> Get()
        {
            foreach (var conversationReference in _conversationReferences.Values)
            {
                await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, conversationReference, BotCallback, default(CancellationToken));
            }

            // Let the caller know proactive messages have been sent
            return new ContentResult()
            {
                Content = "<html><body><h1>Proactive messages have been sent.</h1></body></html>",
                ContentType = "text/html",
                StatusCode = (int)HttpStatusCode.OK,
            };
        }

        private async Task BotCallback(ITurnContext turnContext, CancellationToken cancellationToken)
        {
            await turnContext.SendActivityAsync("This is a proactive message");
        }
    }
}


Implement

Find the file named DialogBot.cs and insert the following using statement.

      using System.Collections.Concurrent;

Add the ConcurrentDictionary inside the class.

      protected readonly ConcurrentDictionary<string, ConversationReference> _conversationReferences;

Replace the class’s constructor with this one which passes the conversationReferences in lines 1 and 7.

public DialogBot(ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger, ConcurrentDictionary<string, ConversationReference> conversationReferences)
{
    ConversationState = conversationState;
    UserState = userState;
    Dialog = dialog;
    Logger = logger;
    _conversationReferences = conversationReferences;
}

Add the AddConversationReference function later in the file to collect the conversation reference.

private void AddConversationReference(Activity activity)
{
    var conversationReference = activity.GetConversationReference();
    _conversationReferences.AddOrUpdate(conversationReference.User.Id, conversationReference, (key, newValue) => conversationReference);
}

In the OnMessageActivityAsync() function, add the following line.

      AddConversationReference(turnContext.Activity as Activity);

Next, open the DialogAndWelcomeBot.cs class and insert this using statement.

      using System.Collections.Concurrent;

Change the constructor to the following one to account for the conversation reference in lines 1 and 2.

public DialogAndWelcomeBot(ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger, ConcurrentDictionary<string, ConversationReference> conversationReferences)
    : base(conversationState, userState, dialog, logger, conversationReferences)
{
}

Lastly, open the Startup.cs file and add these two using statements.

using Microsoft.Bot.Schema;
using System.Collections.Concurrent;

Include the ConcurrentDictionary service by pasting the following line inside the class.

      services.AddSingleton<ConcurrentDictionary<string, ConversationReference>>();

Now functionality may vary depending on the NuGet packages you use, and more specifically on the Microsoft.Bot.Builder.Integration.AspNet.Core package. I am using the 4.8.2 version. I you are having difficulties with getting your proactive messages to appear consider changing the version to the one we are using in this post.


You are now ready to go. Publish your bot in Azure and test it using Microsoft Teams.


Test

Once you have published your bot start talking to it using Teams to capture the new conversation reference. After that call the notify endpoint to get your proactive messages. Your endpoint will look like this: BOT_NAME.azurewebsites.net/api/notify with BOT_NAME being the name of your web app bot.


Here is the bot working in Microsoft Teams!


This way you can have proactive messages appear in your Teams chat! This post may also work in other channels that the previous implementation was not supported.

About Me

Hi, my name is Demetris Bakas and I am a software engineer that loves to write code and be creative. I always find new technologies intriguing and I like to work with other people and be a part of a team. My goal is to develop software that people will find useful and will aid them in their everyday lives.
For any questions feel free to contact me at social media using the links below.