44using System ;
55using System . Collections . Generic ;
66using System . Runtime . CompilerServices ;
7+ using System . Security . Claims ;
78using System . Text . RegularExpressions ;
89using System . Threading ;
910using System . Threading . Tasks ;
1011using AdaptiveExpressions . Properties ;
12+ using Microsoft . Bot . Builder ;
1113using Microsoft . Bot . Builder . Dialogs ;
1214using Microsoft . Bot . Builder . Dialogs . Adaptive ;
15+ using Microsoft . Bot . Components . Telephony . Common ;
1316using Microsoft . Bot . Schema ;
1417using Newtonsoft . Json ;
1518
@@ -23,6 +26,7 @@ public class BatchRegexInput : Dialog
2326 [ JsonProperty ( "$kind" ) ]
2427 public const string Kind = "Microsoft.Telephony.BatchRegexInput" ;
2528 protected const string AggregationDialogMemory = "this.aggregation" ;
29+ private static IStateMatrix stateMatrix = new LatchingStateMatrix ( ) ;
2630
2731 /// <summary>
2832 /// Initializes a new instance of the <see cref="BatchRegexInput"/> class.
@@ -85,9 +89,24 @@ public BatchRegexInput([CallerFilePath] string sourceFilePath = "", [CallerLineN
8589 [ JsonProperty ( "interruptionMask" ) ]
8690 public StringExpression InterruptionMask { get ; set ; }
8791
92+ /// <summary>
93+ /// Gets or sets a value indicating how long to wait for before timing out and using the default value.
94+ /// </summary>
95+ [ JsonProperty ( "timeOutInMilliseconds" ) ]
96+ public IntExpression TimeOutInMilliseconds { get ; set ; }
97+
98+ /// <summary>
99+ /// Gets or sets a default value when failing after a time out.
100+ /// </summary>
101+ [ JsonProperty ( "defaultValue" ) ]
102+ public StringExpression DefaultValue { get ; set ; }
103+
88104 /// <inheritdoc/>
89105 public override async Task < DialogTurnResult > BeginDialogAsync ( DialogContext dc , object options = null , CancellationToken cancellationToken = default ( CancellationToken ) )
90106 {
107+ //start a timer that will continue this conversation
108+ await InitTimeoutTimerAsync ( dc , cancellationToken ) . ConfigureAwait ( false ) ;
109+
91110 return await PromptUserAsync ( dc , cancellationToken ) . ConfigureAwait ( false ) ;
92111 }
93112
@@ -101,6 +120,14 @@ public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext d
101120 return await PromptUserAsync ( dc , cancellationToken ) . ConfigureAwait ( false ) ;
102121 }
103122
123+ //Handle case where we timed out
124+ var activity = dc . Context . Activity ;
125+ if ( ! wasInterrupted && activity . Type != ActivityTypes . Message && activity . Name == ActivityEventNames . ContinueConversation )
126+ {
127+ await SendActivityAsync ( dc , cancellationToken ) . ConfigureAwait ( false ) ;
128+ return await dc . EndDialogAsync ( cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
129+ }
130+
104131 //Get value of termination string from expression
105132 string regexPattern = this . TerminationConditionRegexPattern ? . GetValue ( dc . State ) ;
106133
@@ -120,6 +147,19 @@ public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext d
120147 }
121148 else
122149 {
150+ //If we didn't timeout then we have to manage our timer somehow.
151+ //For starters, complete our existing timer.
152+ var timerId = dc . State . GetValue < string > ( "this.TimerId" ) ;
153+
154+ //Should never happen but if it does, it shouldn't be fatal.
155+ if ( timerId != null )
156+ {
157+ await stateMatrix . CompleteAsync ( timerId ) . ConfigureAwait ( false ) ;
158+ }
159+
160+ // Restart the timeout timer
161+ await InitTimeoutTimerAsync ( dc , cancellationToken ) . ConfigureAwait ( false ) ;
162+
123163 //else, save the updated aggregation and end the turn
124164 dc . State . SetValue ( AggregationDialogMemory , existingAggregation ) ;
125165 return new DialogTurnResult ( DialogTurnStatus . Waiting ) ;
@@ -198,5 +238,59 @@ protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, Dial
198238
199239 return new DialogTurnResult ( DialogTurnStatus . Waiting ) ;
200240 }
241+
242+ private async Task < DialogTurnResult > SendActivityAsync ( DialogContext dc , CancellationToken cancellationToken = default ( CancellationToken ) )
243+ {
244+ var defaultValue = this . DefaultValue ?? throw new InvalidOperationException ( $ "Value for DefaultValue property is not defined.") ;
245+ var msg = new Activity ( )
246+ {
247+ From = new ChannelAccount ( )
248+ {
249+ Role = "bot"
250+ } ,
251+ Type = "message" ,
252+ Text = defaultValue . GetValue ( dc . State )
253+ } ;
254+ await dc . Context . SendActivityAsync ( msg , cancellationToken ) . ConfigureAwait ( false ) ;
255+
256+ return new DialogTurnResult ( DialogTurnStatus . Complete ) ;
257+ }
258+
259+ private void CreateTimerForConversation ( DialogContext dc , string timerId , CancellationToken cancellationToken )
260+ {
261+ BotAdapter adapter = dc . Context . Adapter ;
262+ var identity = dc . Context . TurnState . Get < ClaimsIdentity > ( "BotIdentity" ) ;
263+
264+ ConversationReference conversationReference = dc . Context . Activity . GetConversationReference ( ) ;
265+ var timeoutExpression = this . TimeOutInMilliseconds ?? throw new InvalidOperationException ( $ "Value for TimeOutInMilliseconds property is not defined.") ;
266+ int timeout = timeoutExpression . GetValue ( dc . State ) ;
267+
268+ var audience = dc . Context . TurnState . Get < string > ( BotAdapter . OAuthScopeKey ) ;
269+
270+ //Question remaining to be answered: Will this task get garbage collected? If so, we need to maintain a handle for it.
271+ Task . Run ( async ( ) =>
272+ {
273+ await Task . Delay ( timeout ) . ConfigureAwait ( false ) ;
274+
275+ //if we aren't already complete, go ahead and timeout
276+ await stateMatrix . RunForStatusAsync ( timerId , StateStatus . Running , async ( ) =>
277+ {
278+ await adapter . ContinueConversationAsync (
279+ identity ,
280+ conversationReference ,
281+ audience ,
282+ BotWithLookup . OnTurn , //Leverage dirty hack to achieve Bot lookup from component
283+ cancellationToken ) . ConfigureAwait ( false ) ;
284+ } ) . ConfigureAwait ( false ) ;
285+ } ) ;
286+ }
287+
288+ private async Task InitTimeoutTimerAsync ( DialogContext dc , CancellationToken cancellationToken )
289+ {
290+ var timerId = Guid . NewGuid ( ) . ToString ( ) ;
291+ CreateTimerForConversation ( dc , timerId , cancellationToken ) ;
292+ await stateMatrix . StartAsync ( timerId ) . ConfigureAwait ( false ) ;
293+ dc . State . SetValue ( "this.TimerId" , timerId ) ;
294+ }
201295 }
202296}
0 commit comments