The key thing I want to do with my bot is to push messages out to users when something happens in D365, not just in response to the user sending a message to the bot. This was the part I was unsure of, but a bit of searching gives me the key phrase Proactive Messages. Apparently this is what I need to implement.

Required Information

From a bit of research it seems I need a few IDs to be able to send a proactive message:

  • my bot
  • the user to send the message to
  • the Azure Active Directory tenant the user is in

I already know the ID of my bot, but I need to capture the user and tenant IDs. The user ID is not the user’s login name (user@contoso.com) but an apparently random ID assigned by Teams (29:cXh2ZnlWREI2VgpMY3o3UFdGZU9xCnJMUzBScUJqNWIKSlVrZkpaanh0cgpOMEtXVThVSDl)

I can grab this user ID and the tenant ID from the events in my bot. I finally need to write some code!

Editing the bot code

In part 1 I created the bot using the Echo Bot sample, which deployed a fully-built bot for me. Now I need to start customising this bot, so I need to grab that code.

Back in the Azure Portal I just have to open the Web App Bot resource I created earlier, click on the Build item and then on “Download Bot source code”. That gives me a zip file – just unzip it and open the solution inside it in Visual Studio.

The core code that handles the incoming messages in this bot is:

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    var replyText = $"Echo: {turnContext.Activity.Text}";
    await turnContext.SendActivityAsync(MessageFactory.Text(replyText, replyText), cancellationToken);
}

To get started I just want to quickly prove I can get the required information. I’m going to replace the current “Echo” response with all these IDs:

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    var teamConversationData = turnContext.Activity.GetChannelData<TeamsChannelData>();

    await turnContext.SendActivityAsync(MessageFactory.Text("Your ID is " + turnContext.Activity.From.Id), cancellationToken);

    await turnContext.SendActivityAsync(MessageFactory.Text("My ID is " + turnContext.Activity.Recipient.Id), cancellationToken);

    await turnContext.SendActivityAsync(MessageFactory.Text("Tenant ID is " + teamConversationData.Tenant.Id));
}

The ID of the user and the bot is available in the standard turnContext parameter, but the tenant ID is specific to the Teams channel so we need to get that out of the TeamsChannelData class. This is a good start:

My bot ID appears to be the same ID I used when I created the Teams app package, but with the prefix 28:. The Azure Active Directory tenant ID is the same ID I’m used to seeing in various situations now, but the user ID appears completely random.

Sending a Proactive Message

Now I’ve got the required details I want to set up a simple console app. This should demonstrate I can send a message before I jump into building anything into D365. Following the docs again and using some hard-coded IDs for now just to get started I built:

async static Task Main(string[] args)
{
    const string url = "https://mybotapp.azurewebsites.net/";
    const string appId = "<guid>";
    const string appPassword = "<password>";

    MicrosoftAppCredentials.TrustServiceUrl(url);

    var client = new ConnectorClient(new Uri(url), appId, appPassword);

    // Create or get existing chat conversation with user
    var parameters = new ConversationParameters
    {
        Bot = new ChannelAccount("28:<guid>"),
        Members = new[] { new ChannelAccount("29:<user id copied from earlier chat>") },
        ChannelData = new TeamsChannelData
        {
            Tenant = new TenantInfo("<tenant id copied from earlier chat>"),
        },
    };

    var response = await client.Conversations.CreateConversationAsync(parameters);

    // Construct the message to post to conversation
    var newActivity = new Activity
    {
        Text = "Hello",
        Type = ActivityTypes.Message,
        Conversation = new ConversationAccount
        {
            Id = response.Id
        },
    };

    // Post the message to chat conversation with user
    await client.Conversations.SendToConversationAsync(response.Id, newActivity);
}

Aaaannnddd… I get the error:

Operation returned an invalid status code 'NotFound'

After some frustrated searching I found that the URL I needed to connect to was not that of my bot itself, but a channel-specific URL provided by the Azure Bot Framework. I found some suggested URLs to use for different channels, but the way I went for was to include this URL in my bot response by adding in this line:

await turnContext.SendActivityAsync(MessageFactory.Text("Service URL is " + turnContext.Activity.ServiceUrl));

This gave me the URL https://smba.trafficmanager.net/emea/. I assume that will change depending on the region you’re in, so it does seem best to get this value dynamically in the same way as the other IDs rather than hard-coding it.

With that hurdle crossed I can now run my console app and get an alert in Teams!

Everything seems to work just how I’d hope at this point – I get the popup alert message with the contents of my message, the Teams app in the task bar is flashing to show I’ve got a message, and it appears in the timeline of my conversation when I open Teams. Great!

This seems to be all I need to get messages sent out to me, but currently I’m skipping a load of complexity by hard-coding my own user IDs. I need to start making this more dynamic so I can send messages to others too, which I’ll look at next time.

3 thoughts on “Creating a bot pt. 2 – Proactive Messages”

  1. Hello, Mr. Mark Carrington.

    Thank you for your great sample!
    But I can’t work your sample well.
    The sample suddenly stops with no errors at “CreateConversationAsync”.
    What do you think of it.

    Regards,
    Takeshi Haramoto

    1. There must be some error being generated here. Is it possible your debugger is not breaking on all exceptions? In Visual Studio try enabling all CLR exceptions to make sure.

      I’d also double check the various IDs and URLs are correct. Have you replaced the URL with your local version of the service URL (https://smba.trafficmanager.net/emea/ in my case) and all the correct IDs for the bot, destination user and tenant in the ConversationParameters object?

      Failing that, you could use Fiddler to inspect the HTTP requests being made and the responses being generated to see if there’s any more information available there.

  2. Hey can i know if we can send a message from the http request and then trigger the bot and get the value in the body of the request?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.