Search code examples
c#botframework

How to send out of turn messages and replies in bot builder framework 4.0


I am (on external events) posting messages to my microsoft team channel.

var activity = new Activity("message", text: $"", attachments: new List<Microsoft.Bot.Schema.Attachment>
    {
        new Microsoft.Bot.Schema.Attachment{ ContentType = "application/vnd.microsoft.card.adaptive" , Content= new JRaw(json)}
    });
var a = await connector.Conversations.SendToConversationAsync("hardcoded channel string, cant find it anywhere",
    activity);

Which works, the adaptive card is shown in my channel.

I now want to follow up with an reply to this activity later.

 var b = await connector.Conversations.ReplyToActivityAsync("same channel id hardcoded", a.Id, new Activity("message", text: "user disconected",replyToId : a.Id));

but the message is not shown as a reply but instead just a new message in the channel.

The id returned from response of SendToConversationAsync, do not look like an activity id, so how do i get the activity id such i can reply to it properly.

The connectorClient is created like:

var connector = new ConnectorClient(new Uri("https://smba.trafficmanager.net/emea/"), appCredentials);

Solution

  • Have a look at this sample to see how to send proactive messages in BotBuilder V4: https://github.com/Microsoft/BotBuilder-Samples/tree/master/samples/csharp_dotnetcore/16.proactive-messages

    Of particular interest should be the CompleteJobAsync method:

    // Sends a proactive message to the user.
    private async Task CompleteJobAsync(
        BotAdapter adapter,
        string botId,
        JobLog.JobData jobInfo,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        await adapter.ContinueConversationAsync(botId, jobInfo.Conversation, CreateCallback(jobInfo), cancellationToken);
    }
    

    From this we can see that the trick is to use BotAdapter.ContinueConversationAsync

    I've tested this in Teams and found that it will live up to its name by continuing the conversation rather than starting a new one.


    EDIT: I now understand that you meant to ask something more like this question: Microsoft Bot Connector - get activity id from response after sending message

    That is to say, you understand that in a Microsoft Teams team channel, all activities will share the same "root" conversation ID that looks like this: 19:[email protected]

    And each "thread" will have its own conversation ID that consists of the root ID plus the message ID that started the thread: 19:[email protected];messageid=1545970892795

    So if you want to start a new thread then you can send a message to the root conversation ID and if you want to reply to a thread then you can send a message to the thread's conversation ID. In this regard, there's no difference between SendToConversationAsync and ReplyToActivityAsync because, despite appearances, Teams doesn't "nest" messages under other messages. Instead, each message chain is identified by its own conversation ID.

    So in order to send a message to a thread, you've discovered that you need to know the ID of the message that started the thread. Unfortunately, the ID returned by SendToConversationAsync and ReplyToActivityAsync is in a different form from the one you need. After extensive testing I've discovered that Teams seems to have two forms of message ID:

    • Epoch timestamps (1545970800530)
      • Used as the activity ID in messages sent to the bot
      • Used as part of a conversation ID to define a thread (19:[email protected];messageid=1545970892795)
    • Alphanumeric codes (1:1zDTpbvf1DFFLC5cL0n72d-wPdIIV2L6L5LZ5H_nzqhs)
      • Returned as HTTP content from calls to the REST API
      • Used for updating and presumably for deleting activities

    So are you out of luck? Not at all! It turns out that if you create the thread with CreateConversationAsync then it will return the thread's conversation ID which you can use for your replies. Here is an extension class to make it easy for you:

    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Connector.Teams.Models;
    using Microsoft.Bot.Schema;
    
    namespace Microsoft.Bot.Connector.Teams
    {
        public static class TeamsExtensions
        {
            public static async Task<ConversationResourceResponse> CreateTeamsThreadAsync(
                this ITurnContext turnContext,
                Activity activity,
                CancellationToken cancellationToken = default(CancellationToken))
            {
                var connectorClient = turnContext.TurnState.Get<IConnectorClient>() as ConnectorClient;
                activity.ChannelData = turnContext.Activity.ChannelData;
    
                return await connectorClient.Conversations.CreateTeamsThreadAsync(turnContext.Activity.Conversation.Id, activity, cancellationToken);
            }
    
            public static async Task<ConversationResourceResponse> CreateTeamsThreadAsync(
                this IConversations conversations,
                string conversationId,
                Activity activity,
                CancellationToken cancellationToken = default(CancellationToken))
            {
                var channelData = activity.GetChannelData<TeamsChannelData>();
    
                var parameters = new ConversationParameters
                {
                    ChannelData = new TeamsChannelData
                    {
                        Channel = channelData.Channel,
                        Team = channelData.Team,
                        Tenant = channelData.Tenant,
                    },
                    Activity = activity,
                };
    
                return await conversations.CreateConversationAsync(parameters, cancellationToken);
            }
        }
    }
    

    Since I saw that you already have a ConnectorClient and a conversation ID I made a method that uses those. You can also call the method that extends ITurnContext if you're want to create a thread in response to a message from a user. All you have to do is save the Id value of the ConversationResourceResponse that the method returns so you can use it for your proactive messages.

    Bot replying to itself