diff --git a/.gitignore b/.gitignore index 1635c14d55..54da99a7b6 100644 --- a/.gitignore +++ b/.gitignore @@ -383,6 +383,7 @@ experimental/generator-dotnet-yeoman/node_modules # Allow the root-level /packages/ directory or it gets confused with NuGet directories !/[Pp]ackages/* +!/tests/[Pp]ackages/* # Allow the packages directory under tests !/tests/unit/[Pp]ackages/* diff --git a/build/yaml/pipelines/telephony.yml b/build/yaml/pipelines/telephony.yml index d18a52503d..7d92da5200 100644 --- a/build/yaml/pipelines/telephony.yml +++ b/build/yaml/pipelines/telephony.yml @@ -7,8 +7,6 @@ pool: extends: template: ../templates/component-template.yml - parameters: - PublishPackageArtifacts: # parameter is set in ADO variables: BuildConfiguration: 'Release' diff --git a/build/yaml/templates/nuget-versioning-steps.yml b/build/yaml/templates/nuget-versioning-steps.yml index 9192584a46..022e483ed7 100644 --- a/build/yaml/templates/nuget-versioning-steps.yml +++ b/build/yaml/templates/nuget-versioning-steps.yml @@ -53,7 +53,7 @@ steps: # Configure version suffix based on deployment ring if ($deploymentRing.ToLowerInvariant() -ne "stable") { - $nugetVersionSuffix = $deploymentRing + $vs; + $nugetVersionSuffix = $deploymentRing; "Version Suffix = $nugetVersionSuffix"; $packageVersion += "-" + $nugetVersionSuffix; } @@ -73,4 +73,4 @@ steps: NugetPackageVersion: $(NugetPackageVersion) continueOnError: true -- template: debug-workspace-steps.yml \ No newline at end of file +- template: debug-workspace-steps.yml diff --git a/packages/Microsoft.Bot.Components.sln b/packages/Microsoft.Bot.Components.sln index 649edc2e7c..ffc6cf54e9 100644 --- a/packages/Microsoft.Bot.Components.sln +++ b/packages/Microsoft.Bot.Components.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Components.Ad EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Components.Teams", "Teams\dotnet\Microsoft.Bot.Components.Teams.csproj", "{FD29CBA6-C18F-498B-9F00-A3C34C1BEC5F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Components.Telephony", "Telephony\Microsoft.Bot.Components.Telephony.csproj", "{A854B5EC-3A34-4D1F-8080-F0846DEDF63F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {FD29CBA6-C18F-498B-9F00-A3C34C1BEC5F}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD29CBA6-C18F-498B-9F00-A3C34C1BEC5F}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD29CBA6-C18F-498B-9F00-A3C34C1BEC5F}.Release|Any CPU.Build.0 = Release|Any CPU + {A854B5EC-3A34-4D1F-8080-F0846DEDF63F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A854B5EC-3A34-4D1F-8080-F0846DEDF63F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A854B5EC-3A34-4D1F-8080-F0846DEDF63F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A854B5EC-3A34-4D1F-8080-F0846DEDF63F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/packages/Telephony/Actions/CallTransfer.cs b/packages/Telephony/Actions/CallTransfer.cs new file mode 100644 index 0000000000..d89250c1f5 --- /dev/null +++ b/packages/Telephony/Actions/CallTransfer.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AdaptiveExpressions.Properties; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Adaptive.Actions; +using Microsoft.Bot.Connector; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Components.Telephony.Actions +{ + /// + /// Transfers call to given phone number. + /// + public class CallTransfer : SendHandoffActivity + { + /// + /// Class identifier. + /// + [JsonProperty("$kind")] + public new const string Kind = "Microsoft.Telephony.CallTransfer"; + + /// + /// Initializes a new instance of the class. + /// + /// Optional, source file full path. + /// Optional, line number in source file. + [JsonConstructor] + public CallTransfer([CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + : base() + { + // enable instances of this command as debug break point + this.RegisterSourceLocation(sourceFilePath, sourceLineNumber); + } + + /// + /// Gets or sets the phone number to be included when sending the handoff activity. + /// + /// + /// . + /// + [JsonProperty("phoneNumber")] + public StringExpression PhoneNumber { get; set; } + + /// + /// Called when the dialog is started and pushed onto the dialog stack. + /// + /// The for the current turn of conversation. + /// Optional, initial information to pass to the dialog. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A representing the asynchronous operation. + public async override Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken)) + { + if (dc.Context.Activity.ChannelId == Channels.Telephony) + { + var phoneNumber = this.PhoneNumber?.GetValue(dc.State); + + // Create handoff event, passing the phone number to transfer to as context. + HandoffContext = new { TargetPhoneNumber = phoneNumber }; + + return await base.BeginDialogAsync(dc, options, cancellationToken).ConfigureAwait(false); + } + + return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/packages/Telephony/Actions/PauseRecording.cs b/packages/Telephony/Actions/PauseRecording.cs new file mode 100644 index 0000000000..b80b5f81bf --- /dev/null +++ b/packages/Telephony/Actions/PauseRecording.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Components.Telephony.Common; +using Microsoft.Bot.Connector; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Components.Telephony.Actions +{ + /// + /// Pauses recording the current conversation. + /// + public class PauseRecording : CommandDialog + { + /// + /// Class identifier. + /// + [JsonProperty("$kind")] + public const string Kind = "Microsoft.Telephony.PauseRecording"; + + private const string RecordingPause = "channel/vnd.microsoft.telephony.recording.pause"; + + /// + /// Initializes a new instance of the class. + /// + /// Optional, source file full path. + /// Optional, line number in source file. + [JsonConstructor] + public PauseRecording([CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + : base() + { + // enable instances of this command as debug break point + this.RegisterSourceLocation(sourceFilePath, sourceLineNumber); + + this.CommandName = RecordingPause; + } + + public async override Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default) + { + if (dc.Context.Activity.ChannelId == Channels.Telephony) + { + return await base.BeginDialogAsync(dc, options, cancellationToken).ConfigureAwait(false); + } + + return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default) + { + // TODO: Carlos try to delete + if (dc.Context.Activity.ChannelId == Channels.Telephony) + { + return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false); + } + + return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/packages/Telephony/Actions/ResumeRecording.cs b/packages/Telephony/Actions/ResumeRecording.cs new file mode 100644 index 0000000000..c089f20ae3 --- /dev/null +++ b/packages/Telephony/Actions/ResumeRecording.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Components.Telephony.Common; +using Microsoft.Bot.Connector; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Components.Telephony.Actions +{ + /// + /// Resume recording the current conversation. + /// + public class ResumeRecording : CommandDialog + { + public const string RecordingResume = "channel/vnd.microsoft.telephony.recording.resume"; + + /// + /// Class identifier. + /// + [JsonProperty("$kind")] + public const string Kind = "Microsoft.Telephony.ResumeRecording"; + + /// + /// Initializes a new instance of the class. + /// + /// Optional, source file full path. + /// Optional, line number in source file. + [JsonConstructor] + public ResumeRecording([CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + : base() + { + // enable instances of this command as debug break point + this.RegisterSourceLocation(sourceFilePath, sourceLineNumber); + + this.CommandName = RecordingResume; + } + + public async override Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default) + { + if (dc.Context.Activity.ChannelId == Channels.Telephony) + { + return await base.BeginDialogAsync(dc, options, cancellationToken).ConfigureAwait(false); + } + + return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default) + { + // TODO: Carlos try to delete + if (dc.Context.Activity.ChannelId == Channels.Telephony) + { + return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false); + } + + return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/packages/Telephony/Actions/StartRecording.cs b/packages/Telephony/Actions/StartRecording.cs new file mode 100644 index 0000000000..82d2a5c3f9 --- /dev/null +++ b/packages/Telephony/Actions/StartRecording.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Components.Telephony.Common; +using Microsoft.Bot.Connector; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Components.Telephony.Actions +{ + /// + /// Starts recording the current conversation. + /// + public class StartRecording : CommandDialog + { + /// + /// Class identifier. + /// + [JsonProperty("$kind")] + public const string Kind = "Microsoft.Telephony.StartRecording"; + + private const string RecordingStart = "channel/vnd.microsoft.telephony.recording.start"; + + /// + /// Initializes a new instance of the class. + /// + /// Optional, source file full path. + /// Optional, line number in source file. + [JsonConstructor] + public StartRecording([CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + : base() + { + // enable instances of this command as debug break point + this.RegisterSourceLocation(sourceFilePath, sourceLineNumber); + + this.CommandName = RecordingStart; + } + + /// + public async override Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default) + { + if (dc.Context.Activity.ChannelId == Channels.Telephony) + { + return await base.BeginDialogAsync(dc, options, cancellationToken).ConfigureAwait(false); + } + + return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default) + { + // TODO: Carlos try to delete + if (dc.Context.Activity.ChannelId == Channels.Telephony) + { + return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false); + } + + return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/packages/Telephony/Common/CommandDialog.cs b/packages/Telephony/Common/CommandDialog.cs new file mode 100644 index 0000000000..6e39ed7905 --- /dev/null +++ b/packages/Telephony/Common/CommandDialog.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Bot.Components.Telephony.Common +{ + /// + /// Generic dialog to orchestrate issuing command activities and releasing control once a command result is received. + /// + public class CommandDialog : CommandDialog + { + } +} diff --git a/packages/Telephony/Common/CommandDialog{T}.cs b/packages/Telephony/Common/CommandDialog{T}.cs new file mode 100644 index 0000000000..d33dc48324 --- /dev/null +++ b/packages/Telephony/Common/CommandDialog{T}.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using AdaptiveExpressions.Properties; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Adaptive; +using Microsoft.Bot.Schema; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Components.Telephony.Common +{ + /// + /// Generic dialog to orchestrate issuing command activities and releasing control once a command result is received. + /// + /// + /// TODO: Command.Value.Data and CommandResult.Value.Data can be of two different types + /// Potentially need T1 and T2. + /// + /// Type of data stored in the . + public class CommandDialog : Dialog + { + /// + /// Gets or sets intteruption policy. + /// + /// + /// Bool or expression which evalutes to bool. + /// + [JsonProperty("allowInterruptions")] + public BoolExpression AllowInterruptions { get; set; } = true; + + /// + /// Gets or sets the name of the command. + /// + /// + /// . + /// + protected StringExpression CommandName { get; set; } + + /// + /// Gets or sets the data payload of the command. + /// + /// + /// . + /// + protected T Data { get; set; } + + public override async Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default) + { + // TODO: check name not null / expression has value + var startRecordingActivity = CreateCommandActivity(dc.Context, this.Data, this.CommandName.GetValue(dc.State)); + + var response = await dc.Context.SendActivityAsync(startRecordingActivity, cancellationToken).ConfigureAwait(false); + + // TODO: Save command id / activity id for correlation with the command result. + + return new DialogTurnResult(DialogTurnStatus.Waiting, startRecordingActivity.Name); + } + + public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default) + { + var activity = dc.Context.Activity; + + // We are expecting a command result with the same name as our current CommandName. + if (activity.Type == ActivityTypes.CommandResult + && activity.Name == this.CommandName.GetValue(dc.State)) + { + // TODO: correlate command id before handling it. + + var commandResult = TelephonyExtensions.GetCommandResultValue(activity); + + if (commandResult?.Error != null) + { + throw new ErrorResponseException($"{commandResult.Error.Code}: {commandResult.Error.Message}"); + } + + return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + + // This activity was not the command result we were expecting. Mark as waiting and end the turn. + return new DialogTurnResult(DialogTurnStatus.Waiting); + } + + /// + protected override async Task OnPreBubbleEventAsync(DialogContext dc, DialogEvent e, CancellationToken cancellationToken) + { + if (e.Name == DialogEvents.ActivityReceived && dc.Context.Activity.Type == ActivityTypes.Message) + { + // Ask parent to perform recognition + await dc.Parent.EmitEventAsync(AdaptiveEvents.RecognizeUtterance, value: dc.Context.Activity, bubble: false, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Should we allow interruptions + var canInterrupt = true; + if (this.AllowInterruptions != null) + { + var (allowInterruptions, error) = this.AllowInterruptions.TryGetValue(dc.State); + canInterrupt = error == null && allowInterruptions; + } + + // Stop bubbling if interruptions ar NOT allowed + return !canInterrupt; + } + + return false; + } + + private static Activity CreateCommandActivity(ITurnContext turnContext, T data, string name) + { + if (turnContext == null) + { + throw new ArgumentNullException(nameof(turnContext)); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + var commandActivity = new Activity(type: ActivityTypes.Command); + + commandActivity.Name = name; + + var commandValue = new CommandValue() + { + CommandId = Guid.NewGuid().ToString(), + Data = data, + }; + + commandActivity.Value = commandValue; + + commandActivity.From = turnContext.Activity.From; + + commandActivity.ReplyToId = turnContext.Activity.Id; + commandActivity.ServiceUrl = turnContext.Activity.ServiceUrl; + commandActivity.ChannelId = turnContext.Activity.ChannelId; + + return commandActivity; + } + } +} diff --git a/packages/Telephony/Common/TelephonyExtensions.cs b/packages/Telephony/Common/TelephonyExtensions.cs new file mode 100644 index 0000000000..e4c0967889 --- /dev/null +++ b/packages/Telephony/Common/TelephonyExtensions.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Schema; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Bot.Components.Telephony.Common +{ + public static class TelephonyExtensions + { + /// + /// Gets the from the activity. + /// + /// The command activity. + /// The underlying value type for the . + /// The from the activity. + public static CommandValue GetCommandValue(this ICommandActivity activity) + { + object value = activity?.Value; + + if (value == null) + { + return null; + } + else if (value is CommandValue commandValue) + { + return commandValue; + } + else + { + return ((JObject)value).ToObject>(); + } + } + + public static CommandResultValue GetCommandResultValue(this ICommandResultActivity activity) + { + return GetCommandResultValue(activity); + } + + /// + /// Gets the CommandResultValue from the commmand result activity. + /// + /// The command result activity. + /// The underlying value type for the . + /// Gets the from the activity. + public static CommandResultValue GetCommandResultValue(this ICommandResultActivity activity) + { + object value = activity?.Value; + + if (value == null) + { + return null; + } + else if (value is CommandResultValue commandResultValue) + { + return commandResultValue; + } + else + { + return ((JObject)value).ToObject>(); + } + } + } +} \ No newline at end of file diff --git a/packages/Telephony/Microsoft.Bot.Components.Telephony.csproj b/packages/Telephony/Microsoft.Bot.Components.Telephony.csproj new file mode 100644 index 0000000000..b407c8e75f --- /dev/null +++ b/packages/Telephony/Microsoft.Bot.Components.Telephony.csproj @@ -0,0 +1,54 @@ + + + + Library + netstandard2.0 + + + + Microsoft.Bot.Components.Telephony + 0.0.1 + This library implements .NET support for adaptive dialogs with Telephony. + This library implements .NET support for adaptive dialogs with Telephony. + https://github.com/Microsoft/botframework-components/tree/main/packages/Telephony + true + ..\..\build\35MSSharedLib1024.snk + true + content + msbot-component;msbot-action + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + + + + + + + + + + $(NoWarn),SA0001,SA1649 + + + diff --git a/packages/Telephony/Readme.md b/packages/Telephony/Readme.md new file mode 100644 index 0000000000..2f193a46a5 --- /dev/null +++ b/packages/Telephony/Readme.md @@ -0,0 +1,115 @@ +# Microsoft.Bot.Components.Telephony + +The Microsoft.Bot.Components.Telephon package contains pre-built actions for building bots with Telephony capabilities. Install the package using [Bot Framework Composer](https://docs.microsoft.com/composer) to add telephony specific actions to your bot. Here are the actions supported: + +- [Call Transfer](#Call-Transfer) +- [Call Recording](#Call-Recording) +- [DTMF Batching ](#DTMF-Batching) + +## **Call Transfer** +Like any other channel, Telephony channel allows you to transfer call to an agent over a phone number. Learn more at [Telephony Advanced Features - Call Transfer](https://github.com/microsoft/botframework-telephony/blob/main/TransferCallOut.md). + +#### Parameters +* PhoneNumber + +#### Usage +* Phone Number should not be empty and should be in the E.164 format. +* The call transfer action is only valid when called in a conversation on the Telephony channel. The action can be considered a No-op for all other channels. + +#### Dialog Flow +* Once the call transfer is completed, the bot is removed from the current conversation and control is transferred to the external phone number. +* The bot will not get any handoff status on success. +* Any actions specified after call transfer will not be executed. Treat it like a call end. + +#### Failures +* For all failure cases where the connection is not established, either due to Phone Number being empty, invalid, bogus or just connection failure, an asynchronous "handoff.status" event is sent with value "failed". More details [here](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-design-pattern-handoff-human?view=azure-bot-service-4.0). +* This can be handled either in code as per [this](https://github.com/microsoft/botframework-telephony/blob/main/TransferCallOut.md) or in Composer by adding a trigger -> Activities -> Event Received (Event Activity), with this condition, turn.activity.name == "handoff.status", following which @turn.activity.value can be used for handling the failure case. +* In the failure case, subsequent actions will be executed. + +## **Call Recording** +The call recording commands enable bots to request that calls are recorded by the phone provider. The bot can control when to start, stop, pause and resume the recording with these commands. For more information about the call recording capabilities, see [Telephony Advanced Features - Call Recording](https://github.com/microsoft/botframework-telephony/blob/main/CallRecording.md). + +The recording extensions included in the Telephony package provide custom actions to take care of sending each of the call recording commands and waiting for the corresponding command result. Bot developers can also choose if interruptions are allowed when waiting for the command result. + +### **Start Recording** +The Start Recording action starts recording of the conversation. + +#### Parameters +* AllowInterruptions [`true`,`false`] + +#### Usage +* If a recording is started for a conversation, another recording for the same conversation cannot be started. In such case, the Telephony Channel returns an error indicating that the _"Recording is already in progress"_. +* The start recording action is only valid when called in a conversation on the Telephony channel. + +#### Dialog Flow +* By default AllowInterruptions is set to `true` i.e. dialog continues while the recording is started in the background. +* To block the dialog when a recording is started, set AllowInterruptions to `false`. +* When a start recording result is received and indicates success, the action is considered complete. If the dialog was blocked, interrruptions will be unblocked once the result is received. + +#### Failures +* When a start recording result is received and indicates error, the dialog throws an `ErrorResponseException`. + +### **Pause Recording** +The Pause Recording action pauses recording of the conversation. This action is typically used when the current turn/set of turns deals with sensitive information and must not be recorded. + +#### Parameters +* AllowInterruptions [`true`,`false`] + +#### Usage +* If PauseRecording is called and there is no recording in progress, Telephony channel returns an error indicating that the _"Recording has not started"_. +* The pause recording action is only valid when called in a conversation on the Telephony channel. + +#### Dialog Flow +* By default AllowInterruptions is set to `true` i.e. dialog continues while the recording is being paused in the background. +* To block the dialog when recording is paused, set AllowInterruptions to `false`. +* When a pause recording result is received and indicates success, the action is considered complete. If the dialog was blocked, interrruptions will be unblocked once the result is received. + +#### Failures +* When a pause recording result is received and indicates error, the dialog throws an `ErrorResponseException`. + +### **Resume Recording** +The Resume Recording action resumes recording of the conversation. This action is used to resume a previouly paused recording. + +#### Parameters_ +* AllowInterruptions [`true`,`false`] + +#### Usage +* [Open] _Add error details_ +* The pause recording action is only valid when called in a conversation on the Telephony channel. + +#### Dialog Flow +* By default AllowInterruptions is set to `true` i.e. dialog continues while the recording is being resumed in the background. +* To block the dialog when recording is resumed, set AllowInterruptions to `false`. +* When a resume recording result is received and indicates success, the action is considered complete. If the dialog was blocked, interrruptions will be unblocked once the result is received. + +#### Failures +* When a resume recording result is received and indicates error, the dialog throws an `ErrorResponseException`. + +### **Stop Recording** +The Stop Recording action stops recording of the conversation. Note that it is not required to call StopRecording explicitly. The recording is always stopped when the bot/caller ends the conversation or if the call is transferred to an external phone number. + +#### Parameters +* AllowInterruptions [`true`,`false`] + +#### Usage +* If StopRecording is called and there is no recording in progress, Telephony channel returns an error indicating that the _"Recording has not started"_. +* If a recording for a single conversation is stopped and started again, the recordings appear as multiple recording sessions in the storage. We do not recommend using the pattern StartRecording-StopRecording-StartRecording-StopRecording since it creates multiple recording files for a single conversation. Instead, we recommend using StartRecording-PauseRecording-ResumeRecording-EndCall/StopRecording to create a single recording file for the converastion. +* The stop recording action is only valid when called in a conversation on the Telephony channel. + +#### Dialog Flow +* By default AllowInterruptions is set to `true` i.e. dialog continues while the recording is being stopped in the background. +* To block the dialog when recording is stopped, set AllowInterruptions to `false`. +* When a stop recording result is received and indicates success, the action is considered complete. If the dialog was blocked, interrruptions will be unblocked once the result is received. + +#### Failures +* When a stop recording result is received and indicates error, the dialog throws an `ErrorResponseException`. + +## **DTMF Batching** +_In progress_ + +## Learn more +Learn more about [creating bots with telephony capabilities](https://github.com/microsoft/botframework-telephony). + +## Feedback and issues +If you encounter any issues with this package, or would like to share any feedback please open an Issue in our [GitHub repository](https://github.com/microsoft/botframework-components/issues/new/choose). + diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.CallTransfer.schema b/packages/Telephony/Schemas/Microsoft.Telephony.CallTransfer.schema new file mode 100644 index 0000000000..4666c744e9 --- /dev/null +++ b/packages/Telephony/Schemas/Microsoft.Telephony.CallTransfer.schema @@ -0,0 +1,21 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema", + "$role": "implements(Microsoft.IDialog)", + "title": "Call Transfer", + "description": "Phone number to transfer the call to (in E.164 format such as +1425123456)", + "type": "object", + "required": [ + "phoneNumber" + ], + "additionalProperties": false, + "properties": { + "phoneNumber": { + "$ref": "schema:#/definitions/stringExpression", + "title": "Phone Number", + "description": "Phone number to transfer the call to.", + "examples": [ + "in E.164 format such as +1425123456" + ] + } + } +} diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.CallTransfer.uischema b/packages/Telephony/Schemas/Microsoft.Telephony.CallTransfer.uischema new file mode 100644 index 0000000000..2cc45aa58b --- /dev/null +++ b/packages/Telephony/Schemas/Microsoft.Telephony.CallTransfer.uischema @@ -0,0 +1,26 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/ui/v1.0/ui.schema", + "form": { + "label": "Call Transfer", + "subtitle": "Transfers call to the phone number specified (in E.164 format such as +1425123456)", + "order": [ + "phoneNumber", + "*" + ], + "properties": { + "phoneNumber": { + "intellisenseScopes": [ + "variable-scopes" + ] + } + } + }, + "menu": { + "label": "Call Transfer", + "submenu": [ "Telephony" ] + }, + "flow": { + "widget": "ActionCard", + "body": "=action.phoneNumber" + } +} \ No newline at end of file diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.PauseRecording.schema b/packages/Telephony/Schemas/Microsoft.Telephony.PauseRecording.schema new file mode 100644 index 0000000000..be93806974 --- /dev/null +++ b/packages/Telephony/Schemas/Microsoft.Telephony.PauseRecording.schema @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema", + "$role": "implements(Microsoft.IDialog)", + "title": "Recording Pause", + "description": "Pauses recording the current conversation.", + "type": "object", + "additionalProperties": false, + "properties": { + "allowInterruptions": { + "$ref": "schema:#/definitions/booleanExpression", + "title": "Allow Interruptions", + "description": "Allow the dialog to continue when recording pause operation is in progress" + } + } +} diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.PauseRecording.uischema b/packages/Telephony/Schemas/Microsoft.Telephony.PauseRecording.uischema new file mode 100644 index 0000000000..74a91821e7 --- /dev/null +++ b/packages/Telephony/Schemas/Microsoft.Telephony.PauseRecording.uischema @@ -0,0 +1,16 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/ui/v1.0/ui.schema", + "form": { + "label": "Recording Pause", + "subtitle": "Pauses recording the current conversation", + "order": [ + "*" + ], + "properties": { + } + }, + "menu": { + "label": "Recording Pause", + "submenu": [ "Telephony" ] + } +} \ No newline at end of file diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.ResumeRecording.schema b/packages/Telephony/Schemas/Microsoft.Telephony.ResumeRecording.schema new file mode 100644 index 0000000000..353070899f --- /dev/null +++ b/packages/Telephony/Schemas/Microsoft.Telephony.ResumeRecording.schema @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema", + "$role": "implements(Microsoft.IDialog)", + "title": "Recording Resume", + "description": "Resumes recording the current conversation.", + "type": "object", + "additionalProperties": false, + "properties": { + "allowInterruptions": { + "$ref": "schema:#/definitions/booleanExpression", + "title": "Allow Interruptions", + "description": "Allow the dialog to continue when recording resume operation is in progress" + } + } +} diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.ResumeRecording.uischema b/packages/Telephony/Schemas/Microsoft.Telephony.ResumeRecording.uischema new file mode 100644 index 0000000000..9560a7fd69 --- /dev/null +++ b/packages/Telephony/Schemas/Microsoft.Telephony.ResumeRecording.uischema @@ -0,0 +1,16 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/ui/v1.0/ui.schema", + "form": { + "label": "Recording Resume", + "subtitle": "Resumes recording the current conversation", + "order": [ + "*" + ], + "properties": { + } + }, + "menu": { + "label": "Recording Resume", + "submenu": [ "Telephony" ] + } +} \ No newline at end of file diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.StartRecording.schema b/packages/Telephony/Schemas/Microsoft.Telephony.StartRecording.schema new file mode 100644 index 0000000000..bc1f0d2af9 --- /dev/null +++ b/packages/Telephony/Schemas/Microsoft.Telephony.StartRecording.schema @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema", + "$role": "implements(Microsoft.IDialog)", + "title": "Recording Start", + "description": "Starts recording the current conversation.", + "type": "object", + "additionalProperties": false, + "properties": { + "allowInterruptions": { + "$ref": "schema:#/definitions/booleanExpression", + "title": "Allow Interruptions", + "description": "Allow the dialog to continue when recording start operation is in progress" + } + } +} diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.StartRecording.uischema b/packages/Telephony/Schemas/Microsoft.Telephony.StartRecording.uischema new file mode 100644 index 0000000000..2988a0932f --- /dev/null +++ b/packages/Telephony/Schemas/Microsoft.Telephony.StartRecording.uischema @@ -0,0 +1,16 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/ui/v1.0/ui.schema", + "form": { + "label": "Recording Start", + "subtitle": "Starts recording the current conversation", + "order": [ + "*" + ], + "properties": { + } + }, + "menu": { + "label": "Recording Start", + "submenu": [ "Telephony" ] + } +} \ No newline at end of file diff --git a/packages/Telephony/TelephonyBotComponent.cs b/packages/Telephony/TelephonyBotComponent.cs new file mode 100644 index 0000000000..107064775c --- /dev/null +++ b/packages/Telephony/TelephonyBotComponent.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Bot.Components.Telephony +{ + using Microsoft.Bot.Builder; + using Microsoft.Bot.Builder.Dialogs.Declarative; + using Microsoft.Bot.Components.Telephony.Actions; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Telephony actions registration. + /// + public class TelephonyBotComponent : BotComponent + { + /// + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + // Conditionals + services.AddSingleton(sp => new DeclarativeType(CallTransfer.Kind)); + services.AddSingleton(sp => new DeclarativeType(PauseRecording.Kind)); + services.AddSingleton(sp => new DeclarativeType(ResumeRecording.Kind)); + services.AddSingleton(sp => new DeclarativeType(StartRecording.Kind)); + } + } +} \ No newline at end of file diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/CallTransferTests.cs b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/CallTransferTests.cs new file mode 100644 index 0000000000..5893cadc09 --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/CallTransferTests.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Bot.Connector; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Microsoft.Bot.Components.Telephony.Tests +{ + public class CallTransferTests : IntegrationTestsBase + { + public CallTransferTests(ResourceExplorerFixture resourceExplorerFixture) : base(resourceExplorerFixture) + { + } + + [Fact] + public async Task CallTransfer_HappyPath() + { + await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer, adapterChannel: Channels.Telephony); + } + + [Fact] + public async Task CallTransfer_IgnoredInNonTelephonyChannel() + { + await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer, adapterChannel: Channels.Msteams); + } + } +} diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/CallTransferTests/CallTransfer_BaseScenario.test.dialog b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/CallTransferTests/CallTransfer_BaseScenario.test.dialog new file mode 100644 index 0000000000..3ff4ede604 --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/CallTransferTests/CallTransfer_BaseScenario.test.dialog @@ -0,0 +1,20 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.AdaptiveDialog", + "id": "CallTransfer_BaseScenario.test", + "triggers": [ + { + "$kind": "Microsoft.OnBeginDialog", + "actions": [ + { + "$kind": "Microsoft.Telephony.CallTransfer", + "phoneNumber": "+15554434432" + }, + { + "$kind": "Microsoft.SendActivity", + "activity": "Transfer Initiated!" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/CallTransferTests/CallTransfer_HappyPath.test.dialog b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/CallTransferTests/CallTransfer_HappyPath.test.dialog new file mode 100644 index 0000000000..f052fd85f0 --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/CallTransferTests/CallTransfer_HappyPath.test.dialog @@ -0,0 +1,23 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.Test.Script", + "dialog": "CallTransfer_BaseScenario.test", + "script": [ + { + "$kind": "Microsoft.Test.UserSays", + "text": "Hello I'm Calculon" + }, + { + "$kind": "Microsoft.Test.AssertReplyActivity", + "assertions": [ + "type == 'event'", + "value.TargetPhoneNumber == '+15554434432'" + ] + }, + // Due to this being a mock, after actual call transfer this will not execute + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Transfer Initiated!" + } + ] +} \ No newline at end of file diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/CallTransferTests/CallTransfer_IgnoredInNonTelephonyChannel.test.dialog b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/CallTransferTests/CallTransfer_IgnoredInNonTelephonyChannel.test.dialog new file mode 100644 index 0000000000..49e8b15658 --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/CallTransferTests/CallTransfer_IgnoredInNonTelephonyChannel.test.dialog @@ -0,0 +1,15 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.Test.Script", + "dialog": "CallTransfer_BaseScenario.test", + "script": [ + { + "$kind": "Microsoft.Test.UserSays", + "text": "Hello I'm Calculon" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Transfer Initiated!" + } + ] +} \ No newline at end of file diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/Recording_BaseScenario.test.dialog b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/Recording_BaseScenario.test.dialog new file mode 100644 index 0000000000..34595cf682 --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/Recording_BaseScenario.test.dialog @@ -0,0 +1,76 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.AdaptiveDialog", + "id": "Recording_BaseScenario.test", + "triggers": [ + { + "$kind": "Microsoft.OnBeginDialog", + "actions": [ + { + "$kind": "Microsoft.Telephony.StartRecording", + "allowInterruptions": "=coalesce(settings.allowInterruptions, true)" + }, + { + "$kind": "Microsoft.SendActivity", + "activity": "Started recording!" + }, + { + "$kind": "Microsoft.SendActivity", + "activity": "Enter your account number." + }, + { + "$kind": "Microsoft.Telephony.PauseRecording", + "allowInterruptions": "=coalesce(settings.allowInterruptions, false)" + }, + { + "$kind": "Microsoft.SendActivity", + "activity": "Paused recording!" + }, + { + "$kind": "Microsoft.EndTurn" + } + ] + }, + { + "$kind": "Microsoft.OnIntent", + "intent": "HelpIntent", + "actions": [ + { + "$kind": "Microsoft.SendActivity", + "activity": "On help intent handler" + } + ] + }, + { + "$kind": "Microsoft.OnIntent", + "intent": "AccountIntent", + "actions": [ + { + "$kind": "Microsoft.SendActivity", + "activity": "Your account is active." + }, + { + "$kind": "Microsoft.Telephony.ResumeRecording", + "allowInterruptions": "=coalesce(settings.allowInterruptions, false)" + }, + { + "$kind": "Microsoft.SendActivity", + "activity": "Resumed recording!" + } + ] + } + ], + "recognizer": { + "$kind": "Microsoft.RegexRecognizer", + "intents": [ + { + "intent": "HelpIntent", + "pattern": "help" + }, + { + "intent": "AccountIntent", + "pattern": "account" + } + ] + } +} \ No newline at end of file diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/Recording_HappyPath.test.dialog b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/Recording_HappyPath.test.dialog new file mode 100644 index 0000000000..22d1ff517f --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/Recording_HappyPath.test.dialog @@ -0,0 +1,77 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.Test.Script", + "dialog": "Recording_BaseScenario.test", + "script": [ + { + "$kind": "Microsoft.Test.UserSays", + "text": "Hello I'm Calculon" + }, + { + "$kind": "Microsoft.Test.AssertReplyActivity", + "assertions": [ + "type == 'command'", + "name == 'channel/vnd.microsoft.telephony.recording.start'" + ] + }, + { + "$kind": "Microsoft.Test.UserActivity", + "activity": { + "type": "commandResult", + "name": "channel/vnd.microsoft.telephony.recording.start" + } + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Started recording!" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Enter your account number." + }, + { + "$kind": "Microsoft.Test.AssertReplyActivity", + "assertions": [ + "type == 'command'", + "name == 'channel/vnd.microsoft.telephony.recording.pause'" + ] + }, + { + "$kind": "Microsoft.Test.UserActivity", + "activity": { + "type": "commandResult", + "name": "channel/vnd.microsoft.telephony.recording.pause" + } + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Paused recording!" + }, + { + "$kind": "Microsoft.Test.UserSays", + "text": "account" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Your account is active." + }, + { + "$kind": "Microsoft.Test.AssertReplyActivity", + "assertions": [ + "type == 'command'", + "name == 'channel/vnd.microsoft.telephony.recording.resume'" + ] + }, + { + "$kind": "Microsoft.Test.UserActivity", + "activity": { + "type": "commandResult", + "name": "channel/vnd.microsoft.telephony.recording.resume" + } + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Resumed recording!" + } + ] +} \ No newline at end of file diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/Recording_IgnoredInNonTelephonyChannel.test.dialog b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/Recording_IgnoredInNonTelephonyChannel.test.dialog new file mode 100644 index 0000000000..a3763c5907 --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/Recording_IgnoredInNonTelephonyChannel.test.dialog @@ -0,0 +1,35 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.Test.Script", + "dialog": "Recording_BaseScenario.test", + "script": [ + { + "$kind": "Microsoft.Test.UserSays", + "text": "Hello I'm Bender" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Started recording!" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Enter your account number." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Paused recording!" + }, + { + "$kind": "Microsoft.Test.UserSays", + "text": "account" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Your account is active." + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Resumed recording!" + } + ] +} \ No newline at end of file diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/StartRecording_CommandResultWrongName.test.dialog b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/StartRecording_CommandResultWrongName.test.dialog new file mode 100644 index 0000000000..1f5906f26d --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/StartRecording_CommandResultWrongName.test.dialog @@ -0,0 +1,28 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.Test.Script", + "dialog": "Recording_BaseScenario.test", + "script": [ + { + "$kind": "Microsoft.Test.UserSays", + "text": "Hello I'm Bender" + }, + { + "$kind": "Microsoft.Test.AssertReplyActivity", + "assertions": [ + "type == 'command'", + "name == 'channel/vnd.microsoft.telephony.recording.start'" + ] + }, + { + "$kind": "Microsoft.Test.UserActivity", + "activity": { + "type": "commandResult", + "name": "wrong name" + } + }, + { + "$kind": "Microsoft.Test.AssertNoActivity" + } + ] +} \ No newline at end of file diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/StartRecording_WithTangent_InterruptionDisabled.test.dialog b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/StartRecording_WithTangent_InterruptionDisabled.test.dialog new file mode 100644 index 0000000000..8b15a0dffc --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/StartRecording_WithTangent_InterruptionDisabled.test.dialog @@ -0,0 +1,33 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.Test.Script", + "dialog": "Recording_BaseScenario.test", + "script": [ + { + "$kind": "Microsoft.Test.UserSays", + "text": "Hello I'm Calculon" + }, + { + "$kind": "Microsoft.Test.AssertReplyActivity", + "assertions": [ + "type == 'command'", + "name == 'channel/vnd.microsoft.telephony.recording.start'" + ] + }, + { + "$kind": "Microsoft.Test.UserSays", + "text": "help" + }, + { + "$kind": "Microsoft.Test.UserActivity", + "activity": { + "type": "commandResult", + "name": "channel/vnd.microsoft.telephony.recording.start" + } + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Started recording!" + } + ] +} \ No newline at end of file diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/StartRecording_WithTangent_InterruptionEnabled.test.dialog b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/StartRecording_WithTangent_InterruptionEnabled.test.dialog new file mode 100644 index 0000000000..739e5f9dff --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Integration/RecordingTests/StartRecording_WithTangent_InterruptionEnabled.test.dialog @@ -0,0 +1,37 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.Test.Script", + "dialog": "Recording_BaseScenario.test", + "script": [ + { + "$kind": "Microsoft.Test.UserSays", + "text": "Hello I'm Calculon" + }, + { + "$kind": "Microsoft.Test.AssertReplyActivity", + "assertions": [ + "type == 'command'", + "name == 'channel/vnd.microsoft.telephony.recording.start'" + ] + }, + { + "$kind": "Microsoft.Test.UserSays", + "text": "help" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "On help intent handler" + }, + { + "$kind": "Microsoft.Test.UserActivity", + "activity": { + "type": "commandResult", + "name": "channel/vnd.microsoft.telephony.recording.start" + } + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "Started recording!" + } + ] +} \ No newline at end of file diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/IntegrationTestsBase.cs b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/IntegrationTestsBase.cs new file mode 100644 index 0000000000..9f1af39abe --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/IntegrationTestsBase.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Adaptive; +using Microsoft.Bot.Builder.Dialogs.Adaptive.Testing; +using Microsoft.Bot.Builder.Dialogs.Declarative; +using Microsoft.Bot.Builder.Dialogs.Declarative.Obsolete; +using Xunit; + +namespace Microsoft.Bot.Components.Telephony.Tests +{ + public class IntegrationTestsBase : IClassFixture + { + protected readonly ResourceExplorerFixture _resourceExplorerFixture; + + public IntegrationTestsBase(ResourceExplorerFixture resourceExplorerFixture) + { + ComponentRegistration.Add(new DeclarativeComponentRegistration()); + ComponentRegistration.Add(new DialogsComponentRegistration()); + ComponentRegistration.Add(new AdaptiveComponentRegistration()); + ComponentRegistration.Add(new LanguageGenerationComponentRegistration()); + ComponentRegistration.Add(new AdaptiveTestingComponentRegistration()); + ComponentRegistration.Add(new DeclarativeComponentRegistrationBridge()); + + _resourceExplorerFixture = resourceExplorerFixture.Initialize(this.GetType().Name); + } + } +} diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Microsoft.Bot.Components.Telephony.Tests.csproj b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Microsoft.Bot.Components.Telephony.Tests.csproj new file mode 100644 index 0000000000..99493d43ed --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/Microsoft.Bot.Components.Telephony.Tests.csproj @@ -0,0 +1,59 @@ + + + + netcoreapp2.1 + netcoreapp3.1 + netcoreapp2.1;netcoreapp3.1 + false + Debug;Release + latest + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/NuGet.Config b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/NuGet.Config new file mode 100644 index 0000000000..16e6dfff88 --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/RecordingTests.cs b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/RecordingTests.cs new file mode 100644 index 0000000000..ab3ebed612 --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/RecordingTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Bot.Connector; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Microsoft.Bot.Components.Telephony.Tests +{ + public class RecordingTests : IntegrationTestsBase + { + public RecordingTests(ResourceExplorerFixture resourceExplorerFixture) : base(resourceExplorerFixture) + { + } + + [Fact] + public async Task Recording_HappyPath() + { + await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer, adapterChannel: Channels.Telephony); + } + + [Fact] + public async Task StartRecording_WithTangent_InterruptionEnabled() + { + await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer, adapterChannel: Channels.Telephony); + } + + [Fact] + public async Task StartRecording_WithTangent_InterruptionDisabled() + { + await TestUtils.RunTestScript( + _resourceExplorerFixture.ResourceExplorer, + adapterChannel: Channels.Telephony, + configuration: new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary() { { "allowInterruptions", "false" } }) + .Build()); + } + + [Fact] + public async Task StartRecording_CommandResultWrongName() + { + await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer, adapterChannel: Channels.Telephony); + } + + [Fact] + public async Task Recording_IgnoredInNonTelephonyChannel() + { + await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer, adapterChannel: Channels.Msteams); + } + } +} diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/ResourceExplorerFixture.cs b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/ResourceExplorerFixture.cs new file mode 100644 index 0000000000..48a56815e3 --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/ResourceExplorerFixture.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using Microsoft.Bot.Builder.Dialogs.Declarative.Resources; + +namespace Microsoft.Bot.Components.Telephony.Tests +{ + public class ResourceExplorerFixture : IDisposable + { + private string _folderPath = string.Empty; + + public ResourceExplorerFixture() + { + ResourceExplorer = new ResourceExplorer(); + } + + public ResourceExplorer ResourceExplorer { get; private set; } + + public ResourceExplorerFixture Initialize(string resourceFolder) + { + if (_folderPath.Length == 0) + { + _folderPath = Path.Combine(TestUtils.GetProjectPath(), "Integration", resourceFolder); + ResourceExplorer = ResourceExplorer.AddFolder(_folderPath, monitorChanges: false); + } + + return this; + } + + public void Dispose() + { + _folderPath = string.Empty; + ResourceExplorer.Dispose(); + } + } +} diff --git a/tests/packages/Microsoft.Bot.Components.Telephony.Tests/TestUtils.cs b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/TestUtils.cs new file mode 100644 index 0000000000..e6d2b11a50 --- /dev/null +++ b/tests/packages/Microsoft.Bot.Components.Telephony.Tests/TestUtils.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Dialogs.Adaptive.Testing; +using Microsoft.Bot.Builder.Dialogs.Declarative.Resources; +using Microsoft.Bot.Connector; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Bot.Components.Telephony.Tests +{ + public class TestUtils + { + public static IConfiguration DefaultConfiguration { get; set; } = new ConfigurationBuilder().AddInMemoryCollection().Build(); + + public static string RootFolder { get; set; } = GetProjectPath(); + + public static IEnumerable GetTestScripts(string relativeFolder) + { + var testFolder = Path.GetFullPath(Path.Combine(RootFolder, PathUtils.NormalizePath(relativeFolder))); + return Directory.EnumerateFiles(testFolder, "*.test.dialog", SearchOption.AllDirectories).Select(s => new object[] { Path.GetFileName(s) }).ToArray(); + } + + public static async Task RunTestScript(ResourceExplorer resourceExplorer, string resourceId = null, IConfiguration configuration = null, [CallerMemberName] string testName = null, IEnumerable middleware = null, string adapterChannel = Channels.Msteams) + { + var storage = new MemoryStorage(); + var convoState = new ConversationState(storage); + var userState = new UserState(storage); + + var adapter = (TestAdapter)new TestAdapter(CreateConversation(adapterChannel, testName)); + + if (middleware != null) + { + foreach (var m in middleware) + { + adapter.Use(m); + } + } + + adapter.Use(new RegisterClassMiddleware(configuration ?? DefaultConfiguration)) + .UseStorage(storage) + .UseBotState(userState, convoState) + .Use(new TranscriptLoggerMiddleware(new TraceTranscriptLogger(traceActivity: false))); + + adapter.OnTurnError += async (context, err) => + { + if (err.Message.EndsWith("MemoryAssertion failed")) + { + throw err; + } + + await context.SendActivityAsync(err.Message); + }; + + var script = resourceExplorer.LoadType(resourceId ?? $"{testName}.test.dialog"); + script.Configuration = configuration ?? new ConfigurationBuilder().AddInMemoryCollection().Build(); + script.Description ??= resourceId; + await script.ExecuteAsync(adapter: adapter, testName: testName, resourceExplorer: resourceExplorer).ConfigureAwait(false); + } + + public static string GetProjectPath() + { + var parent = Environment.CurrentDirectory; + while (!string.IsNullOrEmpty(parent)) + { + if (Directory.EnumerateFiles(parent, "*proj").Any()) + { + break; + } + + parent = Path.GetDirectoryName(parent); + } + + return parent; + } + + public static ConversationReference CreateConversation(string channel, string conversationName) + { + return new ConversationReference + { + ChannelId = channel ?? Channels.Test, + ServiceUrl = "https://test.com", + User = new ChannelAccount("user1", "User1"), + Bot = new ChannelAccount("bot", "Bot"), + Conversation = new ConversationAccount(false, "personal", conversationName), + Locale = "en-US", + }; + } + } +} diff --git a/tests/unit/Microsoft.Bot.Components.Tests.sln b/tests/unit/Microsoft.Bot.Components.Tests.sln index 733f8655f9..e5dae090ef 100644 --- a/tests/unit/Microsoft.Bot.Components.Tests.sln +++ b/tests/unit/Microsoft.Bot.Components.Tests.sln @@ -21,6 +21,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Components.Gr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Components.Teams.Tests", "packages\Teams\dotnet\Microsoft.Bot.Components.Teams.Tests.csproj", "{4005C95D-BECC-450B-927D-54C8CCF178BB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "telephony", "telephony", "{47F3E043-D5E7-42E3-8B18-DF64A9F2FC1C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Components.Telephony.Tests", "..\packages\Microsoft.Bot.Components.Telephony.Tests\Microsoft.Bot.Components.Telephony.Tests.csproj", "{485E9D0F-6534-475D-8400-31BFF633EDE0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Components.Telephony", "..\..\packages\Telephony\Microsoft.Bot.Components.Telephony.csproj", "{BDA3A62A-8762-44EA-A417-5DC01D26AAF9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +57,14 @@ Global {4005C95D-BECC-450B-927D-54C8CCF178BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {4005C95D-BECC-450B-927D-54C8CCF178BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {4005C95D-BECC-450B-927D-54C8CCF178BB}.Release|Any CPU.Build.0 = Release|Any CPU + {485E9D0F-6534-475D-8400-31BFF633EDE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {485E9D0F-6534-475D-8400-31BFF633EDE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {485E9D0F-6534-475D-8400-31BFF633EDE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {485E9D0F-6534-475D-8400-31BFF633EDE0}.Release|Any CPU.Build.0 = Release|Any CPU + {BDA3A62A-8762-44EA-A417-5DC01D26AAF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BDA3A62A-8762-44EA-A417-5DC01D26AAF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BDA3A62A-8762-44EA-A417-5DC01D26AAF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BDA3A62A-8762-44EA-A417-5DC01D26AAF9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -61,6 +75,9 @@ Global {F0E73465-DB1D-4F8A-B527-88F1E2515A5C} = {31CBAEDA-E319-49AE-A662-AA0739205C82} {61708441-52A4-48F6-819A-B8D4C279C4C8} = {73825711-6685-48E2-BFA2-3FCDECE1A0FD} {4005C95D-BECC-450B-927D-54C8CCF178BB} = {61708441-52A4-48F6-819A-B8D4C279C4C8} + {47F3E043-D5E7-42E3-8B18-DF64A9F2FC1C} = {73825711-6685-48E2-BFA2-3FCDECE1A0FD} + {485E9D0F-6534-475D-8400-31BFF633EDE0} = {47F3E043-D5E7-42E3-8B18-DF64A9F2FC1C} + {BDA3A62A-8762-44EA-A417-5DC01D26AAF9} = {47F3E043-D5E7-42E3-8B18-DF64A9F2FC1C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2D0FB02B-704A-44E0-AECA-A4FDF6F805C5}