在 Microsoft Teams 机器人中调用 activity 后如何继续 OAuth 对话框?

How can I continue an OAuth dialog after an invoke activity in a Microsoft Teams bot?

我有一个以 Microsoft Teams 为目标的机器人,主要使用 1:1 主动聊天消息,因此它并没有真正管理对话方式。不过,我正在尝试重构一些使用应用程序权限给用户委托的代码,所以我正在尝试实现 here.

中描述的 OAuth 流程

我已经从引用的示例中提取了几乎 1:1 的身份验证对话框(基本瀑布对话框和派生自 ComponentDialog 的 OAuthPrompt),我可以通过登录获得 OAuth 提示按钮,但我无法完成身份验证。这是一个代码片段:

    public class myBot : TeamsActivityHandler
    {
...
        public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
        {
...
            if (turnContext.Activity.Type == ActivityTypes.Message && turnContext.Activity.Text == "login")
            {
                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);
            }
            else if (turnContext.Activity.Type == ActivityTypes.Invoke && turnContext.Activity.Name == "signin/verifyState")
            {
                var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
                var dialogSet = new DialogSet(dialogState);
                DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
                var turnResult=await dc.ContinueDialogAsync();
                await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
            }


如果用户发送“登录”,我会按预期收到 OAuth 提示。单击登录按钮会生成弹出窗口,完成登录后会将调用 activity 发送回机器人。问题出在处理 signin/verifyState 的块中。我可以获得 DialogContext,并尝试 运行 ContinueDialog 将控制权传回 OAuthPrompt,但我收到一个异常提示“无法继续对话。找不到 ID 为 AuthDialog 的对话框”。问题是,如果我检查对话框上下文,我可以看到 dc.ActiveDialog.Id="AuthDialog",并且对话框在堆栈上。此时我还需要做些什么来将控制权交还给对话框吗?

如果重要的话,这个机器人也在使用任务模块,所以我需要能够看到我从这些模块获得的调用响应,这意味着我基本上是从 OnTurnAsync 调度所有内容。

所以我在根本不使用对话框的情况下获得了授权,但是:

一个。我不知道这是否是个好主意,因为所有示例都使用对话框,所以也许有一个很好的理由 b.这不是我在“生产”中拥有的东西,所以它无论如何都没有经过“实战测试”。

也就是说,这基本上就是我正在做的事情,它会进入您的“signin/verifyState”代码分支:

dynamic value = turnContext.Activity.Value;
string magicCode = value.state;
var adapter = (turnContext.Adapter as BotFrameworkAdapter);

    try
    {
        var connectionName = "[the name of your connection registered in the Azure Portal]";
        var token = await adapter.GetUserTokenAsync(turnContext, connectionName, magicCode, cancellationToken).ConfigureAwait(false);
    
        if (token != null)
        {
            // do something, like send the user a "thank you" message, or execute the desired command
        }
        else
        {
            await turnContext.SendActivityAsync(new Activity { Type = ActivityTypesEx.InvokeResponse, Value = new InvokeResponse { Status = 404 } }, cancellationToken).ConfigureAwait(false);
         }
} catch { // todo: this doesn't seem to do anything...
    await turnContext.SendActivityAsync(new Activity { Type = ActivityTypesEx.InvokeResponse, Value = new InvokeResponse { Status = 500 } }, cancellationToken).ConfigureAwait(false);
}

我想我已经弄清楚了,它更多的是关于调整一个不使用对话框的现有机器人来添加一个。首先,我从示例中添加了稍微修改过的身份验证对话框版本:

    public class AuthDialog : ComponentDialog
    {
        public AuthDialog()
            : base(nameof(AuthDialog))
        {
            AddDialog(new OAuthPrompt(
                nameof(OAuthPrompt),
                new OAuthPromptSettings
                {
                    ConnectionName = "botTeamsAuth",
                    Text = "Please Sign In",
                    Title = "Sign In",
                    Timeout = 300000, // User has 5 minutes to login (1000 * 60 * 5)
                }));

            AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));

            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
            {
                PromptStepAsync,
                LoginStepAsync
            }));

            // The initial child Dialog to run.
            InitialDialogId = nameof(WaterfallDialog);
        }

        private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);
        }

        private async Task<DialogTurnResult> LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            var tokenResponse = (TokenResponse)stepContext.Result;
            if (tokenResponse?.Token != null)
            {
                // do something with graph here
                GraphServiceClient client = iceTeamsBotState.GetGraphClient(tokenResponse.Token);
                var messages = await client.Me.MailFolders["inbox"].Messages.Request(new List<Microsoft.Graph.QueryOption> { new Microsoft.Graph.QueryOption("$search", $"\"subject:test\"") }).GetAsync();
                await stepContext.Context.SendActivityAsync($"Found {messages.Count} emails");
                return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
            }
            await stepContext.Context.SendActivityAsync(MessageFactory.Text("Login was not successful please try again."), cancellationToken);
            return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
        }
    }

然后,我需要将我的机器人 class 更改为:

    public class myBot : IBot

至此

    public class myBot<T> : TeamsActivityHandler where T:Dialog

此外,我需要确保清单中的 validDomains 将“token.botframework.com”添加到列表中(我之前错过了这一步)。

最后我在 ConfigureServices 中做了这个更改:

            //Add the auth dialog
            services.AddSingleton<AuthDialog>();

            services.AddBot<myBot<AuthDialog>>(options =>
            {
                options.CredentialProvider = new SimpleCredentialProvider(botConfig.BotID, botConfig.BotSecret);

                options.OnTurnError = async (context, exception) =>
                {
                    _log.Error("Exception caught-OnTurnError: ", exception);
                    await context.SendActivityAsync("Sorry, it looks like something went wrong.");
                };
            });

注意将对话框注册为单例,然后将模板参数(大概代表主对话框)添加到机器人。然后,在我的 OnTurnAsync 代码中,我能够处理几个测试输入:

        public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
        {
...
            else if (turnContext.Activity.Type == ActivityTypes.Message && turnContext.Activity.Text == "logout")
            {
                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 botAdapter = (BotFrameworkAdapter)dc.Context.Adapter;
                await botAdapter.SignOutUserAsync(dc.Context, "botTeamsAuth", null, cancellationToken);
                await turnContext.SendActivityAsync($"logged out of graph");
            }
            else if (turnContext.Activity.Type == ActivityTypes.Message && turnContext.Activity.Text == "login")
            {
                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);
            }
            else if (turnContext.Activity.Type == ActivityTypes.Invoke && turnContext.Activity.Name == "signin/verifyState")
            {
                var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
                var dialogSet = new DialogSet(dialogState);
                dialogSet.Add(new AuthDialog());  //this is counterintuitive, but it gets around the issue where I get the dialog missing exception
                DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
                var turnResult = await dc.ContinueDialogAsync();
                await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
            }

是的,其中有一些重复的代码,但为了清楚起见,我暂时将其保留原样。至少对我来说似乎有点违反直觉的是更新 DialogState 和 DialogSet 以检索现有的状态成员,但这似乎是它的工作原理。在此示例中,我只是从用户收件箱中提取几条消息来验证连接,但它确实按预期工作。如果用户已经登录,响应会立即返回,如果用户需要身份验证,则会发送登录卡并返回令牌响应。

无论如何,我想我现在已经畅通无阻了,希望这个解决方案能帮助处于类似情况的其他人。如果您想使用“魔术代码”身份验证流程,@Hilton 在他的回答中提到的方法也适用。在那种情况下。你会调用这样的东西来获得登录 link:

                var adapter = turnContext.Adapter as BotFrameworkAdapter;
                string url = await adapter.GetOauthSignInLinkAsync(turnContext, "botTeamsAuth");
                await turnContext.SendActivityAsync($"click here to sign in {url}");

然后从用户那里取回一个数字代码作为消息,您可以使用他的回答中提到的 GetUserTokenAsync() 将其提交。卡片登录似乎更简洁一些,因为它不需要弹出外部登录 window,并且 OAuthPrompt 对话框将令牌 retrieval/login 包装到一个步骤中。