Search code examples
c#asp.netbotframeworkchatbotmicrosoft-teams

How can I proactively trigger a dialog from a bot without a turnContext?


I have a Teams bot that can send proactive messages to a user via webAPI. I can get a ConnectorClient from the Microsoft.Bot.Connector namespace, and then from there I can identify the relevant conversation, and call SendToConversationAsync to message a user.

If I want to use this to initiate a dialog though, the challenge seems to be that I don't have a TurnContext to reference. I found a post here that seemed promising, but that still depends on having a dialog turn running. Ideally, I'd like to be able to do this via the ConnectorClient reference that I already have, and trying with a null TurnContext doesn't seem to work. For example, trying this:

var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
var dialogSet = new DialogSet(dialogState);
dialogSet.Add(new MyDialog());
DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
var turnResult = await dc.BeginDialogAsync("MyDialog");

Throws an exception if turnContext is null.

This answer here has some promising ideas too, suggesting faking an incoming event, but I can do something like CreateInvokeActivity(), but sending that to the conversation throws an exception. I'm also not sure how to trigger the pipeline to get the message through in the same process without going as far up as using an HTTPCLient to POST the raw message (which requires getting a token I believe). The bot already has a 1:1 conversation with the user, but I'd like to have this initiate a dialog if possible. Is there a way to have the ConnectorClient begin a dialog proactively, or trigger an invoke to the bot pipeline programmatically to allow it to kick off there?


Solution

  • I managed to figure out a way to do this, but it's probably not an ideal scenario. I wanted to start a dialog from the API, specifically an authentication dialog that gets a user's OAuth token for accessing graph. If the user is signed in, the token is returned immediately, and if not, they get a sign in prompt. I have something like this in my bot code (edited for brevity):

    public static async Task<string> GetTokenAsync(ITurnContext turnContext, CancellationToken cancellationToken)
    {
        var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
        var dialogSet = new DialogSet(dialogState);
        dialogSet.Add(new AuthDialog());
        DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
        var turnResult = await dc.BeginDialogAsync("AuthDialog");
        await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    
        if(turnResult.Status== DialogTurnStatus.Waiting)
        {
            _log.Debug("Got login request for user-waiting for response");
            return string.Empty;
        }
        else if(turnResult.Result is TokenResponse)
        {
            return ((TokenResponse)turnResult.Result).Token;
        }
        return null;
    }
    

    This creates the dialog and, if possible, returns the token. In my webAPI, I have something like this to invoke it proactively:

    string conversationID = "CONV_ID_FROM_STATE";
    
    var members = await m_client.Conversations.GetConversationMembersAsync(conversationID);
    
    BotFrameworkAdapter b = new BotFrameworkAdapter(new SimpleCredentialProvider("BOT ID", "BOT_SECRET"));
    var message = Activity.CreateMessageActivity();
    message.Text = "login";
    message.From = new ChannelAccount(members[0].Id);
    message.Conversation = new ConversationAccount(id: conversationID, conversationType: "personal", tenantId: :BOT_TENANT_ID);
    message.ChannelId = "msteams";
    
    TurnContext t = new TurnContext(b, (Activity)message);
    ClaimsIdentity id = new ClaimsIdentity();
    id.AddClaim(new Claim("aud", "BOT_ID"));
    t.TurnState.Add("BotIdentity", id);
    t.TurnState.Add("Microsoft.Bot.Builder.BotAdapter.OAuthScope", "https://api.botframework.com");
    t.TurnState.Add("Microsoft.Bot.Connector.IConnectorClient", m_client);
    string token = await myBot<AuthDialog>.GetTokenAsync(t, default);
    

    At this point, if the token is an empty string, the user hasn't signed in, but otherwise it should be a valid token to make graph calls with. I've tested this with a few new accounts, and it seems to work, so I'm calling that a win for now. If there's something that's fundamentally busted here though, please comment.