在 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 包装到一个步骤中。
我有一个以 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 包装到一个步骤中。