diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/BridgeCalls.cs b/src/activities/Elsa.Activities.Telnyx/Activities/BridgeCalls.cs index 640f0bb15..fe77f0df3 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/BridgeCalls.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/BridgeCalls.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Elsa.Activities.Telnyx.Client.Models; using Elsa.Activities.Telnyx.Client.Services; using Elsa.Activities.Telnyx.Extensions; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; using Elsa.ActivityResults; using Elsa.Attributes; using Elsa.Builders; @@ -18,7 +21,7 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Bridge two call control calls.", - Outcomes = new[] { OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive }, + Outcomes = new[] {TelnyxOutcomeNames.Bridging, TelnyxOutcomeNames.Bridged, TelnyxOutcomeNames.LegABridged, TelnyxOutcomeNames.LegBBridged, OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive}, DisplayName = "Bridge Calls" )] public class BridgeCalls : Activity @@ -30,40 +33,52 @@ namespace Elsa.Activities.Telnyx.Activities _telnyxClient = telnyxClient; } - [ActivityInput(Label = "Call Control ID A", Hint = "Unique identifier and token for controlling the call.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] - public string? CallControlIdA { get; set; } = default!; + [ActivityInput(Label = "Call Control ID A", Hint = "Unique identifier and token for controlling the call.", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] + public string? CallControlIdA { get; set; } - [ActivityInput(Label = "Call Control ID B", Hint = "The Call Control ID of the call you want to bridge with.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] - public string CallControlIdB { get; set; } = default!; + [ActivityInput(Label = "Call Control ID B", Hint = "The Call Control ID of the call you want to bridge with.", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] + public string? CallControlIdB { get; set; } [ActivityInput( Label = "Command ID", Hint = "Use this field to avoid duplicate commands. Telnyx will ignore commands with the same Command ID.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? CommandId { get; set; } [ActivityInput( - Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", + Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? ClientState { get; set; } [ActivityInput( - Label = "Park After Unbridged", - Hint = "HTTP request type used for Webhook URL", - UIHint = ActivityInputUIHints.Dropdown, - Options = new[] { "", "self" }, - Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })] + Label = "Park After Unbridged", + Hint = "HTTP request type used for Webhook URL", + UIHint = ActivityInputUIHints.Dropdown, + Options = new[] {"", "self"}, + Category = PropertyCategories.Advanced, + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? ParkAfterUnbridged { get; set; } + [ActivityOutput] public CallBridgedPayload? CallBridgedPayloadA { get; set; } + [ActivityOutput] public CallBridgedPayload? CallBridgedPayloadB { get; set; } + protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) { - var callControlIdA = context.GetCallControlId(CallControlIdA); + CallBridgedPayloadA = null; + CallBridgedPayloadB = null; + var callControlIdA = CallControlIdA = context.GetCallControlId(CallControlIdA); + var callControlIdB = CallControlIdB = GetCallControlB(context); + + if (callControlIdB == null) + throw new WorkflowException("Cannot bridge calls because the second leg's call control ID was not specified and no incoming activities provided this value"); + + CallControlIdA = callControlIdA; + var request = new BridgeCallsRequest( - CallControlIdB, + callControlIdB, ClientState, CommandId, ParkAfterUnbridged @@ -72,7 +87,7 @@ namespace Elsa.Activities.Telnyx.Activities try { await _telnyxClient.Calls.BridgeCallsAsync(callControlIdA, request, context.CancellationToken); - return Done(); + return Combine(Outcome(TelnyxOutcomeNames.Bridging), Suspend()); } catch (ApiException e) { @@ -82,6 +97,50 @@ namespace Elsa.Activities.Telnyx.Activities throw new WorkflowException(e.Content ?? e.Message, e); } } + + protected override IActivityExecutionResult OnResume(ActivityExecutionContext context) + { + var payload = context.GetInput()!; + var results = new List(); + + if (payload.CallControlId == CallControlIdA) + { + CallBridgedPayloadA = payload; + results.Add(Outcome(TelnyxOutcomeNames.LegABridged, payload)); + } + + if (payload.CallControlId == CallControlIdB) + { + CallBridgedPayloadB = payload; + results.Add(Outcome(TelnyxOutcomeNames.LegBBridged, payload)); + } + + if (CallBridgedPayloadA != null && CallBridgedPayloadB != null) + { + results.Add(Outcome(TelnyxOutcomeNames.Bridged)); + } + else + { + results.Add(Suspend()); + } + + return Combine(results); + } + + private string? GetCallControlB(ActivityExecutionContext context) + { + if (!string.IsNullOrWhiteSpace(CallControlIdB)) + return CallControlIdB; + + var input = context.GetInput(); + + if (input != null) + return input.CallControlId; + + var inboundCallActivityId = context.WorkflowExecutionContext.GetInboundConnections(Id).Where(x => x.Source.Activity.Type == nameof(Dial)).Select(x => x.Source.Activity.Id).FirstOrDefault(); + var inboundCallActivityResponse = inboundCallActivityId != null ? context.WorkflowExecutionContext.GetActivityProperty(inboundCallActivityId, x => x.DialResponse) : default; + return inboundCallActivityResponse != null ? inboundCallActivityResponse.CallControlId : null; + } } public static class BridgeCallsExtensions diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs b/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs index 71722395c..5f16e36ee 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs @@ -26,7 +26,7 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Call a ring group.", - Outcomes = new[] { "Connected", "No Response" }, + Outcomes = new[] {TelnyxOutcomeNames.Connected, TelnyxOutcomeNames.NoResponse}, DisplayName = "Call Ring Group" )] public class CallRingGroup : CompositeActivity, IActivityPropertyDefaultValueProvider @@ -38,14 +38,14 @@ namespace Elsa.Activities.Telnyx.Activities _logger = logger; } - [ActivityInput(UIHint = ActivityInputUIHints.MultiText, DefaultSyntax = SyntaxNames.Json, SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(UIHint = ActivityInputUIHints.MultiText, DefaultSyntax = SyntaxNames.Json, SupportedSyntaxes = new[] {SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid})] public IList Extensions { get => GetState>(() => new List()); set => SetState(value); } - [ActivityInput(Label = "Call Control ID", Hint = "Unique identifier and token for controlling the call.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(Label = "Call Control ID", Hint = "Unique identifier and token for controlling the call.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string CallControlId { get => GetState()!; @@ -56,7 +56,7 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Call Control App ID", Hint = "The ID of the Call Control App (formerly ID of the connection) to be used when dialing the destination.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? CallControlAppId { @@ -64,7 +64,7 @@ namespace Elsa.Activities.Telnyx.Activities set => SetState(value); } - [ActivityInput(SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})] public RingGroupStrategy Strategy { get => GetState(); @@ -73,7 +73,7 @@ namespace Elsa.Activities.Telnyx.Activities [ActivityInput( Hint = "The 'from' number to be used as the caller id presented to the destination ('To' number). The number should be in +E164 format. This attribute will default to the 'From' number of the original call if omitted.", - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? From { @@ -84,7 +84,7 @@ namespace Elsa.Activities.Telnyx.Activities [ActivityInput( Hint = "The string to be used as the caller id name (SIP From Display Name) presented to the destination ('To' number). The string should have a maximum of 128 characters, containing only letters, numbers, spaces, and -_~!.+ special characters. If omitted, the display name will be the same as the number in the 'From' field.", - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? FromDisplayName { @@ -92,19 +92,13 @@ namespace Elsa.Activities.Telnyx.Activities set => SetState(value); } - [ActivityInput(DefaultValueProvider = typeof(CallRingGroup), SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(DefaultValueProvider = typeof(CallRingGroup), SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public Duration RingTime { get => GetState(() => Duration.FromSeconds(20)); set => SetState(value); } - private string? DialedControlId - { - get => GetState(); - set => SetState(value); - } - private CallAnsweredPayload? CallAnsweredPayload { get => GetState(); @@ -148,64 +142,53 @@ namespace Elsa.Activities.Telnyx.Activities private void BuildPrioritizedHuntFlow(IOutcomeBuilder builder) => builder .ForEach(() => Extensions, iterate => iterate - .Then(a => a - .WithConnectionId(() => CallControlAppId) - .WithTo(async context => await ResolveExtensionAsync(context, context.GetInput()!)) - .WithTimeoutSecs(() => (int) RingTime.TotalSeconds) - .WithFrom(() => From) - .WithFromDisplayName(() => FromDisplayName) - .WithClientState(context => new ClientStatePayload(context.CorrelationId!).ToBase64()) + .Then(dial => dial + .WithConnectionId(() => CallControlAppId) + .WithTo(ResolveExtensionAsync) + .WithTimeoutSecs(() => (int) RingTime.TotalSeconds) + .WithFrom(() => From) + .WithFromDisplayName(() => FromDisplayName), + dial => + { + dial + .When(TelnyxOutcomeNames.Answered) + .Then(bridgeCalls => + bridgeCalls.When(TelnyxOutcomeNames.Bridged) + .Finish(TelnyxOutcomeNames.Connected)); + } ) - .Then(context => DialedControlId = context.GetInput()!.CallControlId) - .Then(fork => fork.WithBranches("Connected", "No Response"), fork => - { - fork - .When("Connected") - .ThenTypeNamed(CallAnsweredPayload.ActivityTypeName) - .Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput()!.Data.Payload) - .Then(bridge => bridge - .WithCallControlIdA(() => CallControlId) - .WithCallControlIdB(() => DialedControlId)) - .ThenTypeNamed(CallBridgedPayload.ActivityTypeName) - .ThenTypeNamed(CallBridgedPayload.ActivityTypeName) - .Then(finish => finish.WithOutcome("Connected").WithOutput(() => CallAnsweredPayload)); - - fork - .When("No Response") - .ThenTypeNamed(CallHangupPayload.ActivityTypeName); - }) ) - .Finish("No Response"); + .Finish(TelnyxOutcomeNames.NoResponse); private void BuildRingAllFlow(IOutcomeBuilder builder) => builder .Then(fork => fork.WithBranches("Connected", "Timeout", "Dial Everyone"), fork => { fork - .When("Connected") + .When(TelnyxOutcomeNames.Connected) .ThenTypeNamed(CallAnsweredPayload.ActivityTypeName) .Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput()!.Data.Payload) .Then(bridge => bridge .WithCallControlIdA(() => CallControlId) - .WithCallControlIdB(() => CallAnsweredPayload!.CallControlId)) - .ThenTypeNamed(CallBridgedPayload.ActivityTypeName) - .Then(finish => finish.WithOutcome("Connected").WithOutput(() => CallAnsweredPayload)); + .WithCallControlIdB(() => CallAnsweredPayload!.CallControlId), bridge => bridge + .When(TelnyxOutcomeNames.Bridged) + .Finish(TelnyxOutcomeNames.Connected)); fork .When("Timeout") .StartIn(() => RingTime) - .Finish("No Response"); + .Finish(TelnyxOutcomeNames.NoResponse); fork .When("Dial Everyone") .ParallelForEach(() => Extensions, iterate => iterate .Then(a => a + .WithSuspendWorkflow(false) .WithConnectionId(() => CallControlAppId) - .WithTo(async context => await ResolveExtensionAsync(context, context.GetInput()!)) + .WithTo(ResolveExtensionAsync) .WithTimeoutSecs(() => (int) RingTime.TotalSeconds) .WithFrom(() => From) .WithFromDisplayName(() => FromDisplayName) - .WithClientState(context => new ClientStatePayload(context.CorrelationId!).ToBase64()) ) .Then(CollectCallControlIds)); }); @@ -217,9 +200,13 @@ namespace Elsa.Activities.Telnyx.Activities collection.Add(dialResponse); CollectedDialResponses = collection; } - - private static async Task ResolveExtensionAsync(ActivityExecutionContext context, string extension) + + private static async ValueTask ResolveExtensionAsync(ActivityExecutionContext context) { + if (context.Resuming) + return context.GetActivityProperty(x => x.To)!; + + var extension = context.GetInput()!; var extensionProvider = context.GetService(); var resolvedExtension = await extensionProvider.GetAsync(extension, context.CancellationToken); return resolvedExtension?.Number ?? extension; diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs b/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs index 337d792f8..a330d384a 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs @@ -5,7 +5,9 @@ using Elsa.Activities.Telnyx.Client.Models; using Elsa.Activities.Telnyx.Client.Services; using Elsa.Activities.Telnyx.Exceptions; using Elsa.Activities.Telnyx.Extensions; +using Elsa.Activities.Telnyx.Models; using Elsa.Activities.Telnyx.Options; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; using Elsa.ActivityResults; using Elsa.Attributes; using Elsa.Builders; @@ -21,7 +23,7 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Dial a number or SIP URI from a given connection.", - Outcomes = new[] { OutcomeNames.Done }, + Outcomes = new[] { TelnyxOutcomeNames.Dialing, TelnyxOutcomeNames.CallInitiated, TelnyxOutcomeNames.Answered, TelnyxOutcomeNames.Hangup, OutcomeNames.Done }, DisplayName = "Dial" )] public class Dial : Activity @@ -76,12 +78,6 @@ namespace Elsa.Activities.Telnyx.Activities SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] public string? CommandId { get; set; } - [ActivityInput( - Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", - Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] - public string? ClientState { get; set; } - [ActivityInput(Label = "Custom Headers", Hint = "Custom headers to be added to the SIP INVITE.", Category = PropertyCategories.Advanced, UIHint = ActivityInputUIHints.Json)] public IList
? CustomHeaders { get; set; } @@ -116,10 +112,36 @@ namespace Elsa.Activities.Telnyx.Activities SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })] public string? WebhookUrlMethod { get; set; } + [ActivityInput( + Hint = "A flag indicating whether this activity should complete immediately or suspend the workflow.", + Category = PropertyCategories.Advanced, + DefaultValue = true, + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})] + public bool SuspendWorkflow { get; set; } = true; + + [ActivityOutput] public DialResponse DialResponse { get; set; } + protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) { var response = await DialAsync(context); - return Done(response); + DialResponse = response; + + return !SuspendWorkflow + ? Done(response) + : Combine(Outcome(TelnyxOutcomeNames.Dialing, response), Suspend()); + } + + protected override IActivityExecutionResult OnResume(ActivityExecutionContext context) + { + var payload = context.GetInput(); + + return payload switch + { + CallAnsweredPayload callAnsweredPayload => Outcome(TelnyxOutcomeNames.Answered, callAnsweredPayload), + CallHangupPayload callHangupPayload => Outcome(TelnyxOutcomeNames.Hangup, callHangupPayload), + CallInitiatedPayload callInitiatedPayload => Combine(Outcome(TelnyxOutcomeNames.CallInitiated, callInitiatedPayload), Suspend()), + _ => throw new ArgumentOutOfRangeException(nameof(payload)) + }; } private async Task DialAsync(ActivityExecutionContext context) @@ -131,6 +153,8 @@ namespace Elsa.Activities.Telnyx.Activities var fromNumber = context.GetFromNumber(From); + var clientState = new ClientStatePayload(context.CorrelationId!).ToBase64(); + var request = new DialRequest( connectionId, To, @@ -138,7 +162,7 @@ namespace Elsa.Activities.Telnyx.Activities FromDisplayName, AnsweringMachineDetection, AnsweringMachineDetectionConfig, - ClientState, + clientState, CommandId, CustomHeaders, SipAuthUsername, @@ -207,12 +231,6 @@ namespace Elsa.Activities.Telnyx.Activities public static ISetupActivity WithCommandId(this ISetupActivity setup, Func value) => setup.Set(x => x.CommandId, value); public static ISetupActivity WithCommandId(this ISetupActivity setup, string? value) => setup.Set(x => x.CommandId, value); - public static ISetupActivity WithClientState(this ISetupActivity setup, Func> value) => setup.Set(x => x.ClientState, value); - public static ISetupActivity WithClientState(this ISetupActivity setup, Func value) => setup.Set(x => x.ClientState, value); - public static ISetupActivity WithClientState(this ISetupActivity setup, Func> value) => setup.Set(x => x.ClientState, value); - public static ISetupActivity WithClientState(this ISetupActivity setup, Func value) => setup.Set(x => x.ClientState, value); - public static ISetupActivity WithClientState(this ISetupActivity setup, string? value) => setup.Set(x => x.ClientState, value); - public static ISetupActivity WithCustomHeaders(this ISetupActivity setup, Func?>> value) => setup.Set(x => x.CustomHeaders, value); public static ISetupActivity WithCustomHeaders(this ISetupActivity setup, Func?> value) => setup.Set(x => x.CustomHeaders, value); public static ISetupActivity WithCustomHeaders(this ISetupActivity setup, Func?>> value) => setup.Set(x => x.CustomHeaders, value); @@ -254,5 +272,11 @@ namespace Elsa.Activities.Telnyx.Activities public static ISetupActivity WithWebhookUrlMethod(this ISetupActivity setup, Func> value) => setup.Set(x => x.WebhookUrlMethod, value); public static ISetupActivity WithWebhookUrlMethod(this ISetupActivity setup, Func value) => setup.Set(x => x.WebhookUrlMethod, value); public static ISetupActivity WithWebhookUrlMethod(this ISetupActivity setup, string? value) => setup.Set(x => x.WebhookUrlMethod, value); + + public static ISetupActivity WithSuspendWorkflow(this ISetupActivity setup, Func> value) => setup.Set(x => x.SuspendWorkflow, value); + public static ISetupActivity WithSuspendWorkflow(this ISetupActivity setup, Func value) => setup.Set(x => x.SuspendWorkflow, value); + public static ISetupActivity WithSuspendWorkflow(this ISetupActivity setup, Func> value) => setup.Set(x => x.SuspendWorkflow, value); + public static ISetupActivity WithSuspendWorkflow(this ISetupActivity setup, Func value) => setup.Set(x => x.SuspendWorkflow, value); + public static ISetupActivity WithSuspendWorkflow(this ISetupActivity setup, bool value) => setup.Set(x => x.SuspendWorkflow, value); } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs index 04b5f119e..a0168ca9e 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs @@ -18,7 +18,7 @@ namespace Elsa.Activities.Telnyx.Activities [Job( Category = Constants.Category, Description = "Play an audio file on the call until the required DTMF signals are gathered to build interactive menus.", - Outcomes = new[] { TelnyxOutcomeNames.Pending, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive }, + Outcomes = new[] { TelnyxOutcomeNames.GatheringInput, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive }, DisplayName = "Gather Using Audio" )] public class GatherUsingAudio : Activity @@ -121,7 +121,7 @@ namespace Elsa.Activities.Telnyx.Activities try { await _telnyxClient.Calls.GatherUsingAudioAsync(callControlId, request, context.CancellationToken); - return Combine(Outcome(TelnyxOutcomeNames.Pending), Suspend()); + return Combine(Outcome(TelnyxOutcomeNames.GatheringInput), Suspend()); } catch (ApiException e) { diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingSpeak.cs b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingSpeak.cs index f5b6f2f6f..132011faa 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingSpeak.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingSpeak.cs @@ -21,7 +21,7 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Convert text to speech and play it on the call until the required DTMF signals are gathered to build interactive menus.", - Outcomes = new[] { TelnyxOutcomeNames.Pending, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive }, + Outcomes = new[] { TelnyxOutcomeNames.GatheringInput, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive }, DisplayName = "Gather Using Speak" )] public class GatherUsingSpeak : Activity @@ -151,7 +151,7 @@ namespace Elsa.Activities.Telnyx.Activities try { await _telnyxClient.Calls.GatherUsingSpeakAsync(callControlId, request, context.CancellationToken); - return Combine(Outcome(TelnyxOutcomeNames.Pending), Suspend()); + return Combine(Outcome(TelnyxOutcomeNames.GatheringInput), Suspend()); } catch (ApiException e) { diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs b/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs index ea8939b7f..4485ceacb 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs @@ -109,7 +109,7 @@ namespace Elsa.Activities.Telnyx.Activities try { await _telnyxClient.Calls.SpeakTextAsync(callControlId, request, context.CancellationToken); - return Combine(Outcome(TelnyxOutcomeNames.Pending), Suspend()); + return Combine(Outcome(TelnyxOutcomeNames.Speaking), Suspend()); } catch (ApiException e) { diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs b/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs index d3694b1f5..6caed1a78 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Elsa.Activities.Telnyx.Client.Models; using Elsa.Activities.Telnyx.Client.Services; using Elsa.Activities.Telnyx.Extensions; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; using Elsa.ActivityResults; using Elsa.Attributes; using Elsa.Builders; @@ -19,7 +20,14 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Transfer a call to a new destination", - Outcomes = new[] { OutcomeNames.Done }, + Outcomes = new[] + { + TelnyxOutcomeNames.Transferring, + TelnyxOutcomeNames.CallInitiated, + TelnyxOutcomeNames.Bridged, + TelnyxOutcomeNames.Answered, + TelnyxOutcomeNames.Hangup + }, DisplayName = "Transfer Call" )] public class TransferCall : Activity @@ -35,23 +43,23 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Call Control ID", Hint = "Unique identifier and token for controlling the call.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? CallControlId { get; set; } = default!; - [ActivityInput(Label = "To", Hint = "The DID or SIP URI to dial out and bridge to the given call.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(Label = "To", Hint = "The DID or SIP URI to dial out and bridge to the given call.", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string To { get; set; } = default!; [ActivityInput( Hint = "The 'from' number to be used as the caller id presented to the destination ('To' number). The number should be in +E164 format. This attribute will default to the 'From' number of the original call if omitted.", - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? From { get; set; } [ActivityInput( Hint = "The string to be used as the caller id name (SIP From Display Name) presented to the destination ('To' number). The string should have a maximum of 128 characters, containing only letters, numbers, spaces, and -_~!.+ special characters. If omitted, the display name will be the same as the number in the 'From' field.", - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? FromDisplayName { get; set; } @@ -59,8 +67,8 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Answering Machine Detection", Hint = "Enables Answering Machine Detection.", UIHint = ActivityInputUIHints.Dropdown, - Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end" }, - SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid } + Options = new[] {"disabled", "detect", "detect_beep", "detect_words", "greeting_end"}, + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? AnsweringMachineDetection { get; set; } @@ -76,46 +84,46 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Command ID", Hint = "Use this field to avoid duplicate commands. Telnyx will ignore commands with the same Command ID.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? CommandId { get; set; } [ActivityInput( Label = "Audio URL", Hint = "Audio URL to be played back when the transfer destination answers before bridging the call. The URL can point to either a WAV or MP3 file.", - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public Uri? AudioUrl { get; set; } [ActivityInput( Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? ClientState { get; set; } [ActivityInput( Hint = "Use this field to add state to every subsequent webhook for the new leg. It must be a valid Base-64 encoded string.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? TargetLegClientState { get; set; } [ActivityInput(Hint = "Custom headers to be added to the SIP INVITE.", Category = PropertyCategories.Advanced, UIHint = ActivityInputUIHints.Json)] public IList
? CustomHeaders { get; set; } - [ActivityInput(Label = "SIP Authentication Username", Hint = "SIP Authentication username used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(Label = "SIP Authentication Username", Hint = "SIP Authentication username used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? SipAuthUsername { get; set; } - [ActivityInput(Label = "SIP Authentication Password", Hint = "SIP Authentication password used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(Label = "SIP Authentication Password", Hint = "SIP Authentication password used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? SipAuthPassword { get; set; } - [ActivityInput(Label = "Time Limit", Hint = "Sets the maximum duration of a Call Control Leg in seconds.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(Label = "Time Limit", Hint = "Sets the maximum duration of a Call Control Leg in seconds.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public int? TimeLimitSecs { get; set; } [ActivityInput( Label = "Timeout", Hint = "The number of seconds that Telnyx will wait for the call to be answered by the destination to which it is being transferred.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public int? TimeoutSecs { get; set; } @@ -123,7 +131,7 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Webhook URL", Hint = "Use this field to override the URL for which Telnyx will send subsequent webhooks to for this call.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? WebhookUrl { get; set; } @@ -131,22 +139,36 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Webhook URL Method", Hint = "HTTP request type used for Webhook URL", UIHint = ActivityInputUIHints.Dropdown, - Options = new[] { "GET", "POST" }, + Options = new[] {"GET", "POST"}, Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? WebhookUrlMethod { get; set; } protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) { await TransferCallAsync(context); - return Done(); + return Combine(Outcome(TelnyxOutcomeNames.Transferring), Suspend()); + } + + protected override IActivityExecutionResult OnResume(ActivityExecutionContext context) + { + var payload = context.GetInput(); + + return payload switch + { + CallAnsweredPayload callAnsweredPayload => Outcome(TelnyxOutcomeNames.Answered, callAnsweredPayload), + CallBridgedPayload callBridgedPayload => Outcome(TelnyxOutcomeNames.Bridged, callBridgedPayload), + CallHangupPayload callHangupPayload => Outcome(TelnyxOutcomeNames.Hangup, callHangupPayload), + CallInitiatedPayload callInitiatedPayload => Outcome(TelnyxOutcomeNames.CallInitiated, callInitiatedPayload), + _ => throw new ArgumentOutOfRangeException(nameof(payload)) + }; } private async ValueTask TransferCallAsync(ActivityExecutionContext context) { var fromNumber = context.GetFromNumber(From); - + var request = new TransferCallRequest( To, fromNumber, diff --git a/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeBridgeCalls.cs b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeBridgeCalls.cs new file mode 100644 index 000000000..80c93d28e --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeBridgeCalls.cs @@ -0,0 +1,13 @@ +using Elsa.Activities.Telnyx.Activities; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; +using Elsa.Services; + +namespace Elsa.Activities.Telnyx.Handlers +{ + public class ResumeBridgeCalls : ResumeWebhookDrivenActivity + { + public ResumeBridgeCalls(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad) + { + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeDial.cs b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeDial.cs new file mode 100644 index 000000000..75271f63d --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeDial.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using Elsa.Activities.Telnyx.Activities; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; +using Elsa.Services; + +namespace Elsa.Activities.Telnyx.Handlers +{ + public class ResumeDial : ResumeWebhookDrivenActivity + { + public ResumeDial(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad) + { + } + + protected override IEnumerable GetSupportedPayloadTypes() => new[] {typeof(CallInitiatedPayload), typeof(CallAnsweredPayload), typeof(CallHangupPayload)}; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeTransferCall.cs b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeTransferCall.cs new file mode 100644 index 000000000..56535c94e --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeTransferCall.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using Elsa.Activities.Telnyx.Activities; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; +using Elsa.Services; + +namespace Elsa.Activities.Telnyx.Handlers +{ + public class ResumeTransferCall : ResumeWebhookDrivenActivity + { + public ResumeTransferCall(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad) + { + } + + protected override IEnumerable GetSupportedPayloadTypes() => new[] {typeof(CallAnsweredPayload), typeof(CallInitiatedPayload), typeof(CallBridgedPayload), typeof(CallHangupPayload)}; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeWebhookDrivenActivity.cs b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeWebhookDrivenActivity.cs index 4b6e875d5..ef360e41c 100644 --- a/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeWebhookDrivenActivity.cs +++ b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeWebhookDrivenActivity.cs @@ -1,6 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using Elsa.Activities.Telnyx.Activities; using Elsa.Activities.Telnyx.Models; using Elsa.Activities.Telnyx.Providers.Bookmarks; using Elsa.Activities.Telnyx.Webhooks.Events; @@ -11,27 +13,41 @@ using MediatR; namespace Elsa.Activities.Telnyx.Handlers { - public abstract class ResumeWebhookDrivenActivity : INotificationHandler where TPayload: CallPayload + public abstract class ResumeWebhookDrivenActivity : ResumeWebhookDrivenActivity where TPayload : CallPayload + { + protected ResumeWebhookDrivenActivity(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad) + { + } + + protected override IEnumerable GetSupportedPayloadTypes() => new[] {typeof(TPayload)}; + } + + public abstract class ResumeWebhookDrivenActivity : INotificationHandler { private readonly IWorkflowLaunchpad _workflowLaunchpad; protected ResumeWebhookDrivenActivity(IWorkflowLaunchpad workflowLaunchpad) => _workflowLaunchpad = workflowLaunchpad; protected virtual string ActivityTypeName => typeof(TActivity).Name; + protected abstract IEnumerable GetSupportedPayloadTypes(); public async Task Handle(TelnyxWebhookReceived notification, CancellationToken cancellationToken) { - if (notification.Webhook.Data.Payload is not TPayload payload) + var supportedPayloadTypes = GetSupportedPayloadTypes().ToHashSet(); + var receivedPayload = (CallPayload) notification.Webhook.Data.Payload; + var receivedPayloadType = receivedPayload.GetType(); + + if (!supportedPayloadTypes.Contains(receivedPayloadType)) return; - - var correlationId = GetCorrelationId(payload); + + var correlationId = GetCorrelationId(receivedPayload); var trigger = CreateBookmark(); var bookmark = CreateBookmark(); var context = new CollectWorkflowsContext(ActivityTypeName, bookmark, trigger, correlationId); - await _workflowLaunchpad.CollectAndDispatchWorkflowsAsync(context, payload, cancellationToken); + await _workflowLaunchpad.CollectAndDispatchWorkflowsAsync(context, receivedPayload, cancellationToken); } - + protected virtual IBookmark CreateBookmark() => new GatherUsingSpeakBookmark(); - - private string GetCorrelationId(TPayload payload) + + private string GetCorrelationId(CallPayload payload) { if (!string.IsNullOrWhiteSpace(payload.ClientState)) { diff --git a/src/activities/Elsa.Activities.Telnyx/OutcomeNames.cs b/src/activities/Elsa.Activities.Telnyx/OutcomeNames.cs index 21bad4cdb..d9fb78766 100644 --- a/src/activities/Elsa.Activities.Telnyx/OutcomeNames.cs +++ b/src/activities/Elsa.Activities.Telnyx/OutcomeNames.cs @@ -4,9 +4,21 @@ { public const string CallIsNoLongerActive = "Call Is No Longer Active"; public const string Pending = "Pending"; + public const string Connected = "Connected"; + public const string NoResponse = "No Response"; + public const string Dialing = "Dialing"; + public const string CallInitiated = "CallInitiated"; public const string Answered = "Answered"; + public const string Hangup = "Hangup"; + public const string GatheringInput = "Gathering Input"; public const string GatherCompleted = "Gather Completed"; - public const string InputReceived = "Input Received"; + public const string Transferring = "Transferring"; + public const string Bridging = "Bridging"; + public const string Bridged = "Bridged"; + public const string LegABridged = "Call A Answered"; + public const string LegBBridged = "Call B Answered"; + public const string MachineDetectionEnded = "Machine Detection Ended"; + public const string MachineGreetingEnded = "Machine Greeting Ended"; public const string FinishedSpeaking = "Finished Speaking"; public const string Speaking = "Speaking"; public const string Recording = "Recording"; diff --git a/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/BridgeCallsBookmark.cs b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/BridgeCallsBookmark.cs new file mode 100644 index 000000000..65fbadf6a --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/BridgeCallsBookmark.cs @@ -0,0 +1,13 @@ +using Elsa.Activities.Telnyx.Activities; +using Elsa.Bookmarks; + +namespace Elsa.Activities.Telnyx.Providers.Bookmarks +{ + public class BridgeCallsBookmark : IBookmark + { + } + + public class BridgeCallsBookmarkProvider : DefaultBookmarkProvider + { + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/DialBookmark.cs b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/DialBookmark.cs new file mode 100644 index 000000000..8e483be28 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/DialBookmark.cs @@ -0,0 +1,13 @@ +using Elsa.Activities.Telnyx.Activities; +using Elsa.Bookmarks; + +namespace Elsa.Activities.Telnyx.Providers.Bookmarks +{ + public class DialBookmark : IBookmark + { + } + + public class DialBookmarkProvider : DefaultBookmarkProvider + { + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/TransferCallBookmark.cs b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/TransferCallBookmark.cs new file mode 100644 index 000000000..2ac7c8e2f --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/TransferCallBookmark.cs @@ -0,0 +1,13 @@ +using Elsa.Activities.Telnyx.Activities; +using Elsa.Bookmarks; + +namespace Elsa.Activities.Telnyx.Providers.Bookmarks +{ + public class TransferCallBookmark : IBookmark + { + } + + public class TransferCallBookmarkProvider : DefaultBookmarkProvider + { + } +} \ No newline at end of file