Telnyx activities enhancements

This commit is contained in:
Sipke Schoorstra 2021-06-05 12:56:04 +02:00
parent b4f83f43aa
commit c2ce8a72b9
15 changed files with 322 additions and 116 deletions

View file

@ -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<IActivityExecutionResult> 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<CallBridgedPayload>()!;
var results = new List<IActivityExecutionResult>();
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<CallAnsweredPayload>();
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<Dial, DialResponse>(inboundCallActivityId, x => x.DialResponse) : default;
return inboundCallActivityResponse != null ? inboundCallActivityResponse.CallControlId : null;
}
}
public static class BridgeCallsExtensions

View file

@ -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<string> Extensions
{
get => GetState<IList<string>>(() => new List<string>());
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<string>()!;
@ -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<RingGroupStrategy>();
@ -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<string?>();
set => SetState(value);
}
private CallAnsweredPayload? CallAnsweredPayload
{
get => GetState<CallAnsweredPayload?>();
@ -148,64 +142,53 @@ namespace Elsa.Activities.Telnyx.Activities
private void BuildPrioritizedHuntFlow(IOutcomeBuilder builder) =>
builder
.ForEach(() => Extensions, iterate => iterate
.Then<Dial>(a => a
.WithConnectionId(() => CallControlAppId)
.WithTo(async context => await ResolveExtensionAsync(context, context.GetInput<string>()!))
.WithTimeoutSecs(() => (int) RingTime.TotalSeconds)
.WithFrom(() => From)
.WithFromDisplayName(() => FromDisplayName)
.WithClientState(context => new ClientStatePayload(context.CorrelationId!).ToBase64())
.Then<Dial>(dial => dial
.WithConnectionId(() => CallControlAppId)
.WithTo(ResolveExtensionAsync)
.WithTimeoutSecs(() => (int) RingTime.TotalSeconds)
.WithFrom(() => From)
.WithFromDisplayName(() => FromDisplayName),
dial =>
{
dial
.When(TelnyxOutcomeNames.Answered)
.Then<BridgeCalls>(bridgeCalls =>
bridgeCalls.When(TelnyxOutcomeNames.Bridged)
.Finish(TelnyxOutcomeNames.Connected));
}
)
.Then(context => DialedControlId = context.GetInput<DialResponse>()!.CallControlId)
.Then<Fork>(fork => fork.WithBranches("Connected", "No Response"), fork =>
{
fork
.When("Connected")
.ThenTypeNamed(CallAnsweredPayload.ActivityTypeName)
.Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput<TelnyxWebhook>()!.Data.Payload)
.Then<BridgeCalls>(bridge => bridge
.WithCallControlIdA(() => CallControlId)
.WithCallControlIdB(() => DialedControlId))
.ThenTypeNamed(CallBridgedPayload.ActivityTypeName)
.ThenTypeNamed(CallBridgedPayload.ActivityTypeName)
.Then<Finish>(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 => fork.WithBranches("Connected", "Timeout", "Dial Everyone"), fork =>
{
fork
.When("Connected")
.When(TelnyxOutcomeNames.Connected)
.ThenTypeNamed(CallAnsweredPayload.ActivityTypeName)
.Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput<TelnyxWebhook>()!.Data.Payload)
.Then<BridgeCalls>(bridge => bridge
.WithCallControlIdA(() => CallControlId)
.WithCallControlIdB(() => CallAnsweredPayload!.CallControlId))
.ThenTypeNamed(CallBridgedPayload.ActivityTypeName)
.Then<Finish>(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<Dial>(a => a
.WithSuspendWorkflow(false)
.WithConnectionId(() => CallControlAppId)
.WithTo(async context => await ResolveExtensionAsync(context, context.GetInput<string>()!))
.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<string> ResolveExtensionAsync(ActivityExecutionContext context, string extension)
private static async ValueTask<string> ResolveExtensionAsync(ActivityExecutionContext context)
{
if (context.Resuming)
return context.GetActivityProperty<Dial, string>(x => x.To)!;
var extension = context.GetInput<string>()!;
var extensionProvider = context.GetService<IExtensionProvider>();
var resolvedExtension = await extensionProvider.GetAsync(extension, context.CancellationToken);
return resolvedExtension?.Number ?? extension;

View file

@ -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<Header>? 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<IActivityExecutionResult> 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<CallPayload>();
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<DialResponse> 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<Dial> WithCommandId(this ISetupActivity<Dial> setup, Func<string?> value) => setup.Set(x => x.CommandId, value);
public static ISetupActivity<Dial> WithCommandId(this ISetupActivity<Dial> setup, string? value) => setup.Set(x => x.CommandId, value);
public static ISetupActivity<Dial> WithClientState(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, ValueTask<string?>> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<Dial> WithClientState(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<Dial> WithClientState(this ISetupActivity<Dial> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<Dial> WithClientState(this ISetupActivity<Dial> setup, Func<string?> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<Dial> WithClientState(this ISetupActivity<Dial> setup, string? value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<Dial> WithCustomHeaders(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, ValueTask<IList<Header>?>> value) => setup.Set(x => x.CustomHeaders, value);
public static ISetupActivity<Dial> WithCustomHeaders(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, IList<Header>?> value) => setup.Set(x => x.CustomHeaders, value);
public static ISetupActivity<Dial> WithCustomHeaders(this ISetupActivity<Dial> setup, Func<ValueTask<IList<Header>?>> value) => setup.Set(x => x.CustomHeaders, value);
@ -254,5 +272,11 @@ namespace Elsa.Activities.Telnyx.Activities
public static ISetupActivity<Dial> WithWebhookUrlMethod(this ISetupActivity<Dial> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.WebhookUrlMethod, value);
public static ISetupActivity<Dial> WithWebhookUrlMethod(this ISetupActivity<Dial> setup, Func<string?> value) => setup.Set(x => x.WebhookUrlMethod, value);
public static ISetupActivity<Dial> WithWebhookUrlMethod(this ISetupActivity<Dial> setup, string? value) => setup.Set(x => x.WebhookUrlMethod, value);
public static ISetupActivity<Dial> WithSuspendWorkflow(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, ValueTask<bool>> value) => setup.Set(x => x.SuspendWorkflow, value);
public static ISetupActivity<Dial> WithSuspendWorkflow(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, bool> value) => setup.Set(x => x.SuspendWorkflow, value);
public static ISetupActivity<Dial> WithSuspendWorkflow(this ISetupActivity<Dial> setup, Func<ValueTask<bool>> value) => setup.Set(x => x.SuspendWorkflow, value);
public static ISetupActivity<Dial> WithSuspendWorkflow(this ISetupActivity<Dial> setup, Func<bool> value) => setup.Set(x => x.SuspendWorkflow, value);
public static ISetupActivity<Dial> WithSuspendWorkflow(this ISetupActivity<Dial> setup, bool value) => setup.Set(x => x.SuspendWorkflow, value);
}
}

View file

@ -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)
{

View file

@ -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)
{

View file

@ -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)
{

View file

@ -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<Header>? 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<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
await TransferCallAsync(context);
return Done();
return Combine(Outcome(TelnyxOutcomeNames.Transferring), Suspend());
}
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
var payload = context.GetInput<CallPayload>();
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,

View file

@ -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<BridgeCalls, CallBridgedPayload>
{
public ResumeBridgeCalls(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad)
{
}
}
}

View file

@ -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<Dial>
{
public ResumeDial(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad)
{
}
protected override IEnumerable<Type> GetSupportedPayloadTypes() => new[] {typeof(CallInitiatedPayload), typeof(CallAnsweredPayload), typeof(CallHangupPayload)};
}
}

View file

@ -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<TransferCall>
{
public ResumeTransferCall(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad)
{
}
protected override IEnumerable<Type> GetSupportedPayloadTypes() => new[] {typeof(CallAnsweredPayload), typeof(CallInitiatedPayload), typeof(CallBridgedPayload), typeof(CallHangupPayload)};
}
}

View file

@ -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<TActivity, TPayload> : INotificationHandler<TelnyxWebhookReceived> where TPayload: CallPayload
public abstract class ResumeWebhookDrivenActivity<TActivity, TPayload> : ResumeWebhookDrivenActivity<TActivity> where TPayload : CallPayload
{
protected ResumeWebhookDrivenActivity(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad)
{
}
protected override IEnumerable<Type> GetSupportedPayloadTypes() => new[] {typeof(TPayload)};
}
public abstract class ResumeWebhookDrivenActivity<TActivity> : INotificationHandler<TelnyxWebhookReceived>
{
private readonly IWorkflowLaunchpad _workflowLaunchpad;
protected ResumeWebhookDrivenActivity(IWorkflowLaunchpad workflowLaunchpad) => _workflowLaunchpad = workflowLaunchpad;
protected virtual string ActivityTypeName => typeof(TActivity).Name;
protected abstract IEnumerable<Type> 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))
{

View file

@ -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";

View file

@ -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<BridgeCallsBookmark, BridgeCalls>
{
}
}

View file

@ -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<DialBookmark, Dial>
{
}
}

View file

@ -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<TransferCallBookmark, TransferCall>
{
}
}