diff --git a/.gitignore b/.gitignore
index 75c6634b81..b646ad1e38 100644
--- a/.gitignore
+++ b/.gitignore
@@ -403,4 +403,5 @@ experimental/generator-dotnet-yeoman/node_modules
package-lock.json
# Ignore test bots
-testing/*
\ No newline at end of file
+testing/*
+packages/.vs/*
diff --git a/packages/Telephony/Actions/ITimeoutInput.cs b/packages/Telephony/Actions/ITimeoutInput.cs
new file mode 100644
index 0000000000..ca5ba1abc8
--- /dev/null
+++ b/packages/Telephony/Actions/ITimeoutInput.cs
@@ -0,0 +1,21 @@
+using AdaptiveExpressions.Properties;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.Bot.Components.Telephony.Actions
+{
+ public interface ITimeoutInput
+ {
+ // Summary:
+ // Defines dialog context state property value.
+ const string SilenceDetected = "dialog.silenceDetected";
+
+ ///
+ /// Gets or sets a value indicating how long to wait for before timing out and using the default value.
+ ///
+ [JsonProperty("timeOutInMilliseconds")]
+ IntExpression TimeOutInMilliseconds { get; set; }
+ }
+}
diff --git a/packages/Telephony/Actions/TimeoutChoiceInput.cs b/packages/Telephony/Actions/TimeoutChoiceInput.cs
index f86cf6e66b..6f3f3d872b 100644
--- a/packages/Telephony/Actions/TimeoutChoiceInput.cs
+++ b/packages/Telephony/Actions/TimeoutChoiceInput.cs
@@ -16,12 +16,16 @@
namespace Microsoft.Bot.Components.Telephony.Actions
{
- public class TimeoutChoiceInput : ChoiceInput
+ public class TimeoutChoiceInput : ChoiceInput, ITimeoutInput
{
[JsonProperty("$kind")]
public new const string Kind = "Microsoft.Telephony.TimeoutChoiceInput";
- private static IStateMatrix stateMatrix = new LatchingStateMatrix();
+ ///
+ /// Gets or sets a value indicating how long to wait for before timing out and using the default value.
+ ///
+ [JsonProperty("timeOutInMilliseconds")]
+ public IntExpression TimeOutInMilliseconds { get; set; }
[JsonConstructor]
public TimeoutChoiceInput([CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
@@ -31,118 +35,20 @@ public TimeoutChoiceInput([CallerFilePath] string sourceFilePath = "", [CallerLi
this.RegisterSourceLocation(sourceFilePath, sourceLineNumber);
}
- ///
- /// Gets or sets a value indicating how long to wait for before timing out and using the default value.
- ///
- [JsonProperty("timeOutInMilliseconds")]
- public IntExpression TimeOutInMilliseconds { get; set; }
-
public async override Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
{
- if (options is CancellationToken)
- {
- throw new ArgumentException($"{nameof(options)} cannot be a cancellation token");
- }
-
- if (this.Disabled != null && this.Disabled.GetValue(dc.State) == true)
- {
- return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
- }
-
- //start a timer that will continue this conversation
- var timerId = Guid.NewGuid().ToString();
- CreateTimerForConversation(dc, timerId, cancellationToken);
- await stateMatrix.StartAsync(timerId).ConfigureAwait(false);
- dc.State.SetValue("this.TimerId", timerId);
-
- return await base.BeginDialogAsync(dc, options, cancellationToken).ConfigureAwait(false);
+ return await TimeoutInput.BeginDialogAsync(this, dc,
+ base.BeginDialogAsync,
+ options, cancellationToken);
}
public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
{
- var activity = dc.Context.Activity;
-
- //Handle case where we timed out
- var interrupted = dc.State.GetValue(TurnPath.Interrupted, () => false);
- if (!interrupted && activity.Type != ActivityTypes.Message && activity.Name == ActivityEventNames.ContinueConversation)
- {
- //Set max turns so that we evaluate the default when we visit the inputdialog.
- MaxTurnCount = 1;
-
- //We need to set interrupted here or it will discard the continueconversation event...
- dc.State.SetValue(TurnPath.Interrupted, true);
- return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- //If we didn't timeout then we have to manage our timer somehow.
- //For starters, complete our existing timer.
- var timerId = dc.State.GetValue("this.TimerId");
-
- //Should never happen but if it does, it shouldn't be fatal.
- if (timerId != null)
- {
- await stateMatrix.CompleteAsync(timerId).ConfigureAwait(false);
- }
-
- //Begin dirty hack to start a timer for the reprompt
-
- //If our input was not valid, restart the timer.
- dc.State.SetValue(VALUE_PROPERTY, activity.Text); //OnRecognizeInput assumes this was already set earlier on in the dialog. We will set it and then unset it to simulate passing an argument to a function... :D
- if (await OnRecognizeInputAsync(dc, cancellationToken).ConfigureAwait(false) != InputState.Valid)
- {
- //We are cheating to force this recognition here. Maybe not good?
-
- //Known bug: Sometimes invalid input gets accepted anyway(due to max turns and defaulting rules), this will start a continuation for a thing it shouldn't.
- //Sure do wish EndDialog was available to the adaptive stack.
-
- var newTimerId = Guid.NewGuid().ToString();
- CreateTimerForConversation(dc, newTimerId, cancellationToken);
- await stateMatrix.StartAsync(newTimerId).ConfigureAwait(false);
- }
-
- //Clear our the input property after recognition since it will happen again later :D
- dc.State.SetValue(VALUE_PROPERTY, null);
-
- //End dirty hack
- }
-
- return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false);
+ return await TimeoutInput.ContinueDialogAsync(this, dc, VALUE_PROPERTY, TURN_COUNT_PROPERTY,
+ OnRecognizeInputAsync,
+ base.ContinueDialogAsync,
+ cancellationToken);
}
- private void CreateTimerForConversation(DialogContext dc, string timerId, CancellationToken cancellationToken)
- {
- BotAdapter adapter = dc.Context.Adapter;
- var identity = dc.Context.TurnState.Get("BotIdentity");
-
- var appId = identity?.Claims?.FirstOrDefault(c => c.Type == AuthenticationConstants.AudienceClaim)?.Value;
- ConversationReference conversationReference = dc.Context.Activity.GetConversationReference();
- int timeout = TimeOutInMilliseconds.GetValue(dc.State);
-
- //Question remaining to be answered: Will this task get garbage collected? If so, we need to maintain a handle for it.
- Task.Run(async () =>
- {
- await Task.Delay(timeout).ConfigureAwait(false);
- string msAppId = appId;
-
- // If the channel is the Emulator, and authentication is not in use,
- // the AppId will be null. We generate a random AppId for this case only.
- // This is not required for production, since the AppId will have a value.
- if (string.IsNullOrEmpty(msAppId))
- {
- msAppId = Guid.NewGuid().ToString(); //if no AppId, use a random Guid
- }
-
- //if we aren't already complete, go ahead and timeout
- await stateMatrix.RunForStatusAsync(timerId, StateStatus.Running, async () =>
- {
- await adapter.ContinueConversationAsync(
- msAppId,
- conversationReference,
- BotWithLookup.OnTurn, //Leverage dirty hack to achieve Bot lookup from component
- cancellationToken).ConfigureAwait(false);
- }).ConfigureAwait(false);
- });
- }
}
}
\ No newline at end of file
diff --git a/packages/Telephony/Actions/TimeoutInput.cs b/packages/Telephony/Actions/TimeoutInput.cs
new file mode 100644
index 0000000000..dccf3f7482
--- /dev/null
+++ b/packages/Telephony/Actions/TimeoutInput.cs
@@ -0,0 +1,231 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Security.Claims;
+using System.Threading;
+using System.Threading.Tasks;
+using AdaptiveExpressions.Properties;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Builder.Adapters;
+using Microsoft.Bot.Builder.Dialogs;
+using Microsoft.Bot.Builder.Dialogs.Adaptive.Input;
+using Microsoft.Bot.Components.Telephony.Common;
+using Microsoft.Bot.Connector.Authentication;
+using Microsoft.Bot.Schema;
+using Newtonsoft.Json;
+
+namespace Microsoft.Bot.Components.Telephony.Actions
+{
+ public static class TimeoutInput
+ {
+ // Summary:
+ // Defines dialog context state property value.
+ private const string TimerID = "dialog.TimerId";
+ //private const string TimeoutId = "this.TimeoutId";
+ //private const string ActiveTimeoutId = "conversation.ActiveTimeoutId";
+
+ private static ConcurrentDictionary triggeredTimers, IStateMatrix timersState)> conversationStateMatrix = new ConcurrentDictionary triggeredTimers, IStateMatrix timersState)>();
+
+ public static async Task BeginDialogAsync(T inputActivity, DialogContext dc,
+ Func> baseClassCall,
+ object options = null, CancellationToken cancellationToken = default(CancellationToken)) where T : InputDialog, ITimeoutInput
+ {
+ if (options is CancellationToken)
+ {
+ throw new ArgumentException($"{nameof(options)} cannot be a cancellation token");
+ }
+
+ if (inputActivity.Disabled?.GetValue(dc.State) == true)
+ {
+ return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ dc.State.SetValue(ITimeoutInput.SilenceDetected, false);
+
+ //start a timer that will continue this conversation
+ var timerId = CreateTimerForConversation(inputActivity, dc, cancellationToken);
+ if (!conversationStateMatrix.TryGetValue(dc.Context.Activity.Conversation.Id, out var convState))
+ {
+ convState = (new ConcurrentQueue(), new LatchingStateMatrix());
+ conversationStateMatrix[dc.Context.Activity.Conversation.Id] = convState;
+ }
+ await convState.timersState.StartAsync(timerId).ConfigureAwait(false);
+
+ dc.Services.Get().TrackEvent("Start TimeoutInput", new Dictionary
+ {
+ {"timerId", timerId}
+ });
+
+ var res = await baseClassCall(dc, options, cancellationToken).ConfigureAwait(false);
+ return res;
+ }
+
+ public static async Task ContinueDialogAsync(K inputActivity, DialogContext dc, string valueProperty, string turnCountProperty,
+ Func> onRecognizeInputAsync,
+ Func> continueDialogAsync,
+ CancellationToken cancellationToken = default(CancellationToken)) where K : InputDialog, ITimeoutInput
+ {
+ var timerId = dc.State.GetValue(TimerID);
+
+ //conversation is ended
+ if (!conversationStateMatrix.TryGetValue(dc.Context.Activity.Conversation.Id, out var convState))
+ return Dialog.EndOfTurn;
+
+
+ //if we aren't already complete, go ahead and timeout
+ return await convState.timersState.RunForStatusAsync(timerId, StateStatus.Running, async () =>
+ {
+ //-----------------------------Body---------------
+ var activity = dc.Context.Activity;
+
+ //Handle case where we timed out
+ var interrupted = dc.State.GetValue(TurnPath.Interrupted, () => false);
+ if (!interrupted && activity.Type != ActivityTypes.Message && activity.Name == ActivityEventNames.ContinueConversation)
+ {
+ string calledTimerId = "";
+ //if there is no matched conversation (could be removed by endOfConversations, or there is no called timer (shouldn't happen) or
+ //the last called Timer is not the same as activity's timer, then don't continue
+ if (!convState.triggeredTimers.TryDequeue(out calledTimerId) || calledTimerId != timerId)
+ {
+ dc.Services.Get().TrackEvent("Abort Timer routine", new Dictionary
+ {
+ {"timerId", calledTimerId}
+ });
+ return Dialog.EndOfTurn;
+ }
+
+
+ dc.Services.Get().TrackEvent("Continue Timer", new Dictionary
+ {
+ {"timerId", timerId}
+ });
+
+ //stop any more incoming event for this activity
+ convState.timersState.ForceComplete(timerId);
+
+ //Set max turns so that we evaluate the default when we visit the inputdialog.
+ var oldValue = inputActivity.MaxTurnCount;
+ inputActivity.MaxTurnCount = 1;
+
+ //We need to set interrupted here or it will discard the continueconversation event...
+ DialogTurnResult result;
+ try
+ {
+ dc.State.SetValue(ITimeoutInput.SilenceDetected, true);
+ dc.State.SetValue(TurnPath.Interrupted, true);
+ result = await continueDialogAsync(dc, cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ inputActivity.MaxTurnCount = oldValue;
+ }
+
+ return result;
+ }
+
+ //continue for any other events
+
+ //stop any more incoming event for this activity
+ convState.timersState.ForceComplete(timerId);
+
+ //check if it is user input
+ if (activity.Type == ActivityTypes.Message)
+ {
+ dc.Services.Get().TrackEvent("User Continue", new Dictionary
+ {
+ {"timerId", timerId}
+ });
+
+ //Begin dirty hack to start a timer for the reprompt
+
+ //If our input was not valid, restart the timer.
+ dc.State.SetValue(valueProperty, dc.Context.Activity.Text); //OnRecognizeInput assumes this was already set earlier on in the dialog. We will set it and then unset it to simulate passing an argument to a function... :D
+
+ if (await onRecognizeInputAsync(dc, cancellationToken).ConfigureAwait(false) != InputState.Valid)
+ {
+ var turnCount = dc.State.GetValue(turnCountProperty, () => 0);
+ if (turnCount < inputActivity.MaxTurnCount?.GetValue(dc.State))
+ {
+ //We are cheating to force this recognition here. Maybe not good?
+
+ //Known bug: Sometimes invalid input gets accepted anyway(due to max turns and defaulting rules), this will start a continuation for a thing it shouldn't.
+ //Sure do wish EndDialog was available to the adaptive stack.
+
+ timerId = inputActivity.CreateTimerForConversation(dc, cancellationToken);
+ await convState.timersState.StartAsync(timerId).ConfigureAwait(false);
+ }
+ }
+
+ //Clear our the input property after recognition since it will happen again later :D
+ dc.State.SetValue(valueProperty, null);
+
+
+ //End dirty hack
+ }
+
+ return await continueDialogAsync(dc, cancellationToken).ConfigureAwait(false);
+ },
+ () =>
+ {
+ dc.Services.Get().TrackEvent("Stop the routine", new Dictionary
+ {
+ {"timerId", timerId}
+ });
+ return Task.FromResult(Dialog.EndOfTurn);
+ });
+ }
+
+ public static string CreateTimerForConversation(this K inputActivity, DialogContext dc, CancellationToken cancellationToken) where K : InputDialog, ITimeoutInput
+ {
+ var timerId = Guid.NewGuid().ToString();
+ dc.State.SetValue(TimerID, timerId);
+ BotAdapter adapter = dc.Context.Adapter;
+ var identity = dc.Context.TurnState.Get("BotIdentity");
+
+ var appId = identity?.Claims?.FirstOrDefault(c => c.Type == AuthenticationConstants.AudienceClaim)?.Value;
+ ConversationReference conversationReference = dc.Context.Activity.GetConversationReference();
+
+ dc.Services.Get().TrackEvent("Creating Timer", new Dictionary
+ {
+ {"timerId", timerId}
+ });
+
+ int timeout = inputActivity.TimeOutInMilliseconds.GetValue(dc.State);
+
+ //Question remaining to be answered: Will this task get garbage collected? If so, we need to maintain a handle for it.
+ Task.Run(async () =>
+ {
+ await Task.Delay(timeout).ConfigureAwait(false);
+ if (!conversationStateMatrix.TryGetValue(conversationReference.Conversation.Id, out var convState))
+ return;
+ convState.triggeredTimers.Enqueue(timerId);
+ // If the channel is the Emulator, and authentication is not in use,
+ // the AppId will be null. We generate a random AppId for this case only.
+ // This is not required for production, since the AppId will have a value.
+ if (string.IsNullOrEmpty(appId))
+ {
+ appId = Guid.NewGuid().ToString(); //if no AppId, use a random Guid
+ }
+
+
+ dc.Services.Get().TrackEvent("Timer Triggered", new Dictionary
+ {
+ {"timerId", timerId}
+ });
+ await adapter.ContinueConversationAsync(
+ appId,
+ conversationReference,
+ BotWithLookup.OnTurn, //Leverage dirty hack to achieve Bot lookup from component
+ cancellationToken).ConfigureAwait(false);
+ });
+ return timerId;
+ }
+
+ public static void RemoveTimers(ITurnContext turnContext)
+ {
+ conversationStateMatrix.TryRemove(turnContext.Activity.Conversation.Id, out _);
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/Telephony/Actions/TimeoutTextInput.cs b/packages/Telephony/Actions/TimeoutTextInput.cs
new file mode 100644
index 0000000000..9c4c4f187f
--- /dev/null
+++ b/packages/Telephony/Actions/TimeoutTextInput.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Security.Claims;
+using System.Threading;
+using System.Threading.Tasks;
+using AdaptiveExpressions.Properties;
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Builder.Adapters;
+using Microsoft.Bot.Builder.Dialogs;
+using Microsoft.Bot.Builder.Dialogs.Adaptive.Input;
+using Microsoft.Bot.Components.Telephony.Common;
+using Microsoft.Bot.Connector.Authentication;
+using Microsoft.Bot.Schema;
+using Newtonsoft.Json;
+
+namespace Microsoft.Bot.Components.Telephony.Actions
+{
+ public class TimeoutTextInput : TextInput, ITimeoutInput
+ {
+ [JsonProperty("$kind")]
+ public new const string Kind = "Microsoft.Telephony.TimeoutTextInput";
+
+ ///
+ /// Gets or sets a value indicating how long to wait for before timing out and using the default value.
+ ///
+ [JsonProperty("timeOutInMilliseconds")]
+ public IntExpression TimeOutInMilliseconds { get; set; }
+
+ [JsonConstructor]
+ public TimeoutTextInput([CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
+ : base()
+ {
+ // enable instances of this command as debug break point
+ this.RegisterSourceLocation(sourceFilePath, sourceLineNumber);
+ }
+
+ public async override Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return await TimeoutInput.BeginDialogAsync(this, dc,
+ base.BeginDialogAsync,
+ options, cancellationToken);
+ }
+
+ public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return await TimeoutInput.ContinueDialogAsync(this, dc, VALUE_PROPERTY, TURN_COUNT_PROPERTY,
+ OnRecognizeInputAsync,
+ base.ContinueDialogAsync,
+ cancellationToken);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/packages/Telephony/Common/IStateMatrix.cs b/packages/Telephony/Common/IStateMatrix.cs
index da4a1359a9..783e90d42b 100644
--- a/packages/Telephony/Common/IStateMatrix.cs
+++ b/packages/Telephony/Common/IStateMatrix.cs
@@ -24,9 +24,10 @@ internal interface IStateMatrix
Task GetStatusForIdAsync(string id);
Task CompleteAsync(string id);
+ void ForceComplete(string id);
Task StartAsync(string id);
- Task RunForStatusAsync(string id, StateStatus status, Func action);
+ Task RunForStatusAsync(string id, StateStatus status, Func> matchedAction, Func> notMatchedAction);
}
}
diff --git a/packages/Telephony/Common/LatchingStateMatrix.cs b/packages/Telephony/Common/LatchingStateMatrix.cs
index 8d08045784..94335d5f5a 100644
--- a/packages/Telephony/Common/LatchingStateMatrix.cs
+++ b/packages/Telephony/Common/LatchingStateMatrix.cs
@@ -27,6 +27,19 @@ public async Task CompleteAsync(string id)
}
}
+ public void ForceComplete(string id)
+ {
+ (SemaphoreSlim semaphore, _) = perStateIndexLatching[id];
+ try
+ {
+ perStateIndexLatching[id] = (semaphore, StateStatus.Completed);
+ }
+ finally
+ {
+ semaphore.Release();
+ }
+ }
+
public async Task GetStatusForIdAsync(string id)
{
(SemaphoreSlim semaphore, _) = perStateIndexLatching[id];
@@ -44,7 +57,7 @@ public async Task GetStatusForIdAsync(string id)
}
}
- public async Task RunForStatusAsync(string id, StateStatus status, Func action)
+ public async Task RunForStatusAsync(string id, StateStatus status, Func> matchedAction, Func> notMatchedAction)
{
(SemaphoreSlim semaphore, _) = perStateIndexLatching[id];
try
@@ -54,9 +67,8 @@ public async Task RunForStatusAsync(string id, StateStatus status, Func ac
//retrieve status again, since we got it wrong the first time XD
(_, StateStatus updatedStatus) = perStateIndexLatching[id];
if (updatedStatus == status)
- {
- await action().ConfigureAwait(false);
- }
+ return await matchedAction().ConfigureAwait(false);
+ else return await notMatchedAction().ConfigureAwait(false);
}
finally
{
diff --git a/packages/Telephony/Microsoft.Bot.Components.Telephony.csproj b/packages/Telephony/Microsoft.Bot.Components.Telephony.csproj
index 4f38a5a37a..506fd2d509 100644
--- a/packages/Telephony/Microsoft.Bot.Components.Telephony.csproj
+++ b/packages/Telephony/Microsoft.Bot.Components.Telephony.csproj
@@ -2,7 +2,7 @@
Library
- netstandard2.0
+ netstandard2.1
@@ -19,6 +19,7 @@
+
@@ -51,12 +52,16 @@
+
+
$(NoWarn),SA0001,SA1649
+ False
+ False
diff --git a/packages/Telephony/Middleware/ContextReceiverMiddleware.cs b/packages/Telephony/Middleware/ContextReceiverMiddleware.cs
new file mode 100644
index 0000000000..6f1b59b611
--- /dev/null
+++ b/packages/Telephony/Middleware/ContextReceiverMiddleware.cs
@@ -0,0 +1,60 @@
+// ---------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+// ---------------------------------------------------------------------------
+
+using Microsoft.Bot.Builder;
+using Microsoft.Bot.Schema;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Bot.Components.Telephony.Middleware
+{
+ ///
+ /// The middleware that handles all outgoing messages
+ ///
+ public class ContextReceiverMiddleware : IMiddleware
+ {
+
+ public delegate void EventReceiverHandler(ITurnContext turnContext);
+ private Dictionary> _receivers;
+
+ ///
+ /// Initializes a new SetSpeakMiddleware class
+ ///
+ public ContextReceiverMiddleware()
+ {
+ _receivers = new Dictionary>();
+ }
+
+ ///
+ /// Handles the outgoing message
+ ///
+ /// The turn context
+ /// The next delegate
+ /// The cancellation token
+ ///
+ public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
+ {
+ if (!string.IsNullOrEmpty(turnContext.Activity?.Type) && _receivers.TryGetValue(turnContext.Activity.Type, out var receivers))
+ receivers.ForEach(rc => rc.Invoke(turnContext));
+
+ await next(cancellationToken);
+ }
+
+ public void addEventReceiver(string eventName, EventReceiverHandler handler)
+ {
+ if (!_receivers.ContainsKey(eventName)) _receivers.Add(eventName, new List());
+ _receivers[eventName].Add(handler);
+ }
+
+ public void removeEventReceiver(string eventName, EventReceiverHandler handler)
+ {
+ if (!_receivers.ContainsKey(eventName)) return;
+ _receivers[eventName].Remove(handler);
+ }
+
+ }
+}
diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutChoiceInput.schema b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutChoiceInput.schema
index 9db6c1e4a9..8e91a522fe 100644
--- a/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutChoiceInput.schema
+++ b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutChoiceInput.schema
@@ -1,7 +1,7 @@
{
"$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema",
"$role": [ "implements(Microsoft.IDialog)", "extends(Microsoft.InputDialog)" ],
- "title": "(Preview) Choice input dialog with silence detection",
+ "title": "Choice input dialog with silence detection",
"description": "Collect information - Pick from a list of choices",
"type": "object",
"properties": {
diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.schema b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.schema
new file mode 100644
index 0000000000..8515dd7d48
--- /dev/null
+++ b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.schema
@@ -0,0 +1,50 @@
+{
+ "$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema",
+ "$role": [ "implements(Microsoft.IDialog)", "extends(Microsoft.InputDialog)" ],
+ "type": "object",
+ "title": "Text input dialog with silence detection",
+ "description": "Collection information - Ask for a word or sentence.",
+ "properties": {
+ "timeOutInMilliseconds": {
+ "$ref": "schema:#/definitions/integerExpression",
+ "title": "Timeout in milliseconds",
+ "description": "After the specified amount of milliseconds the dialog will complete with its default value if the user doesn't respond.",
+ "examples": [
+ "10",
+ "=conversation.xyz"
+ ]
+ },
+ "defaultValue": {
+ "$ref": "schema:#/definitions/stringExpression",
+ "title": "Default value",
+ "description": "'Property' will be set to the value of this expression when max turn count is exceeded.",
+ "examples": [
+ "hello world",
+ "Hello ${user.name}",
+ "=concat(user.firstname, user.lastName)"
+ ]
+ },
+ "value": {
+ "$ref": "schema:#/definitions/stringExpression",
+ "title": "Value",
+ "description": "'Property' will be set to the value of this expression unless it evaluates to null.",
+ "examples": [
+ "hello world",
+ "Hello ${user.name}",
+ "=concat(user.firstname, user.lastName)"
+ ]
+ },
+ "outputFormat": {
+ "$ref": "schema:#/definitions/stringExpression",
+ "title": "Output format",
+ "description": "Expression to format the output.",
+ "examples": [
+ "=toUpper(this.value)",
+ "${toUpper(this.value)}"
+ ]
+ }
+ },
+ "$policies": {
+ "interactive": true
+ }
+}
diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.uischema b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.uischema
new file mode 100644
index 0000000000..8f7965aa25
--- /dev/null
+++ b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.uischema
@@ -0,0 +1,63 @@
+{
+ "$schema": "https://schemas.botframework.com/schemas/ui/v1.0/ui.schema",
+ "form": {
+ "label": "Prompt for text",
+ "subtitle": "Text Input",
+ "helpLink": "https://aka.ms/bfc-ask-for-user-input",
+ "order": [
+ "prompt",
+ "timeOutInMilliseconds",
+ "*"
+ ]
+ "properties": {
+ "property": {
+ "intellisenseScopes": [
+ "variable-scopes"
+ ]
+ }
+ }
+ },
+ "menu": {
+ "label": "Text input | Preview",
+ "submenu": [ "Silence Detection" ]
+ },
+ "flow": {
+ "widget": "PromptWidget",
+ "body": "=action.prompt",
+ "nowrap": true,
+ "botAsks": {
+ "widget": "ActionCard",
+ "header": {
+ "widget": "ActionHeader",
+ "icon": "MessageBot",
+ "colors": {
+ "theme": "#EEEAF4",
+ "icon": "#5C2E91"
+ }
+ },
+ "body": {
+ "widget": "LgWidget",
+ "field": "prompt",
+ "defaultContent": ""
+ }
+ },
+ "userInput": {
+ "widget": "ActionCard",
+ "header": {
+ "widget": "ActionHeader",
+ "disableSDKTitle": true,
+ "icon": "User",
+ "menu": "none",
+ "colors": {
+ "theme": "#E5F0FF",
+ "icon": "#0078D4"
+ }
+ },
+ "body": {
+ "widget": "LgWidget",
+ "field": "prompt",
+ "defaultContent": ""
+ }
+ }
+ }
+}
diff --git a/packages/Telephony/TelephonyBotComponent.cs b/packages/Telephony/TelephonyBotComponent.cs
index ab9cf9e4be..0020fb8bcd 100644
--- a/packages/Telephony/TelephonyBotComponent.cs
+++ b/packages/Telephony/TelephonyBotComponent.cs
@@ -6,6 +6,7 @@ namespace Microsoft.Bot.Components.Telephony
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs.Declarative;
using Microsoft.Bot.Components.Telephony.Actions;
+ using Microsoft.Bot.Schema;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -18,6 +19,10 @@ public class TelephonyBotComponent : BotComponent
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
// Conditionals
+ var middleware = new Middleware.ContextReceiverMiddleware();
+ services.AddSingleton(middleware);
+ middleware.addEventReceiver(ActivityTypes.EndOfConversation, TimeoutInput.RemoveTimers);
+
services.AddSingleton(sp => new DeclarativeType(CallTransfer.Kind));
services.AddSingleton(sp => new DeclarativeType(PauseRecording.Kind));
services.AddSingleton(sp => new DeclarativeType(ResumeRecording.Kind));
@@ -26,6 +31,7 @@ public override void ConfigureServices(IServiceCollection services, IConfigurati
services.AddSingleton(sp => new DeclarativeType(BatchTerminationCharacterInput.Kind));
services.AddSingleton(sp => new DeclarativeType(BatchRegexInput.Kind));
services.AddSingleton(sp => new DeclarativeType(TimeoutChoiceInput.Kind));
+ services.AddSingleton(sp => new DeclarativeType(TimeoutTextInput.Kind));
services.AddSingleton(sp => new DeclarativeType(SerialNumberInput.Kind));
}
}