diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/AnswerCall.cs b/src/activities/Elsa.Activities.Telnyx/Activities/AnswerCall.cs index b738fe15a..67126f63a 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/AnswerCall.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/AnswerCall.cs @@ -5,15 +5,17 @@ using Elsa.Activities.Telnyx.Extensions; using Elsa.ActivityResults; using Elsa.Attributes; using Elsa.Design; +using Elsa.Exceptions; using Elsa.Services; using Elsa.Services.Models; +using Refit; namespace Elsa.Activities.Telnyx.Activities { [Action( Category = Constants.Category, Description = "Answer an incoming call. You must issue this command before executing subsequent commands on an incoming call", - Outcomes = new[] { OutcomeNames.Done }, + Outcomes = new[] { OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive }, DisplayName = "Answer Call" )] public class AnswerCall : Activity @@ -44,16 +46,21 @@ namespace Elsa.Activities.Telnyx.Activities public string? WebhookUrlMethod { get; set; } protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) - { - await AnswerCallAsync(context); - return Done(); - } - - private async ValueTask AnswerCallAsync(ActivityExecutionContext context) - { - var callControlId = context.GetCallControlId(CallControlId); - var request = new AnswerCallRequest(BillingGroupId, ClientState, CommandId, WebhookUrl, WebhookUrlMethod); - await _telnyxClient.Calls.AnswerCallAsync(callControlId, request, context.CancellationToken); + { + try + { + var callControlId = context.GetCallControlId(CallControlId); + var request = new AnswerCallRequest(BillingGroupId, ClientState, CommandId, WebhookUrl, WebhookUrlMethod); + await _telnyxClient.Calls.AnswerCallAsync(callControlId, request, context.CancellationToken); + return Done(); + } + catch (ApiException e) + { + if (await e.CallIsNoLongerActiveAsync()) + return Outcome(TelnyxOutcomeNames.CallIsNoLongerActive); + + throw new WorkflowException(e.Content ?? e.Message, e); + } } } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/BridgeCalls.cs b/src/activities/Elsa.Activities.Telnyx/Activities/BridgeCalls.cs new file mode 100644 index 000000000..263dde0bb --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Activities/BridgeCalls.cs @@ -0,0 +1,104 @@ +using System; +using System.Threading.Tasks; +using Elsa.Activities.Telnyx.Client.Models; +using Elsa.Activities.Telnyx.Client.Services; +using Elsa.Activities.Telnyx.Extensions; +using Elsa.ActivityResults; +using Elsa.Attributes; +using Elsa.Builders; +using Elsa.Design; +using Elsa.Exceptions; +using Elsa.Services; +using Elsa.Services.Models; +using Refit; + +namespace Elsa.Activities.Telnyx.Activities +{ + [Action( + Category = Constants.Category, + Description = "Bridge two call control calls.", + Outcomes = new[] { OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive }, + DisplayName = "Bridge Calls" + )] + public class BridgeCalls : Activity + { + private readonly ITelnyxClient _telnyxClient; + + public BridgeCalls(ITelnyxClient telnyxClient) + { + _telnyxClient = telnyxClient; + } + + [ActivityProperty(Label = "Call Control ID A", Hint = "Unique identifier and token for controlling the call.")] + public string CallControlIdA { get; set; } = default!; + + [ActivityProperty(Label = "Call Control ID B", Hint = "The Call Control ID of the call you want to bridge with.")] + public string CallControlIdB { get; set; } = default!; + + [ActivityProperty(Label = "Command ID", Hint = "Use this field to avoid duplicate commands. Telnyx will ignore commands with the same Command ID.", Category = PropertyCategories.Advanced)] + public string? CommandId { get; set; } + + [ActivityProperty(Label = "Client State", Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", Category = PropertyCategories.Advanced)] + public string? ClientState { get; set; } + + [ActivityProperty(Label = "Webhook URL Method", Hint = "HTTP request type used for Webhook URL", UIHint = ActivityPropertyUIHints.Dropdown, Options = new[] { "", "self" }, Category = PropertyCategories.Advanced)] + public string? ParkAfterUnbridged { get; set; } + + protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) + { + var callControlIdA = context.GetCallControlId(CallControlIdA); + + var request = new BridgeCallsRequest( + CallControlIdB, + ClientState, + CommandId, + ParkAfterUnbridged + ); + + try + { + await _telnyxClient.Calls.BridgeCallsAsync(callControlIdA, request, context.CancellationToken); + return Done(); + } + catch (ApiException e) + { + if (await e.CallIsNoLongerActiveAsync()) + return Outcome(TelnyxOutcomeNames.CallIsNoLongerActive); + + throw new WorkflowException(e.Content ?? e.Message, e); + } + } + } + + public static class BridgeCallsExtensions + { + public static ISetupActivity WithCallControlIdA(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlIdA, value); + public static ISetupActivity WithCallControlIdA(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlIdA, value); + public static ISetupActivity WithCallControlIdA(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlIdA, value); + public static ISetupActivity WithCallControlIdA(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlIdA, value); + public static ISetupActivity WithCallControlIdA(this ISetupActivity setup, string? value) => setup.Set(x => x.CallControlIdA, value); + + public static ISetupActivity WithCallControlIdB(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlIdB, value); + public static ISetupActivity WithCallControlIdB(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlIdB, value); + public static ISetupActivity WithCallControlIdB(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlIdB, value); + public static ISetupActivity WithCallControlIdB(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlIdB, value); + public static ISetupActivity WithCallControlIdB(this ISetupActivity setup, string? value) => setup.Set(x => x.CallControlIdB, value); + + public static ISetupActivity WithCommandId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CommandId, value); + public static ISetupActivity WithCommandId(this ISetupActivity setup, Func value) => setup.Set(x => x.CommandId, value); + public static ISetupActivity WithCommandId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CommandId, value); + 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 WithParkAfterUnbridged(this ISetupActivity setup, Func value) => setup.Set(x => x.ParkAfterUnbridged, value); + public static ISetupActivity WithParkAfterUnbridged(this ISetupActivity setup, Func> value) => setup.Set(x => x.ParkAfterUnbridged, value); + public static ISetupActivity WithParkAfterUnbridged(this ISetupActivity setup, Func value) => setup.Set(x => x.ParkAfterUnbridged, value); + public static ISetupActivity WithParkAfterUnbridged(this ISetupActivity setup, string? value) => setup.Set(x => x.ParkAfterUnbridged, value); + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs b/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs index 90ff9d24a..addc58700 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs @@ -1,10 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Elsa.Activities.ControlFlow; using Elsa.Activities.Telnyx.Client.Models; using Elsa.Activities.Telnyx.Client.Services; +using Elsa.Activities.Telnyx.Models; using Elsa.Activities.Telnyx.Webhooks.Models; using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; using Elsa.Activities.Temporal; @@ -14,7 +16,9 @@ using Elsa.Design; using Elsa.Metadata; using Elsa.Services; using Elsa.Services.Models; +using Microsoft.Extensions.Logging; using NodaTime; +using Refit; namespace Elsa.Activities.Telnyx.Activities { @@ -26,6 +30,13 @@ namespace Elsa.Activities.Telnyx.Activities )] public class CallRingGroup : CompositeActivity, IActivityPropertyDefaultValueProvider { + private readonly ILogger _logger; + + public CallRingGroup(ILogger logger) + { + _logger = logger; + } + [ActivityProperty(UIHint = ActivityPropertyUIHints.MultiText)] public IList Extensions { @@ -33,13 +44,13 @@ namespace Elsa.Activities.Telnyx.Activities set => SetState(value); } - [ActivityProperty(Label = "Call Control ID", Hint = "Unique identifier and token for controlling the call.", Category = PropertyCategories.Advanced)] + [ActivityProperty(Label = "Call Control ID", Hint = "Unique identifier and token for controlling the call.")] public string? CallControlId { get => GetState(); set => SetState(value); } - + [ActivityProperty] public RingGroupStrategy Strategy { @@ -69,6 +80,18 @@ namespace Elsa.Activities.Telnyx.Activities get => GetState(() => Duration.FromSeconds(20)); set => SetState(value); } + + private string? DialedControlId + { + get => GetState(); + set => SetState(value); + } + + private CallAnsweredPayload? CallAnsweredPayload + { + get => GetState(); + set => SetState(value); + } private IList CollectedCallControlIds { @@ -86,16 +109,25 @@ namespace Elsa.Activities.Telnyx.Activities protected override async ValueTask OnExitAsync(ActivityExecutionContext context, object? output) { string? answeredCallControlId = null; - - if (output is CallAnsweredPayload callAnsweredPayload) + + if (output is CallAnsweredPayload callAnsweredPayload) answeredCallControlId = callAnsweredPayload.CallControlId; - + // Hang up any pending calls. var outgoingCalls = CollectedCallControlIds.Where(x => x != answeredCallControlId).ToList(); var client = context.GetService(); - foreach (var outgoingCall in outgoingCalls) - await client.Calls.HangupCallAsync(outgoingCall, new HangupCallRequest(null, null), context.CancellationToken); + foreach (var outgoingCall in outgoingCalls) + { + try + { + await client.Calls.HangupCallAsync(outgoingCall, new HangupCallRequest(null, null), context.CancellationToken); + } + catch (ApiException e) + { + _logger.LogTrace(e, "Error while trying to hang up an outgoing call"); + } + } } private void BuildPrioritizedHuntFlow(IOutcomeBuilder builder) => @@ -108,14 +140,19 @@ namespace Elsa.Activities.Telnyx.Activities .WithTimeoutSecs(() => (int) RingTime.TotalSeconds) .WithFrom(() => From) .WithFromDisplayName(() => FromDisplayName) + .WithClientState(context => new ClientStatePayload(context.CorrelationId!).ToBase64()) ) + .Then(context => DialedControlId = context.GetInput()!.CallControlId) .Then(fork => fork.WithBranches("Connected", "No Response"), fork => { fork .When("Connected") .ThenTypeNamed(CallAnsweredPayload.ActivityTypeName) - // Bridge - .Then(finish => finish.WithOutcome("Connected").WithOutput(context => context.GetInput()!.Data.Payload)); + .Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput()!.Data.Payload) + .Then(bridge => bridge + .WithCallControlIdA(() => DialedControlId) + .WithCallControlIdB(() => CallAnsweredPayload!.CallControlId)) + .Then(finish => finish.WithOutcome("Connected").WithOutput(() => CallAnsweredPayload)); fork .When("No Response") @@ -131,8 +168,11 @@ namespace Elsa.Activities.Telnyx.Activities fork .When("Connected") .ThenTypeNamed(CallAnsweredPayload.ActivityTypeName) - // Bridge - .Then(finish => finish.WithOutcome("Connected").WithOutput(context => context.GetInput()!.Data.Payload)); + .Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput()!.Data.Payload) + .Then(bridge => bridge + .WithCallControlIdA(() => DialedControlId) + .WithCallControlIdB(() => CallAnsweredPayload!.CallControlId)) + .Then(finish => finish.WithOutcome("Connected").WithOutput(() => CallAnsweredPayload)); fork .When("Timeout") @@ -149,9 +189,11 @@ namespace Elsa.Activities.Telnyx.Activities .WithTimeoutSecs(() => (int) RingTime.TotalSeconds) .WithFrom(() => From) .WithFromDisplayName(() => FromDisplayName) + .WithClientState(context => new ClientStatePayload(context.CorrelationId!).ToBase64()) ) + // TODO: Store list of dial results so we can pick correct one from Call Answered Payload webhook. + //.Then(context => DialedControlId = context.GetInput()!.CallControlId) .Then(CollectCallControlIds)); - }); private void CollectCallControlIds(ActivityExecutionContext context) @@ -161,7 +203,7 @@ namespace Elsa.Activities.Telnyx.Activities collection.Add(callControlId); CollectedCallControlIds = collection; } - + object IActivityPropertyDefaultValueProvider.GetDefaultValue(PropertyInfo property) => Duration.FromSeconds(20); } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs b/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs index c184822f9..f45d889f3 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs @@ -109,7 +109,8 @@ namespace Elsa.Activities.Telnyx.Activities try { - return await _telnyxClient.Calls.DialAsync(request, context.CancellationToken); + var response = await _telnyxClient.Calls.DialAsync(request, context.CancellationToken); + return response.Data; } catch (ApiException e) { diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs index bb42ea7c0..d6c4ed7ee 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using Elsa.Activities.Telnyx.Client.Models; using Elsa.Activities.Telnyx.Client.Services; @@ -16,7 +18,7 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Play an audio file on the call until the required DTMF signals are gathered to build interactive menus", - Outcomes = new[] { OutcomeNames.Done }, + Outcomes = new[] { OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive }, DisplayName = "Gather Using Audio" )] public class GatherUsingAudio : Activity @@ -65,12 +67,6 @@ namespace Elsa.Activities.Telnyx.Activities public int? TimeoutMillis { get; set; } = 60000; protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) - { - await GatherUsingAudioAsync(context); - return Done(); - } - - private async ValueTask GatherUsingAudioAsync(ActivityExecutionContext context) { var request = new GatherUsingAudioRequest( AudioUrl, @@ -91,9 +87,13 @@ namespace Elsa.Activities.Telnyx.Activities try { await _telnyxClient.Calls.GatherUsingAudioAsync(callControlId, request, context.CancellationToken); + return Done(); } catch (ApiException e) { + if (await e.CallIsNoLongerActiveAsync()) + return Outcome(TelnyxOutcomeNames.CallIsNoLongerActive); + throw new WorkflowException(e.Content ?? e.Message, e); } } diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/HangupCall.cs b/src/activities/Elsa.Activities.Telnyx/Activities/HangupCall.cs index 141530f3f..5e579a46f 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/HangupCall.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/HangupCall.cs @@ -15,7 +15,7 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Hang up the call.", - Outcomes = new[] { OutcomeNames.Done }, + Outcomes = new[] { OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive }, DisplayName = "Hangup Call" )] public class HangupCall : Activity @@ -29,7 +29,7 @@ namespace Elsa.Activities.Telnyx.Activities [ActivityProperty(Label = "Call Control ID", Hint = "Unique identifier and token for controlling the call", Category = PropertyCategories.Advanced)] public string CallControlId { get; set; } = default!; - + [ActivityProperty(Label = "Client State", Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", Category = PropertyCategories.Advanced)] public string? ClientState { get; set; } @@ -37,22 +37,20 @@ namespace Elsa.Activities.Telnyx.Activities public string? CommandId { get; set; } protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) - { - await HangupCallAsync(context); - return Done(); - } - - private async ValueTask HangupCallAsync(ActivityExecutionContext context) { var callControlId = context.GetCallControlId(CallControlId); var request = new HangupCallRequest(ClientState, CommandId); - + try { await _telnyxClient.Calls.HangupCallAsync(callControlId, request, context.CancellationToken); + return Done(); } catch (ApiException e) { + if (await e.CallIsNoLongerActiveAsync()) + return Outcome(TelnyxOutcomeNames.CallIsNoLongerActive); + throw new WorkflowException(e.Content ?? e.Message, e); } } diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/ResolveExtension.cs b/src/activities/Elsa.Activities.Telnyx/Activities/ResolveExtension.cs index b4475e289..6800782e5 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/ResolveExtension.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/ResolveExtension.cs @@ -31,7 +31,7 @@ namespace Elsa.Activities.Telnyx.Activities { var resolvedNumber = await _extensionProvider.GetAsync(Extension, context.CancellationToken); var result = resolvedNumber != null ? Outcome("Resolved", resolvedNumber) : Outcome("Unresolved", Extension); - return Combine(result, Done()); + return Combine(result); } } diff --git a/src/activities/Elsa.Activities.Telnyx/ActivityTypes/NotificationActivityTypeProvider.cs b/src/activities/Elsa.Activities.Telnyx/ActivityTypes/NotificationActivityTypeProvider.cs index be5dd7785..807733718 100644 --- a/src/activities/Elsa.Activities.Telnyx/ActivityTypes/NotificationActivityTypeProvider.cs +++ b/src/activities/Elsa.Activities.Telnyx/ActivityTypes/NotificationActivityTypeProvider.cs @@ -13,7 +13,6 @@ using Elsa.ActivityProviders; using Elsa.ActivityResults; using Elsa.Metadata; using Elsa.Services.Models; -using Microsoft.AspNetCore.Authorization.Policy; namespace Elsa.Activities.Telnyx.ActivityTypes { @@ -41,12 +40,18 @@ namespace Elsa.Activities.Telnyx.ActivityTypes var activityTypes = payloadTypes.Select(CreateWebhookActivityType).ToList(); // Add variations on the same webhooks. The webhook filters will conditionally select the appropriate one. - var hangupWebhookAttribute = payloadTypes.First(x => x == typeof(CallHangupPayload)).GetCustomAttribute()!; - activityTypes.Add(CreateWebhookActivityType(new WebhookAttribute(hangupWebhookAttribute.EventType, "OriginatorCallHangup", "Originator Call Hangup", "Triggered when an incoming call was hangup by the originator."))); + activityTypes.Add(CreateWebhookActivityTypeVariation("BridgeCallInitiated", "Bridge Call Initiated", "Triggered when an incoming bridging call was received.")); + activityTypes.Add(CreateWebhookActivityTypeVariation("OriginatorCallHangup", "Originator Call Hangup", "Triggered when an incoming call was hangup by the originator.")); return activityTypes; } + private ActivityType CreateWebhookActivityTypeVariation(string activityType, string displayName, string description) + { + var hangupWebhookAttribute = typeof(T).GetCustomAttribute()!; + return CreateWebhookActivityType(new WebhookAttribute(hangupWebhookAttribute.EventType, activityType, displayName, description)); + } + private static ActivityType CreateWebhookActivityType(Type payloadType) { var webhookAttribute = payloadType.GetCustomAttribute(); @@ -92,11 +97,8 @@ namespace Elsa.Activities.Telnyx.ActivityTypes { var webhook = (TelnyxWebhook) context.Input!; - if (webhook.Data.Payload is CallPayload callPayload) - { + if (webhook.Data.Payload is CallPayload callPayload) context.WorkflowExecutionContext.CorrelationId ??= callPayload.CallSessionId; - context.SetVariable("CallControlId", callPayload.CallControlId); - } return new(new CombinedResult(new OutputResult(context.Input), new DoneResult())); } diff --git a/src/activities/Elsa.Activities.Telnyx/Client/Models/Errors.cs b/src/activities/Elsa.Activities.Telnyx/Client/Models/Errors.cs new file mode 100644 index 000000000..02fcc7d8a --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Client/Models/Errors.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Elsa.Activities.Telnyx.Client.Models +{ + public record ErrorResponse(IList Errors); + public record Error(int Code, string Title, string Detail); + + public static class ErrorCodes + { + public const int CallHasAlreadyEnded = 90018; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Client/Models/Requests.cs b/src/activities/Elsa.Activities.Telnyx/Client/Models/Requests.cs index 435ddb54c..466bb0a50 100644 --- a/src/activities/Elsa.Activities.Telnyx/Client/Models/Requests.cs +++ b/src/activities/Elsa.Activities.Telnyx/Client/Models/Requests.cs @@ -55,4 +55,11 @@ namespace Elsa.Activities.Telnyx.Client.Models string? WebhookUrl, string? WebhookUrlMethod ); + + public record BridgeCallsRequest( + string CallControlId, + string? ClientState, + string? CommandId, + string? ParkAfterUnbridge + ); } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Client/Models/Responses.cs b/src/activities/Elsa.Activities.Telnyx/Client/Models/Responses.cs index d63d85c34..54c6609a6 100644 --- a/src/activities/Elsa.Activities.Telnyx/Client/Models/Responses.cs +++ b/src/activities/Elsa.Activities.Telnyx/Client/Models/Responses.cs @@ -1,4 +1,5 @@ namespace Elsa.Activities.Telnyx.Client.Models { + public record TelnyxResponse(T Data); public record DialResponse(string CallControlId, string CallLegId, string CallSessionId, bool IsAlive, string RecordType); } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs b/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs index 56365d8cd..2b37023d3 100644 --- a/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs +++ b/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs @@ -8,7 +8,7 @@ namespace Elsa.Activities.Telnyx.Client.Services public interface ICallsApi { [Post("/v2/calls")] - Task DialAsync([Body]DialRequest request, CancellationToken cancellationToken = default); + Task> DialAsync([Body]DialRequest request, CancellationToken cancellationToken = default); [Post("/v2/calls/{callControlId}/actions/answer")] Task AnswerCallAsync(string callControlId, [Body]AnswerCallRequest request, CancellationToken cancellationToken = default); @@ -21,5 +21,8 @@ namespace Elsa.Activities.Telnyx.Client.Services [Post("/v2/calls/{callControlId}/actions/gather_using_audio")] Task GatherUsingAudioAsync(string callControlId, [Body]GatherUsingAudioRequest request, CancellationToken cancellationToken = default); + + [Post("/v2/calls/{callControlId}/actions/bridge")] + Task BridgeCallsAsync(string callControlId, [Body]BridgeCallsRequest request, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Elsa.Activities.Telnyx.csproj b/src/activities/Elsa.Activities.Telnyx/Elsa.Activities.Telnyx.csproj index 32226340d..d2353b2b0 100644 --- a/src/activities/Elsa.Activities.Telnyx/Elsa.Activities.Telnyx.csproj +++ b/src/activities/Elsa.Activities.Telnyx/Elsa.Activities.Telnyx.csproj @@ -18,6 +18,7 @@ + diff --git a/src/activities/Elsa.Activities.Telnyx/Extensions/ActivityExecutionContextExtensions.cs b/src/activities/Elsa.Activities.Telnyx/Extensions/ActivityExecutionContextExtensions.cs index 34e6ca247..a4fc4365a 100644 --- a/src/activities/Elsa.Activities.Telnyx/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/activities/Elsa.Activities.Telnyx/Extensions/ActivityExecutionContextExtensions.cs @@ -14,10 +14,10 @@ namespace Elsa.Activities.Telnyx.Extensions if (!string.IsNullOrWhiteSpace(callControlId)) return callControlId; - callControlId = context.GetVariable("CallControlId"); - - if (!string.IsNullOrWhiteSpace(callControlId)) - return callControlId; + // callControlId = context.GetVariable("CallControlId"); + // + // if (!string.IsNullOrWhiteSpace(callControlId)) + // return callControlId; var options = context.GetService(); callControlId = options.CallControlAppId; diff --git a/src/activities/Elsa.Activities.Telnyx/Extensions/ApiExceptionExtensions.cs b/src/activities/Elsa.Activities.Telnyx/Extensions/ApiExceptionExtensions.cs new file mode 100644 index 000000000..07f779785 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Extensions/ApiExceptionExtensions.cs @@ -0,0 +1,25 @@ +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Telnyx.Client.Models; +using Refit; + +namespace Elsa.Activities.Telnyx.Extensions +{ + public static class ApiExceptionExtensions + { + public static async Task GetErrorResponseAsync(this ApiException e, CancellationToken cancellationToken = default) + { + var httpContent = new StringContent(e.Content!); + return (await e.RefitSettings.ContentSerializer.FromHttpContentAsync(httpContent, cancellationToken))!; + } + + public static async Task CallIsNoLongerActiveAsync(this ApiException e, CancellationToken cancellationToken = default) + { + var errorResponse = await e.GetErrorResponseAsync(cancellationToken); + var errors = errorResponse.Errors; + return errors.Any(x => x.Code == ErrorCodes.CallHasAlreadyEnded); + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Extensions/SetupExtensions.cs b/src/activities/Elsa.Activities.Telnyx/Extensions/SetupExtensions.cs index 014679729..75c6899b4 100644 --- a/src/activities/Elsa.Activities.Telnyx/Extensions/SetupExtensions.cs +++ b/src/activities/Elsa.Activities.Telnyx/Extensions/SetupExtensions.cs @@ -49,6 +49,7 @@ namespace Elsa.Activities.Telnyx.Extensions .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddScoped(telnyxOptions.ExtensionProviderFactory); // Telnyx API Client. @@ -139,7 +140,9 @@ namespace Elsa.Activities.Telnyx.Extensions protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var json = await (request.Content as StringContent)?.ReadAsStringAsync(cancellationToken)!; - return await base.SendAsync(request, cancellationToken); + var response = await base.SendAsync(request, cancellationToken); + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); + return response; } } diff --git a/src/activities/Elsa.Activities.Telnyx/Liquid/ConfigureLiquidEngine.cs b/src/activities/Elsa.Activities.Telnyx/Liquid/ConfigureLiquidEngine.cs new file mode 100644 index 000000000..68bad4916 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Liquid/ConfigureLiquidEngine.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Telnyx.Models; +using Elsa.Activities.Telnyx.Webhooks.Models; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; +using Elsa.Scripting.Liquid.Messages; +using Fluid; +using MediatR; + +namespace Elsa.Activities.Telnyx.Liquid +{ + public class ConfigureLiquidEngine : INotificationHandler + { + public Task Handle(EvaluatingLiquidExpression notification, CancellationToken cancellationToken) + { + var context = notification.TemplateContext; + var memberAccessStrategy = context.MemberAccessStrategy; + + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Models/ClientStatePayload.cs b/src/activities/Elsa.Activities.Telnyx/Models/ClientStatePayload.cs new file mode 100644 index 000000000..78e5285aa --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Models/ClientStatePayload.cs @@ -0,0 +1,23 @@ +using System; +using System.Text; +using Newtonsoft.Json; + +namespace Elsa.Activities.Telnyx.Models +{ + public record ClientStatePayload(string CorrelationId) + { + public static ClientStatePayload FromBase64(string base64) + { + var bytes = Convert.FromBase64String(base64); + var json = Encoding.UTF8.GetString(bytes); + return JsonConvert.DeserializeObject(json); + } + + public string ToBase64() + { + var json = JsonConvert.SerializeObject(this); + var bytes = Encoding.UTF8.GetBytes(json); + return Convert.ToBase64String(bytes); + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Models/RingGroupStrategy.cs b/src/activities/Elsa.Activities.Telnyx/Models/RingGroupStrategy.cs index 651e0d441..5c93e18ad 100644 --- a/src/activities/Elsa.Activities.Telnyx/Models/RingGroupStrategy.cs +++ b/src/activities/Elsa.Activities.Telnyx/Models/RingGroupStrategy.cs @@ -1,4 +1,4 @@ -namespace Elsa.Activities.Telnyx.Activities +namespace Elsa.Activities.Telnyx.Models { public enum RingGroupStrategy { diff --git a/src/activities/Elsa.Activities.Telnyx/OutcomeNames.cs b/src/activities/Elsa.Activities.Telnyx/OutcomeNames.cs new file mode 100644 index 000000000..a32e6b449 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/OutcomeNames.cs @@ -0,0 +1,7 @@ +namespace Elsa.Activities.Telnyx +{ + public static class TelnyxOutcomeNames + { + public const string CallIsNoLongerActive = "Call Is No Longer Active"; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Consumers/TriggerWorkflows.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Consumers/TriggerWorkflows.cs index df48bd90b..551936aff 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Consumers/TriggerWorkflows.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Consumers/TriggerWorkflows.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; using System.Threading.Tasks; using Elsa.Activities.Telnyx.Bookmarks; -using Elsa.Activities.Telnyx.Webhooks.Attributes; +using Elsa.Activities.Telnyx.Models; using Elsa.Activities.Telnyx.Webhooks.Events; using Elsa.Activities.Telnyx.Webhooks.Payloads.Abstract; using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; @@ -89,6 +86,12 @@ namespace Elsa.Activities.Telnyx.Webhooks.Consumers private string GetCorrelationId(Payload payload) { + if (!string.IsNullOrWhiteSpace(payload.ClientState)) + { + var clientStatePayload = ClientStatePayload.FromBase64(payload.ClientState); + return clientStatePayload.CorrelationId; + } + if (payload is CallPayload callPayload) return callPayload.CallSessionId; diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Filters/CallInitiatedWebhookFilter.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Filters/CallInitiatedWebhookFilter.cs new file mode 100644 index 000000000..bfa49541d --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Filters/CallInitiatedWebhookFilter.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using Elsa.Activities.Telnyx.Webhooks.Attributes; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Abstract; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; +using Elsa.Activities.Telnyx.Webhooks.Services; + +namespace Elsa.Activities.Telnyx.Webhooks.Filters +{ + public class CallInitiatedWebhookFilter : IWebhookFilter + { + public int Priority => 1; + + public bool CanHandlePayload(Payload payload) => payload is CallInitiatedPayload; + + public string GetActivityTypeName(Payload payload) + { + var callInitiated = (CallInitiatedPayload) payload; + var attribute = payload.GetType().GetCustomAttribute()!; + + if (callInitiated.State != "bridging") + return attribute.ActivityType; + + return "BridgeCallInitiated"; + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Messages/WebhookReceived.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Messages/WebhookReceived.cs index 40e7fb181..f6480de71 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Messages/WebhookReceived.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Messages/WebhookReceived.cs @@ -1,4 +1,4 @@ -namespace Elsa.Activities.Telnyx.Messages +namespace Elsa.Activities.Telnyx.Webhooks.Messages { public class WebhookReceived { diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallBridgedPayload.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallBridgedPayload.cs new file mode 100644 index 000000000..9edf8b9ba --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallBridgedPayload.cs @@ -0,0 +1,14 @@ +using Elsa.Activities.Telnyx.Webhooks.Attributes; + +namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call +{ + [Webhook(EventType, ActivityTypeName, "Call Bridged", "Triggered when an a call is bridged.")] + public sealed record CallBridgedPayload : CallPayload + { + public const string EventType = "call.bridged"; + public const string ActivityTypeName = "CallBridged"; + public string From { get; init; } = default!; + public string To { get; init; } = default!; + public string State { get; init; } = default!; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallInitiatedPayload.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallInitiatedPayload.cs index 5b647d9b5..efea98057 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallInitiatedPayload.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallInitiatedPayload.cs @@ -9,5 +9,6 @@ namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call public const string ActivityTypeName = "CallInitiated"; public string Direction { get; init; } = default!; public string State { get; init; } = default!; + public string To { get; init; } = default!; } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookFilter.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookFilter.cs index d2b39bd2e..9defe46ab 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookFilter.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookFilter.cs @@ -1,5 +1,4 @@ -using Elsa.Activities.Telnyx.Webhooks.Attributes; -using Elsa.Activities.Telnyx.Webhooks.Payloads.Abstract; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Abstract; namespace Elsa.Activities.Telnyx.Webhooks.Services { diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookHandler.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookHandler.cs index f0a5e8aee..fdf49e6d6 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookHandler.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Elsa.Activities.Telnyx.Webhooks.Attributes; using Microsoft.AspNetCore.Http; namespace Elsa.Activities.Telnyx.Webhooks.Services diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/WebhookFilterService.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/WebhookFilterService.cs index 9a23f6ff4..80cce7a93 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/WebhookFilterService.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/WebhookFilterService.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using Elsa.Activities.Telnyx.Webhooks.Attributes; using Elsa.Activities.Telnyx.Webhooks.Payloads.Abstract; namespace Elsa.Activities.Telnyx.Webhooks.Services