Incremental work on CallRingGroup and other Telnyx activities

This commit is contained in:
Sipke Schoorstra 2021-03-25 22:16:56 +01:00
parent dfbba1e176
commit aa0c2fe711
28 changed files with 382 additions and 66 deletions

View file

@ -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<IActivityExecutionResult> 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);
}
}
}
}

View file

@ -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<IActivityExecutionResult> 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<BridgeCalls> WithCallControlIdA(this ISetupActivity<BridgeCalls> setup, Func<ActivityExecutionContext, ValueTask<string?>> value) => setup.Set(x => x.CallControlIdA, value);
public static ISetupActivity<BridgeCalls> WithCallControlIdA(this ISetupActivity<BridgeCalls> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.CallControlIdA, value);
public static ISetupActivity<BridgeCalls> WithCallControlIdA(this ISetupActivity<BridgeCalls> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.CallControlIdA, value);
public static ISetupActivity<BridgeCalls> WithCallControlIdA(this ISetupActivity<BridgeCalls> setup, Func<string?> value) => setup.Set(x => x.CallControlIdA, value);
public static ISetupActivity<BridgeCalls> WithCallControlIdA(this ISetupActivity<BridgeCalls> setup, string? value) => setup.Set(x => x.CallControlIdA, value);
public static ISetupActivity<BridgeCalls> WithCallControlIdB(this ISetupActivity<BridgeCalls> setup, Func<ActivityExecutionContext, ValueTask<string?>> value) => setup.Set(x => x.CallControlIdB, value);
public static ISetupActivity<BridgeCalls> WithCallControlIdB(this ISetupActivity<BridgeCalls> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.CallControlIdB, value);
public static ISetupActivity<BridgeCalls> WithCallControlIdB(this ISetupActivity<BridgeCalls> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.CallControlIdB, value);
public static ISetupActivity<BridgeCalls> WithCallControlIdB(this ISetupActivity<BridgeCalls> setup, Func<string?> value) => setup.Set(x => x.CallControlIdB, value);
public static ISetupActivity<BridgeCalls> WithCallControlIdB(this ISetupActivity<BridgeCalls> setup, string? value) => setup.Set(x => x.CallControlIdB, value);
public static ISetupActivity<BridgeCalls> WithCommandId(this ISetupActivity<BridgeCalls> setup, Func<ActivityExecutionContext, ValueTask<string?>> value) => setup.Set(x => x.CommandId, value);
public static ISetupActivity<BridgeCalls> WithCommandId(this ISetupActivity<BridgeCalls> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.CommandId, value);
public static ISetupActivity<BridgeCalls> WithCommandId(this ISetupActivity<BridgeCalls> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.CommandId, value);
public static ISetupActivity<BridgeCalls> WithCommandId(this ISetupActivity<BridgeCalls> setup, Func<string?> value) => setup.Set(x => x.CommandId, value);
public static ISetupActivity<BridgeCalls> WithCommandId(this ISetupActivity<BridgeCalls> setup, string? value) => setup.Set(x => x.CommandId, value);
public static ISetupActivity<BridgeCalls> WithClientState(this ISetupActivity<BridgeCalls> setup, Func<ActivityExecutionContext, ValueTask<string?>> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<BridgeCalls> WithClientState(this ISetupActivity<BridgeCalls> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<BridgeCalls> WithClientState(this ISetupActivity<BridgeCalls> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<BridgeCalls> WithClientState(this ISetupActivity<BridgeCalls> setup, Func<string?> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<BridgeCalls> WithClientState(this ISetupActivity<BridgeCalls> setup, string? value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<BridgeCalls> WithParkAfterUnbridged(this ISetupActivity<BridgeCalls> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.ParkAfterUnbridged, value);
public static ISetupActivity<BridgeCalls> WithParkAfterUnbridged(this ISetupActivity<BridgeCalls> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.ParkAfterUnbridged, value);
public static ISetupActivity<BridgeCalls> WithParkAfterUnbridged(this ISetupActivity<BridgeCalls> setup, Func<string?> value) => setup.Set(x => x.ParkAfterUnbridged, value);
public static ISetupActivity<BridgeCalls> WithParkAfterUnbridged(this ISetupActivity<BridgeCalls> setup, string? value) => setup.Set(x => x.ParkAfterUnbridged, value);
}
}

View file

@ -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<CallRingGroup> _logger;
public CallRingGroup(ILogger<CallRingGroup> logger)
{
_logger = logger;
}
[ActivityProperty(UIHint = ActivityPropertyUIHints.MultiText)]
public IList<string> 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<string>();
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<string?>();
set => SetState(value);
}
private CallAnsweredPayload? CallAnsweredPayload
{
get => GetState<CallAnsweredPayload?>();
set => SetState(value);
}
private IList<string> 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<ITelnyxClient>();
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<DialResponse>()!.CallControlId)
.Then<Fork>(fork => fork.WithBranches("Connected", "No Response"), fork =>
{
fork
.When("Connected")
.ThenTypeNamed(CallAnsweredPayload.ActivityTypeName)
// Bridge
.Then<Finish>(finish => finish.WithOutcome("Connected").WithOutput(context => context.GetInput<TelnyxWebhook>()!.Data.Payload));
.Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput<TelnyxWebhook>()!.Data.Payload)
.Then<BridgeCalls>(bridge => bridge
.WithCallControlIdA(() => DialedControlId)
.WithCallControlIdB(() => CallAnsweredPayload!.CallControlId))
.Then<Finish>(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 => finish.WithOutcome("Connected").WithOutput(context => context.GetInput<TelnyxWebhook>()!.Data.Payload));
.Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput<TelnyxWebhook>()!.Data.Payload)
.Then<BridgeCalls>(bridge => bridge
.WithCallControlIdA(() => DialedControlId)
.WithCallControlIdB(() => CallAnsweredPayload!.CallControlId))
.Then<Finish>(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<DialResponse>()!.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);
}
}

View file

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

View file

@ -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<IActivityExecutionResult> 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);
}
}

View file

@ -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<IActivityExecutionResult> 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);
}
}

View file

@ -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);
}
}

View file

@ -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<WebhookAttribute>()!;
activityTypes.Add(CreateWebhookActivityType(new WebhookAttribute(hangupWebhookAttribute.EventType, "OriginatorCallHangup", "Originator Call Hangup", "Triggered when an incoming call was hangup by the originator.")));
activityTypes.Add(CreateWebhookActivityTypeVariation<CallInitiatedPayload>("BridgeCallInitiated", "Bridge Call Initiated", "Triggered when an incoming bridging call was received."));
activityTypes.Add(CreateWebhookActivityTypeVariation<CallHangupPayload>("OriginatorCallHangup", "Originator Call Hangup", "Triggered when an incoming call was hangup by the originator."));
return activityTypes;
}
private ActivityType CreateWebhookActivityTypeVariation<T>(string activityType, string displayName, string description)
{
var hangupWebhookAttribute = typeof(T).GetCustomAttribute<WebhookAttribute>()!;
return CreateWebhookActivityType(new WebhookAttribute(hangupWebhookAttribute.EventType, activityType, displayName, description));
}
private static ActivityType CreateWebhookActivityType(Type payloadType)
{
var webhookAttribute = payloadType.GetCustomAttribute<WebhookAttribute>();
@ -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()));
}

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Elsa.Activities.Telnyx.Client.Models
{
public record ErrorResponse(IList<Error> Errors);
public record Error(int Code, string Title, string Detail);
public static class ErrorCodes
{
public const int CallHasAlreadyEnded = 90018;
}
}

View file

@ -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
);
}

View file

@ -1,4 +1,5 @@
namespace Elsa.Activities.Telnyx.Client.Models
{
public record TelnyxResponse<T>(T Data);
public record DialResponse(string CallControlId, string CallLegId, string CallSessionId, bool IsAlive, string RecordType);
}

View file

@ -8,7 +8,7 @@ namespace Elsa.Activities.Telnyx.Client.Services
public interface ICallsApi
{
[Post("/v2/calls")]
Task<DialResponse> DialAsync([Body]DialRequest request, CancellationToken cancellationToken = default);
Task<TelnyxResponse<DialResponse>> 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);
}
}

View file

@ -18,6 +18,7 @@
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
<ProjectReference Include="..\..\scripting\Elsa.Scripting.Liquid\Elsa.Scripting.Liquid.csproj" />
<ProjectReference Include="..\Elsa.Activities.Temporal.Common\Elsa.Activities.Temporal.Common.csproj" />
</ItemGroup>

View file

@ -14,10 +14,10 @@ namespace Elsa.Activities.Telnyx.Extensions
if (!string.IsNullOrWhiteSpace(callControlId))
return callControlId;
callControlId = context.GetVariable<string>("CallControlId");
if (!string.IsNullOrWhiteSpace(callControlId))
return callControlId;
// callControlId = context.GetVariable<string>("CallControlId");
//
// if (!string.IsNullOrWhiteSpace(callControlId))
// return callControlId;
var options = context.GetService<TelnyxOptions>();
callControlId = options.CallControlAppId;

View file

@ -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<ErrorResponse> GetErrorResponseAsync(this ApiException e, CancellationToken cancellationToken = default)
{
var httpContent = new StringContent(e.Content!);
return (await e.RefitSettings.ContentSerializer.FromHttpContentAsync<ErrorResponse>(httpContent, cancellationToken))!;
}
public static async Task<bool> 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);
}
}
}

View file

@ -49,6 +49,7 @@ namespace Elsa.Activities.Telnyx.Extensions
.AddSingleton<IWebhookFilterService, WebhookFilterService>()
.AddSingleton<IWebhookFilter, AttributeBasedWebhookFilter>()
.AddSingleton<IWebhookFilter, HangupWebhookFilter>()
.AddSingleton<IWebhookFilter, CallInitiatedWebhookFilter>()
.AddScoped(telnyxOptions.ExtensionProviderFactory);
// Telnyx API Client.
@ -139,7 +140,9 @@ namespace Elsa.Activities.Telnyx.Extensions
protected override async Task<HttpResponseMessage> 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;
}
}

View file

@ -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<EvaluatingLiquidExpression>
{
public Task Handle(EvaluatingLiquidExpression notification, CancellationToken cancellationToken)
{
var context = notification.TemplateContext;
var memberAccessStrategy = context.MemberAccessStrategy;
memberAccessStrategy.Register<Extension>();
memberAccessStrategy.Register<CallAnsweredPayload>();
memberAccessStrategy.Register<CallBridgedPayload>();
memberAccessStrategy.Register<CallDtmfReceivedPayload>();
memberAccessStrategy.Register<CallGatherEndedPayload>();
memberAccessStrategy.Register<CallHangupPayload>();
memberAccessStrategy.Register<CallInitiatedPayload>();
memberAccessStrategy.Register<CallPayload>();
memberAccessStrategy.Register<CallPlayback>();
memberAccessStrategy.Register<CallPlaybackEndedPayload>();
memberAccessStrategy.Register<CallPlaybackStartedPayload>();
memberAccessStrategy.Register<TelnyxRecord>();
memberAccessStrategy.Register<TelnyxWebhook>();
memberAccessStrategy.Register<TelnyxWebhookData>();
memberAccessStrategy.Register<TelnyxWebhookMeta>();
return Task.CompletedTask;
}
}
}

View file

@ -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<ClientStatePayload>(json);
}
public string ToBase64()
{
var json = JsonConvert.SerializeObject(this);
var bytes = Encoding.UTF8.GetBytes(json);
return Convert.ToBase64String(bytes);
}
}
}

View file

@ -1,4 +1,4 @@
namespace Elsa.Activities.Telnyx.Activities
namespace Elsa.Activities.Telnyx.Models
{
public enum RingGroupStrategy
{

View file

@ -0,0 +1,7 @@
namespace Elsa.Activities.Telnyx
{
public static class TelnyxOutcomeNames
{
public const string CallIsNoLongerActive = "Call Is No Longer Active";
}
}

View file

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

View file

@ -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<WebhookAttribute>()!;
if (callInitiated.State != "bridging")
return attribute.ActivityType;
return "BridgeCallInitiated";
}
}
}

View file

@ -1,4 +1,4 @@
namespace Elsa.Activities.Telnyx.Messages
namespace Elsa.Activities.Telnyx.Webhooks.Messages
{
public class WebhookReceived
{

View file

@ -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!;
}
}

View file

@ -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!;
}
}

View file

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

View file

@ -1,5 +1,4 @@
using System.Threading.Tasks;
using Elsa.Activities.Telnyx.Webhooks.Attributes;
using Microsoft.AspNetCore.Http;
namespace Elsa.Activities.Telnyx.Webhooks.Services

View file

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