-
Notifications
You must be signed in to change notification settings - Fork 486
/
DialogExtensions.cs
141 lines (121 loc) · 6.8 KB
/
DialogExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Skills;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
namespace Microsoft.Bot.Builder.Dialogs
{
/// <summary>
/// Provides extension methods for <see cref="Dialog"/> and derived classes.
/// </summary>
public static class DialogExtensions
{
/// <summary>
/// Creates a dialog stack and starts a dialog, pushing it onto the stack.
/// </summary>
/// <param name="dialog">The dialog to start.</param>
/// <param name="turnContext">The context for the current turn of the conversation.</param>
/// <param name="accessor">The <see cref="IStatePropertyAccessor{DialogState}"/> accessor
/// with which to manage the state of the dialog stack.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task RunAsync(this Dialog dialog, ITurnContext turnContext, IStatePropertyAccessor<DialogState> accessor, CancellationToken cancellationToken)
{
var dialogSet = new DialogSet(accessor) { TelemetryClient = dialog.TelemetryClient };
dialogSet.Add(dialog);
var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken).ConfigureAwait(false);
// Handle EoC and Reprompt event from a parent bot (can be root bot to skill or skill to skill)
if (IsFromParentToSkill(turnContext))
{
// Handle remote cancellation request from parent.
if (turnContext.Activity.Type == ActivityTypes.EndOfConversation)
{
if (!dialogContext.Stack.Any())
{
// No dialogs to cancel, just return.
return;
}
var activeDialogContext = GetActiveDialogContext(dialogContext);
var remoteCancelText = "Skill was canceled through an EndOfConversation activity from the parent.";
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{remoteCancelText}", cancellationToken: cancellationToken).ConfigureAwait(false);
// Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the right order.
await activeDialogContext.CancelAllDialogsAsync(true, cancellationToken: cancellationToken).ConfigureAwait(false);
return;
}
// Handle a reprompt event sent from the parent.
if (turnContext.Activity.Type == ActivityTypes.Event && turnContext.Activity.Name == DialogEvents.RepromptDialog)
{
if (!dialogContext.Stack.Any())
{
// No dialogs to reprompt, just return.
return;
}
await dialogContext.RepromptDialogAsync(cancellationToken).ConfigureAwait(false);
return;
}
}
// Continue or start the dialog.
var result = await dialogContext.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
if (result.Status == DialogTurnStatus.Empty)
{
result = await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken).ConfigureAwait(false);
}
// Skills should send EoC when the dialog completes.
if (result.Status == DialogTurnStatus.Complete || result.Status == DialogTurnStatus.Cancelled)
{
if (SendEoCToParent(turnContext))
{
var endMessageText = $"Dialog {dialog.Id} has **completed**. Sending EndOfConversation.";
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{endMessageText}", value: result.Result, cancellationToken: cancellationToken).ConfigureAwait(false);
// Send End of conversation at the end.
var activity = new Activity(ActivityTypes.EndOfConversation) { Value = result.Result, Locale = turnContext.Activity.Locale };
await turnContext.SendActivityAsync(activity, cancellationToken).ConfigureAwait(false);
}
}
}
/// <summary>
/// Helper to determine if we should send an EoC to the parent or not.
/// </summary>
private static bool SendEoCToParent(ITurnContext turnContext)
{
if (turnContext.TurnState.Get<IIdentity>(BotAdapter.BotIdentityKey) is ClaimsIdentity claimIdentity && SkillValidation.IsSkillClaim(claimIdentity.Claims))
{
// EoC Activities returned by skills are bounced back to the bot by SkillHandler.
// In those cases we will have a SkillConversationReference instance in state.
var skillConversationReference = turnContext.TurnState.Get<SkillConversationReference>(SkillHandler.SkillConversationReferenceKey);
if (skillConversationReference != null)
{
// If the skillConversationReference.OAuthScope is for one of the supported channels, we are at the root and we should not send an EoC.
return skillConversationReference.OAuthScope != AuthenticationConstants.ToChannelFromBotOAuthScope && skillConversationReference.OAuthScope != GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope;
}
return true;
}
return false;
}
private static bool IsFromParentToSkill(ITurnContext turnContext)
{
if (turnContext.TurnState.Get<SkillConversationReference>(SkillHandler.SkillConversationReferenceKey) != null)
{
return false;
}
return turnContext.TurnState.Get<IIdentity>(BotAdapter.BotIdentityKey) is ClaimsIdentity claimIdentity && SkillValidation.IsSkillClaim(claimIdentity.Claims);
}
// Recursively walk up the DC stack to find the active DC.
private static DialogContext GetActiveDialogContext(DialogContext dialogContext)
{
var child = dialogContext.Child;
if (child == null)
{
return dialogContext;
}
return GetActiveDialogContext(child);
}
}
}