diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/AnswerCall.cs b/src/activities/Elsa.Activities.Telnyx/Activities/AnswerCall.cs index 4a4f871f8..b738fe15a 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/AnswerCall.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/AnswerCall.cs @@ -1,8 +1,7 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +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.Design; @@ -52,7 +51,7 @@ namespace Elsa.Activities.Telnyx.Activities private async ValueTask AnswerCallAsync(ActivityExecutionContext context) { - var callControlId = CallControlId is not null and not "" ? CallControlId : context.CorrelationId ?? throw new InvalidOperationException("Cannot answer call without a call control ID"); + var callControlId = context.GetCallControlId(CallControlId); var request = new AnswerCallRequest(BillingGroupId, ClientState, CommandId, WebhookUrl, WebhookUrlMethod); await _telnyxClient.Calls.AnswerCallAsync(callControlId, request, context.CancellationToken); } diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs b/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs index 565d1db60..34657ecca 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs @@ -28,6 +28,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)] + public string? CallControlId + { + get => GetState(); + set => SetState(value); + } + [ActivityProperty] public RingGroupStrategy Strategy { @@ -69,7 +76,8 @@ namespace Elsa.Activities.Telnyx.Activities builder .ForEach(() => Extensions, iterate => iterate .Then(a => a.WithExtension(context => context.GetInput())) - .Then(a => a + .Then(a => a + .WithCallControlAppId(() => CallControlId) .WithTo(context => context.GetInput()) .WithTimeoutSecs(() => (int) RingTime.TotalSeconds) .WithFrom(() => From) @@ -107,7 +115,8 @@ namespace Elsa.Activities.Telnyx.Activities .When("Dial Everyone") .ParallelForEach(() => Extensions, iterate => iterate .Then(a => a.WithExtension(context => context.GetInput())) - .Then(a => a + .Then(a => a + .WithCallControlAppId(() => CallControlId) .WithTo(context => context.GetInput()) .WithTimeoutSecs(() => (int) RingTime.TotalSeconds) .WithFrom(() => From) diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs b/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs index aef5c35a6..f13ff0152 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using Elsa.Activities.Telnyx.Client.Models; using Elsa.Activities.Telnyx.Client.Services; -using Elsa.Activities.Telnyx.Options; +using Elsa.Activities.Telnyx.Extensions; using Elsa.ActivityResults; using Elsa.Attributes; using Elsa.Builders; @@ -25,19 +24,17 @@ namespace Elsa.Activities.Telnyx.Activities public class Dial : Activity { private readonly ITelnyxClient _telnyxClient; - private readonly TelnyxOptions _telnyxOptions; - public Dial(ITelnyxClient telnyxClient, TelnyxOptions telnyxOptions) + public Dial(ITelnyxClient telnyxClient) { _telnyxClient = telnyxClient; - _telnyxOptions = telnyxOptions; } - [ActivityProperty(Label = "Connection ID", Hint = "The ID of the Call Control App (formerly ID of the connection) to be used when dialing the destination.", Category = PropertyCategories.Advanced)] - public string CallControlAppId { get; set; } = default!; + [ActivityProperty(Label = "Call Control ID", Hint = "The ID of the Call Control App (formerly ID of the connection) to be used when dialing the destination.", Category = PropertyCategories.Advanced)] + public string CallControlId { get; set; } = default!; [ActivityProperty(Label = "To", Hint = "The DID or SIP URI to dial out and bridge to the given call.")] - public string To { get; set; } + public string To { get; set; } = default!; [ActivityProperty(Label = "From", 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.")] @@ -90,10 +87,10 @@ namespace Elsa.Activities.Telnyx.Activities private async ValueTask DialAsync(ActivityExecutionContext context) { - var connectionId = CallControlAppId is not null and not "" ? CallControlAppId : _telnyxOptions.CallControlAppId ?? throw new Exception("No Call Control ID specified and no default value configured"); + var callControlId = context.GetCallControlId(CallControlId); var request = new DialRequest( - connectionId, + callControlId, To, From, FromDisplayName, @@ -123,11 +120,11 @@ namespace Elsa.Activities.Telnyx.Activities public static class DialExtensions { - public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlAppId, value); - public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlAppId, value); - public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlAppId, value); - public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlAppId, value); - public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, string? value) => setup.Set(x => x.CallControlAppId, value); + public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, string? value) => setup.Set(x => x.CallControlId, value); public static ISetupActivity WithTo(this ISetupActivity setup, Func> value) => setup.Set(x => x.To, value); public static ISetupActivity WithTo(this ISetupActivity setup, Func value) => setup.Set(x => x.To, value); diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs index 2910cd1b2..bb42ea7c0 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs @@ -1,8 +1,8 @@ using System; -using System.Threading; 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.Design; @@ -86,11 +86,11 @@ namespace Elsa.Activities.Telnyx.Activities EmptyToNull(ValidDigits) ); - var callControlId = CallControlId is not null and not "" ? CallControlId : context.CorrelationId ?? throw new InvalidOperationException("Cannot answer call without a call control ID"); + var callControlId = context.GetCallControlId(CallControlId); try { - await _telnyxClient.Calls.GatherUsingAudiAsync(callControlId, request, context.CancellationToken); + await _telnyxClient.Calls.GatherUsingAudioAsync(callControlId, request, context.CancellationToken); } catch (ApiException e) { diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/HangupCall.cs b/src/activities/Elsa.Activities.Telnyx/Activities/HangupCall.cs index 3a2b0fcfd..141530f3f 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/HangupCall.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/HangupCall.cs @@ -1,8 +1,7 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +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.Design; @@ -45,7 +44,7 @@ namespace Elsa.Activities.Telnyx.Activities private async ValueTask HangupCallAsync(ActivityExecutionContext context) { - var callControlId = CallControlId is not null and not "" ? CallControlId : context.CorrelationId ?? throw new InvalidOperationException("Cannot answer call without a call control ID"); + var callControlId = context.GetCallControlId(CallControlId); var request = new HangupCallRequest(ClientState, CommandId); try diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/ResolveExtension.cs b/src/activities/Elsa.Activities.Telnyx/Activities/ResolveExtension.cs index e8d92b51c..b4475e289 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/ResolveExtension.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/ResolveExtension.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using Elsa.Activities.Telnyx.Services; using Elsa.ActivityResults; diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs b/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs index 5e688f4d9..b6d0aab8c 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; -using System.Threading; 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; @@ -33,7 +34,7 @@ namespace Elsa.Activities.Telnyx.Activities public string CallControlId { get; set; } = default!; [ActivityProperty(Label = "To", Hint = "The DID or SIP URI to dial out and bridge to the given call.")] - public string To { get; set; } + public string To { get; set; } = default!; [ActivityProperty(Label = "From", 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.")] @@ -111,7 +112,7 @@ namespace Elsa.Activities.Telnyx.Activities WebhookUrlMethod ); - var callControlId = CallControlId is not null and not "" ? CallControlId : context.CorrelationId ?? throw new InvalidOperationException("Cannot answer call without a call control ID"); + var callControlId = context.GetCallControlId(CallControlId); try { @@ -123,4 +124,97 @@ namespace Elsa.Activities.Telnyx.Activities } } } + + public static class TransferCallExtensions + { + public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlAppId(this ISetupActivity setup, string? value) => setup.Set(x => x.CallControlId, value); + + public static ISetupActivity WithTo(this ISetupActivity setup, Func> value) => setup.Set(x => x.To, value); + public static ISetupActivity WithTo(this ISetupActivity setup, Func value) => setup.Set(x => x.To, value); + public static ISetupActivity WithTo(this ISetupActivity setup, Func> value) => setup.Set(x => x.To, value); + public static ISetupActivity WithTo(this ISetupActivity setup, Func value) => setup.Set(x => x.To, value); + public static ISetupActivity WithTo(this ISetupActivity setup, string? value) => setup.Set(x => x.To, value); + + public static ISetupActivity WithFrom(this ISetupActivity setup, Func> value) => setup.Set(x => x.From, value); + public static ISetupActivity WithFrom(this ISetupActivity setup, Func value) => setup.Set(x => x.From, value); + public static ISetupActivity WithFrom(this ISetupActivity setup, Func> value) => setup.Set(x => x.From, value); + public static ISetupActivity WithFrom(this ISetupActivity setup, Func value) => setup.Set(x => x.From, value); + public static ISetupActivity WithFrom(this ISetupActivity setup, string? value) => setup.Set(x => x.From, value); + + public static ISetupActivity WithFromDisplayName(this ISetupActivity setup, Func> value) => setup.Set(x => x.FromDisplayName, value); + public static ISetupActivity WithFromDisplayName(this ISetupActivity setup, Func value) => setup.Set(x => x.FromDisplayName, value); + public static ISetupActivity WithFromDisplayName(this ISetupActivity setup, Func> value) => setup.Set(x => x.FromDisplayName, value); + public static ISetupActivity WithFromDisplayName(this ISetupActivity setup, Func value) => setup.Set(x => x.FromDisplayName, value); + public static ISetupActivity WithFromDisplayName(this ISetupActivity setup, string? value) => setup.Set(x => x.FromDisplayName, value); + + public static ISetupActivity WithAnsweringMachineDetection(this ISetupActivity setup, Func> value) => setup.Set(x => x.AnsweringMachineDetection, value); + public static ISetupActivity WithAnsweringMachineDetection(this ISetupActivity setup, Func value) => setup.Set(x => x.AnsweringMachineDetection, value); + public static ISetupActivity WithAnsweringMachineDetection(this ISetupActivity setup, Func> value) => setup.Set(x => x.AnsweringMachineDetection, value); + public static ISetupActivity WithAnsweringMachineDetection(this ISetupActivity setup, Func value) => setup.Set(x => x.AnsweringMachineDetection, value); + public static ISetupActivity WithAnsweringMachineDetection(this ISetupActivity setup, string? value) => setup.Set(x => x.AnsweringMachineDetection, value); + + public static ISetupActivity WithAnsweringMachineDetectionConfig(this ISetupActivity setup, Func> value) => setup.Set(x => x.AnsweringMachineDetectionConfig, value); + public static ISetupActivity WithAnsweringMachineDetectionConfig(this ISetupActivity setup, Func value) => setup.Set(x => x.AnsweringMachineDetectionConfig, value); + public static ISetupActivity WithAnsweringMachineDetectionConfig(this ISetupActivity setup, Func> value) => setup.Set(x => x.AnsweringMachineDetectionConfig, value); + public static ISetupActivity WithAnsweringMachineDetectionConfig(this ISetupActivity setup, Func value) => setup.Set(x => x.AnsweringMachineDetectionConfig, value); + public static ISetupActivity WithAnsweringMachineDetectionConfig(this ISetupActivity setup, AnsweringMachineConfig? value) => setup.Set(x => x.AnsweringMachineDetectionConfig, 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 WithCustomHeaders(this ISetupActivity setup, Func?>> value) => setup.Set(x => x.CustomHeaders, value); + public static ISetupActivity WithCustomHeaders(this ISetupActivity setup, Func?> value) => setup.Set(x => x.CustomHeaders, value); + public static ISetupActivity WithCustomHeaders(this ISetupActivity setup, Func?>> value) => setup.Set(x => x.CustomHeaders, value); + public static ISetupActivity WithCustomHeaders(this ISetupActivity setup, Func?> value) => setup.Set(x => x.CustomHeaders, value); + public static ISetupActivity WithCustomHeaders(this ISetupActivity setup, IList
? value) => setup.Set(x => x.CustomHeaders, value); + + public static ISetupActivity WithSipAuthUsername(this ISetupActivity setup, Func> value) => setup.Set(x => x.SipAuthUsername, value); + public static ISetupActivity WithSipAuthUsername(this ISetupActivity setup, Func value) => setup.Set(x => x.SipAuthUsername, value); + public static ISetupActivity WithSipAuthUsername(this ISetupActivity setup, Func> value) => setup.Set(x => x.SipAuthUsername, value); + public static ISetupActivity WithSipAuthUsername(this ISetupActivity setup, Func value) => setup.Set(x => x.SipAuthUsername, value); + public static ISetupActivity WithSipAuthUsername(this ISetupActivity setup, string? value) => setup.Set(x => x.SipAuthUsername, value); + + public static ISetupActivity WithSipAuthPassword(this ISetupActivity setup, Func> value) => setup.Set(x => x.SipAuthPassword, value); + public static ISetupActivity WithSipAuthPassword(this ISetupActivity setup, Func value) => setup.Set(x => x.SipAuthPassword, value); + public static ISetupActivity WithSipAuthPassword(this ISetupActivity setup, Func> value) => setup.Set(x => x.SipAuthPassword, value); + public static ISetupActivity WithSipAuthPassword(this ISetupActivity setup, Func value) => setup.Set(x => x.SipAuthPassword, value); + public static ISetupActivity WithSipAuthPassword(this ISetupActivity setup, string? value) => setup.Set(x => x.SipAuthPassword, value); + + public static ISetupActivity WithTimeLimitSecs(this ISetupActivity setup, Func> value) => setup.Set(x => x.TimeLimitSecs, value); + public static ISetupActivity WithTimeLimitSecs(this ISetupActivity setup, Func value) => setup.Set(x => x.TimeLimitSecs, value); + public static ISetupActivity WithTimeLimitSecs(this ISetupActivity setup, Func> value) => setup.Set(x => x.TimeLimitSecs, value); + public static ISetupActivity WithTimeLimitSecs(this ISetupActivity setup, Func value) => setup.Set(x => x.TimeLimitSecs, value); + public static ISetupActivity WithTimeLimitSecs(this ISetupActivity setup, int? value) => setup.Set(x => x.TimeLimitSecs, value); + + public static ISetupActivity WithTimeoutSecs(this ISetupActivity setup, Func> value) => setup.Set(x => x.TimeoutSecs, value); + public static ISetupActivity WithTimeoutSecs(this ISetupActivity setup, Func value) => setup.Set(x => x.TimeoutSecs, value); + public static ISetupActivity WithTimeoutSecs(this ISetupActivity setup, Func> value) => setup.Set(x => x.TimeoutSecs, value); + public static ISetupActivity WithTimeoutSecs(this ISetupActivity setup, Func value) => setup.Set(x => x.TimeoutSecs, value); + public static ISetupActivity WithTimeoutSecs(this ISetupActivity setup, int? value) => setup.Set(x => x.TimeoutSecs, value); + + public static ISetupActivity WithWebhookUrl(this ISetupActivity setup, Func> value) => setup.Set(x => x.WebhookUrl, value); + public static ISetupActivity WithWebhookUrl(this ISetupActivity setup, Func value) => setup.Set(x => x.WebhookUrl, value); + public static ISetupActivity WithWebhookUrl(this ISetupActivity setup, Func> value) => setup.Set(x => x.WebhookUrl, value); + public static ISetupActivity WithWebhookUrl(this ISetupActivity setup, Func value) => setup.Set(x => x.WebhookUrl, value); + public static ISetupActivity WithWebhookUrl(this ISetupActivity setup, string? value) => setup.Set(x => x.WebhookUrl, value); + + public static ISetupActivity WithWebhookUrlMethod(this ISetupActivity setup, Func> value) => setup.Set(x => x.WebhookUrlMethod, value); + public static ISetupActivity WithWebhookUrlMethod(this ISetupActivity setup, Func value) => setup.Set(x => x.WebhookUrlMethod, value); + public static ISetupActivity WithWebhookUrlMethod(this ISetupActivity setup, Func> value) => setup.Set(x => x.WebhookUrlMethod, value); + public static ISetupActivity WithWebhookUrlMethod(this ISetupActivity setup, Func value) => setup.Set(x => x.WebhookUrlMethod, value); + public static ISetupActivity WithWebhookUrlMethod(this ISetupActivity setup, string? value) => setup.Set(x => x.WebhookUrlMethod, value); + } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/ActivityTypes/NotificationActivityTypeProvider.cs b/src/activities/Elsa.Activities.Telnyx/ActivityTypes/NotificationActivityTypeProvider.cs index d1cac7fe8..be5dd7785 100644 --- a/src/activities/Elsa.Activities.Telnyx/ActivityTypes/NotificationActivityTypeProvider.cs +++ b/src/activities/Elsa.Activities.Telnyx/ActivityTypes/NotificationActivityTypeProvider.cs @@ -7,18 +7,28 @@ using System.Threading.Tasks; using Elsa.Activities.Telnyx.Webhooks.Attributes; using Elsa.Activities.Telnyx.Webhooks.Models; using Elsa.Activities.Telnyx.Webhooks.Payloads.Abstract; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; +using Elsa.Activities.Telnyx.Webhooks.Services; using Elsa.ActivityProviders; using Elsa.ActivityResults; using Elsa.Metadata; using Elsa.Services.Models; +using Microsoft.AspNetCore.Authorization.Policy; namespace Elsa.Activities.Telnyx.ActivityTypes { - public class NotificationActivityTypeProvider : IActivityTypeProvider + internal class NotificationActivityTypeProvider : IActivityTypeProvider { + private readonly IWebhookFilterService _webhookFilterService; + + public NotificationActivityTypeProvider(IWebhookFilterService webhookFilterService) + { + _webhookFilterService = webhookFilterService; + } + public const string NotificationAttribute = "TelnyxNotification"; public const string EventTypeAttribute = "EventType"; - + public ValueTask> GetActivityTypesAsync(CancellationToken cancellationToken = default) { var activityTypes = GetActivityTypes(); @@ -27,20 +37,31 @@ namespace Elsa.Activities.Telnyx.ActivityTypes private IEnumerable GetActivityTypes() { - var types = GetType().Assembly.GetAllWithBaseClass().Where(x => x.GetCustomAttribute() != null); - return types.Select(CreateWebhookActivityType); + var payloadTypes = GetType().Assembly.GetAllWithBaseClass().Where(x => x.GetCustomAttribute() != null).ToList(); + 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."))); + + return activityTypes; } - + private static ActivityType CreateWebhookActivityType(Type payloadType) { - var payloadAttribute = payloadType.GetCustomAttribute(); + var webhookAttribute = payloadType.GetCustomAttribute(); - if (payloadAttribute == null) - throw new InvalidOperationException($"Make sure that the payload type is annotated with the ${nameof(PayloadAttribute)} attribute"); + if (webhookAttribute == null) + throw new InvalidOperationException($"Make sure that the payload type is annotated with the ${nameof(WebhookAttribute)} attribute"); - var typeName = payloadAttribute.ActivityType; - var displayName = payloadAttribute.DisplayName; - var description = payloadAttribute.Description; + return CreateWebhookActivityType(webhookAttribute); + } + + private static ActivityType CreateWebhookActivityType(WebhookAttribute webhookAttribute) + { + var typeName = webhookAttribute.ActivityType; + var displayName = webhookAttribute.DisplayName; + var description = webhookAttribute.Description; return new ActivityType { @@ -59,18 +80,24 @@ namespace Elsa.Activities.Telnyx.ActivityTypes Attributes = new Dictionary { [NotificationAttribute] = true, - [EventTypeAttribute] = payloadAttribute.EventType + [EventTypeAttribute] = webhookAttribute.EventType }, CanExecuteAsync = _ => new ValueTask(true), ExecuteAsync = context => context.WorkflowExecutionContext.IsFirstPass ? ExecuteInternal(context) : new ValueTask(new SuspendResult()), ResumeAsync = ExecuteInternal, }; } - + private static ValueTask ExecuteInternal(ActivityExecutionContext context) { - var webhook = (TelnyxWebhook) context.Input!; - context.WorkflowExecutionContext.CorrelationId ??= (webhook.Data.Payload as ICorrelationId)?.CorrelationId; + var webhook = (TelnyxWebhook) context.Input!; + + 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/Services/ICallsApi.cs b/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs index 6a1ac000c..bfbb8032e 100644 --- a/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs +++ b/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs @@ -20,6 +20,6 @@ namespace Elsa.Activities.Telnyx.Client.Services Task HangupCallAsync(string callControlId, [Body]HangupCallRequest request, CancellationToken cancellationToken = default); [Post("/v2/calls/{callControlId}/actions/gather_using_audio")] - Task GatherUsingAudiAsync(string callControlId, [Body]GatherUsingAudioRequest request, CancellationToken cancellationToken = default); + Task GatherUsingAudioAsync(string callControlId, [Body]GatherUsingAudioRequest request, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Extensions/ActivityExecutionContextExtensions.cs b/src/activities/Elsa.Activities.Telnyx/Extensions/ActivityExecutionContextExtensions.cs new file mode 100644 index 000000000..34e6ca247 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Extensions/ActivityExecutionContextExtensions.cs @@ -0,0 +1,31 @@ +using System; +using Elsa.Activities.Telnyx.Options; +using Elsa.Services.Models; + +namespace Elsa.Activities.Telnyx.Extensions +{ + public static class ActivityExecutionContextExtensions + { + /// + /// Returns the default call control app ID if the specified value is null or empty. + /// + public static string GetCallControlId(this ActivityExecutionContext context, string? callControlId) + { + if (!string.IsNullOrWhiteSpace(callControlId)) + return callControlId; + + callControlId = context.GetVariable("CallControlId"); + + if (!string.IsNullOrWhiteSpace(callControlId)) + return callControlId; + + var options = context.GetService(); + callControlId = options.CallControlAppId; + + if (!string.IsNullOrWhiteSpace(callControlId)) + return callControlId; + + throw new Exception("No Call Control ID specified and no default value configured"); + } + } +} \ 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 b4482c0c9..014679729 100644 --- a/src/activities/Elsa.Activities.Telnyx/Extensions/SetupExtensions.cs +++ b/src/activities/Elsa.Activities.Telnyx/Extensions/SetupExtensions.cs @@ -9,9 +9,9 @@ using Elsa.Activities.Telnyx.ActivityTypes; using Elsa.Activities.Telnyx.Bookmarks; using Elsa.Activities.Telnyx.Client.Services; using Elsa.Activities.Telnyx.Options; -using Elsa.Activities.Telnyx.Services; using Elsa.Activities.Telnyx.Webhooks.Consumers; using Elsa.Activities.Telnyx.Webhooks.Events; +using Elsa.Activities.Telnyx.Webhooks.Filters; using Elsa.Activities.Telnyx.Webhooks.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -46,6 +46,9 @@ namespace Elsa.Activities.Telnyx.Extensions .AddBookmarkProvider() .AddNotificationHandlers(typeof(TriggerWorkflows)) .AddScoped() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddScoped(telnyxOptions.ExtensionProviderFactory); // Telnyx API Client. diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Attributes/PayloadAttribute.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Attributes/WebhookAttribute.cs similarity index 82% rename from src/activities/Elsa.Activities.Telnyx/Webhooks/Attributes/PayloadAttribute.cs rename to src/activities/Elsa.Activities.Telnyx/Webhooks/Attributes/WebhookAttribute.cs index e9a8d11e7..f36a7c24e 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Attributes/PayloadAttribute.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Attributes/WebhookAttribute.cs @@ -3,9 +3,9 @@ namespace Elsa.Activities.Telnyx.Webhooks.Attributes { [AttributeUsage(AttributeTargets.Class)] - public class PayloadAttribute : Attribute + public class WebhookAttribute : Attribute { - public PayloadAttribute(string eventType, string activityType, string displayName, string description) + public WebhookAttribute(string eventType, string activityType, string displayName, string description) { EventType = eventType; ActivityType = activityType; diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Consumers/TriggerWorkflows.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Consumers/TriggerWorkflows.cs index ca4bf99a0..df48bd90b 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Consumers/TriggerWorkflows.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Consumers/TriggerWorkflows.cs @@ -1,10 +1,14 @@ 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.Webhooks.Events; using Elsa.Activities.Telnyx.Webhooks.Payloads.Abstract; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; +using Elsa.Activities.Telnyx.Webhooks.Services; using Elsa.DistributedLock; using Elsa.Models; using Elsa.Persistence; @@ -24,14 +28,22 @@ namespace Elsa.Activities.Telnyx.Webhooks.Consumers private readonly IWorkflowInstanceStore _workflowInstanceStore; private readonly IDistributedLockProvider _distributedLockProvider; private readonly ICommandSender _commandSender; + private readonly IWebhookFilterService _webhookFilterService; private readonly ILogger _logger; - public TriggerWorkflows(IWorkflowRunner workflowRunner, IWorkflowInstanceStore workflowInstanceStore, IDistributedLockProvider distributedLockProvider, ICommandSender commandSender, ILogger logger) + public TriggerWorkflows( + IWorkflowRunner workflowRunner, + IWorkflowInstanceStore workflowInstanceStore, + IDistributedLockProvider distributedLockProvider, + ICommandSender commandSender, + IWebhookFilterService webhookFilterService, + ILogger logger) { _workflowRunner = workflowRunner; _workflowInstanceStore = workflowInstanceStore; _distributedLockProvider = distributedLockProvider; _commandSender = commandSender; + _webhookFilterService = webhookFilterService; _logger = logger; } @@ -40,18 +52,17 @@ namespace Elsa.Activities.Telnyx.Webhooks.Consumers var webhook = message.Webhook; var eventType = webhook.Data.EventType; var payload = message.Webhook.Data.Payload; - var payloadAttribute = payload.GetType().GetCustomAttribute(); + var activityType = _webhookFilterService.GetActivityTypeName(payload); - if (payloadAttribute == null) + if (activityType == null) { - _logger.LogWarning("The received payload type '{PayloadTypeName}' does not have a PayloadAttribute containing metadata, which means it's an unsupported event", payload.GetType().Name); + _logger.LogWarning("The received event '{EventType}' is an unsupported event", webhook.Data.EventType); return; } - var activityType = payloadAttribute.ActivityType; - var correlationId = (payload as ICorrelationId)?.CorrelationId; + var correlationId = GetCorrelationId(payload); var lockKey = $"telnyx:trigger-workflows:correlation-{correlationId}"; - + if (!await _distributedLockProvider.AcquireLockAsync(lockKey)) { _logger.LogDebug("Lock {LockKey} already taken", lockKey); @@ -75,5 +86,13 @@ namespace Elsa.Activities.Telnyx.Webhooks.Consumers await _distributedLockProvider.ReleaseLockAsync(lockKey); } } + + private string GetCorrelationId(Payload payload) + { + if (payload is CallPayload callPayload) + return callPayload.CallSessionId; + + throw new NotSupportedException($"The received payload type {payload.GetType().Name} is not supported yet."); + } } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Converters/TelnyxWebhookDataJsonConverter.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Converters/TelnyxWebhookDataJsonConverter.cs index 8eb576383..a8a66308f 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Converters/TelnyxWebhookDataJsonConverter.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Converters/TelnyxWebhookDataJsonConverter.cs @@ -19,7 +19,7 @@ namespace Elsa.Activities.Telnyx.Webhooks.Converters var query = from payloadType in payloadTypes - let payloadAttribute = payloadType.GetCustomAttribute() + let payloadAttribute = payloadType.GetCustomAttribute() where payloadAttribute != null select (payloadType, payloadAttribute); diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Filters/AttributeBasedWebhookFilter.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Filters/AttributeBasedWebhookFilter.cs new file mode 100644 index 000000000..26a420f0c --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Filters/AttributeBasedWebhookFilter.cs @@ -0,0 +1,14 @@ +using System.Reflection; +using Elsa.Activities.Telnyx.Webhooks.Attributes; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Abstract; +using Elsa.Activities.Telnyx.Webhooks.Services; + +namespace Elsa.Activities.Telnyx.Webhooks.Filters +{ + public class AttributeBasedWebhookFilter : IWebhookFilter + { + public int Priority => 0; + public bool CanHandlePayload(Payload payload) => payload.GetType().GetCustomAttribute() != null; + public string GetActivityTypeName(Payload payload) => payload.GetType().GetCustomAttribute()!.ActivityType; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Filters/HangupWebhookFilter.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Filters/HangupWebhookFilter.cs new file mode 100644 index 000000000..65361c8b6 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Filters/HangupWebhookFilter.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 HangupWebhookFilter : IWebhookFilter + { + public int Priority => 1; + + public bool CanHandlePayload(Payload payload) => payload is CallHangupPayload; + + public string GetActivityTypeName(Payload payload) + { + var hangupPayload = (CallHangupPayload) payload; + var attribute = payload.GetType().GetCustomAttribute()!; + + if (hangupPayload.HangupCause != "originator_cancel") + return attribute.ActivityType; + + return "OriginatorCallHangup"; + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Abstract/ICorrelationId.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Abstract/ICorrelationId.cs deleted file mode 100644 index bcc51344b..000000000 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Abstract/ICorrelationId.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Abstract -{ - public interface ICorrelationId - { - string CorrelationId { get; } - } -} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallAnsweredPayload.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallAnsweredPayload.cs index a33b799f2..7e76ea0f1 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallAnsweredPayload.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallAnsweredPayload.cs @@ -2,7 +2,7 @@ namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call { - [Payload(EventType, ActivityTypeName, "Call Answered", "Triggered when an incoming call is answered.")] + [Webhook(EventType, ActivityTypeName, "Call Answered", "Triggered when an incoming call is answered.")] public sealed record CallAnsweredPayload : CallPayload { public const string EventType = "call.answered"; diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallDtmfReceivedPayload.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallDtmfReceivedPayload.cs index 469814107..a7388e9ed 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallDtmfReceivedPayload.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallDtmfReceivedPayload.cs @@ -2,7 +2,7 @@ namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call { - [Payload(EventType, ActivityTypeName, "Call DTMF Received", "Triggered when DTMF input is received.")] + [Webhook(EventType, ActivityTypeName, "Call DTMF Received", "Triggered when DTMF input is received.")] public sealed record CallDtmfReceivedPayload : CallPayload { public const string EventType = "call.dtmf.received"; diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallGatherEndedPayload.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallGatherEndedPayload.cs index 3059e279c..13b93d222 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallGatherEndedPayload.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallGatherEndedPayload.cs @@ -2,7 +2,7 @@ namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call { - [Payload(EventType, ActivityTypeName, "Call Gather Ended", "Triggered when an call gather has ended.")] + [Webhook(EventType, ActivityTypeName, "Call Gather Ended", "Triggered when an call gather has ended.")] public sealed record CallGatherEndedPayload : CallPayload { public const string EventType = "call.gather.ended"; diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallHangupPayload.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallHangupPayload.cs index 327bf925a..63c9b1285 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallHangupPayload.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallHangupPayload.cs @@ -3,7 +3,7 @@ using NodaTime; namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call { - [Payload(EventType, ActivityTypeName, "Call Hangup", "Triggered when an incoming call was hangup.")] + [Webhook(EventType, ActivityTypeName, "Call Hangup", "Triggered when an incoming call was hangup.")] public sealed record CallHangupPayload : CallPayload { public const string EventType = "call.hangup"; 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 72cb6f51b..5b647d9b5 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallInitiatedPayload.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallInitiatedPayload.cs @@ -2,7 +2,7 @@ namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call { - [Payload(EventType, ActivityTypeName, "Call Initiated", "Triggered when an incoming call is received.")] + [Webhook(EventType, ActivityTypeName, "Call Initiated", "Triggered when an incoming call is received.")] public sealed record CallInitiatedPayload : CallPayload { public const string EventType = "call.initiated"; diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPayload.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPayload.cs index 6412c8e9c..ca48dfbdc 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPayload.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPayload.cs @@ -2,12 +2,11 @@ namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call { - public abstract record CallPayload : Payload, ICorrelationId + public abstract record CallPayload : Payload { public string CallControlId { get; init; } = default!; public string CallLegId { get; init; } = default!; public string CallSessionId { get; init; } = default!; public string ConnectionId { get; init; } = default!; - string ICorrelationId.CorrelationId => CallControlId; } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPlaybackEndedPayload.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPlaybackEndedPayload.cs index 2b02503a7..872f1aa9a 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPlaybackEndedPayload.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPlaybackEndedPayload.cs @@ -2,7 +2,7 @@ namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call { - [Payload(EventType, ActivityTypeName, "Call Playback Ended", "Triggered when an audio playback has ended.")] + [Webhook(EventType, ActivityTypeName, "Call Playback Ended", "Triggered when an audio playback has ended.")] public sealed record CallPlaybackEndedPayload : CallPlayback { public const string EventType = "call.playback.ended"; diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPlaybackStartedPayload.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPlaybackStartedPayload.cs index e17e24f90..2910c29b6 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPlaybackStartedPayload.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallPlaybackStartedPayload.cs @@ -2,7 +2,7 @@ namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call { - [Payload(EventType, ActivityTypeName, "Call Playback Started", "Triggered when an audio playback has started.")] + [Webhook(EventType, ActivityTypeName, "Call Playback Started", "Triggered when an audio playback has started.")] public sealed record CallPlaybackStartedPayload : CallPlayback { public const string EventType = "call.playback.started"; diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookFilter.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookFilter.cs new file mode 100644 index 000000000..d2b39bd2e --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookFilter.cs @@ -0,0 +1,12 @@ +using Elsa.Activities.Telnyx.Webhooks.Attributes; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Abstract; + +namespace Elsa.Activities.Telnyx.Webhooks.Services +{ + internal interface IWebhookFilter + { + int Priority { get; } + bool CanHandlePayload(Payload payload); + string GetActivityTypeName(Payload payload); + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookFilterService.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookFilterService.cs new file mode 100644 index 000000000..08c7794e0 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookFilterService.cs @@ -0,0 +1,9 @@ +using Elsa.Activities.Telnyx.Webhooks.Payloads.Abstract; + +namespace Elsa.Activities.Telnyx.Webhooks.Services +{ + internal interface IWebhookFilterService + { + string? GetActivityTypeName(Payload payload); + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookHandler.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookHandler.cs index fdf49e6d6..f0a5e8aee 100644 --- a/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookHandler.cs +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/IWebhookHandler.cs @@ -1,4 +1,5 @@ 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 new file mode 100644 index 000000000..9a23f6ff4 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Services/WebhookFilterService.cs @@ -0,0 +1,23 @@ +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 +{ + internal class WebhookFilterService : IWebhookFilterService + { + private readonly IEnumerable _filters; + + public WebhookFilterService(IEnumerable filters) + { + _filters = filters.OrderByDescending(x => x.Priority).ToList(); + } + + public string? GetActivityTypeName(Payload payload) + { + var filter = _filters.FirstOrDefault(x => x.CanHandlePayload(payload)); + return filter?.GetActivityTypeName(payload); + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/ControlFlow/ForEach/ForEach.cs b/src/core/Elsa.Core/Activities/ControlFlow/ForEach/ForEach.cs index e6b6be159..c4fe247ad 100644 --- a/src/core/Elsa.Core/Activities/ControlFlow/ForEach/ForEach.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/ForEach/ForEach.cs @@ -43,7 +43,7 @@ namespace Elsa.Activities.ControlFlow var collection = Items.ToList(); var currentIndex = CurrentIndex ?? 0; - if (currentIndex < collection.Count - 1) + if (currentIndex < collection.Count) { var currentValue = collection[currentIndex]; var scope = context.CreateScope(); diff --git a/src/core/Elsa.Core/Activities/ControlFlow/ForEach/ForEachBuilderExtensions.cs b/src/core/Elsa.Core/Activities/ControlFlow/ForEach/ForEachBuilderExtensions.cs index 76d87bb3f..a081e45d6 100644 --- a/src/core/Elsa.Core/Activities/ControlFlow/ForEach/ForEachBuilderExtensions.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/ForEach/ForEachBuilderExtensions.cs @@ -33,7 +33,7 @@ namespace Elsa.Activities.ControlFlow Action iterate, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => - builder.ForEach(activity => activity.Set(x => x.Items, () => items().Cast()), iterate, lineNumber, sourceFile); + builder.ForEach(activity => activity.Set(x => x.Items, () => items().Select(x => (object)x!).ToList()), iterate, lineNumber, sourceFile); public static IActivityBuilder ForEach( this IBuilder builder, diff --git a/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs b/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs index 8054c3a10..845153c5e 100644 --- a/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs +++ b/src/core/Elsa.Core/Builders/CompositeActivityBuilder.cs @@ -16,6 +16,17 @@ namespace Elsa.Builders private readonly Func _compositeActivityBuilderFactory; private readonly IGetsStartActivitiesForCompositeActivityBlueprint _startingActivitiesProvider; + internal CompositeActivityBuilder( + IServiceProvider serviceProvider, + IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider, + Type activityType, + string activityTypeName) : this(serviceProvider, startingActivitiesProvider) + { + ActivityType = activityType; + ActivityTypeName = activityTypeName; + WorkflowBuilder = this; + } + public CompositeActivityBuilder(IServiceProvider serviceProvider, IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) { _startingActivitiesProvider = startingActivitiesProvider ?? throw new ArgumentNullException(nameof(startingActivitiesProvider)); @@ -106,7 +117,7 @@ namespace Elsa.Builders [CallerFilePath] string? sourceFile = default) where T : class, IActivity { - var activityBuilder = new ActivityBuilder( typeof(T), activityTypeName, this, propertyValueProviders, lineNumber, sourceFile); + var activityBuilder = new ActivityBuilder(typeof(T), activityTypeName, this, propertyValueProviders, lineNumber, sourceFile); return Add(activityBuilder, branch); } @@ -119,12 +130,18 @@ namespace Elsa.Builders return activityBuilder; } - public override IActivityBuilder Then(string activityTypeName, Action>? setup = null, Action? branch = null, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => - StartWith(activityTypeName,setup, branch, lineNumber, sourceFile); + public override IActivityBuilder Then( + string activityTypeName, + Action>? setup = null, + Action? branch = null, + [CallerLineNumber] int lineNumber = default, + [CallerFilePath] string? sourceFile = default) => + StartWith(activityTypeName, setup, branch, lineNumber, sourceFile); public override IActivityBuilder Then(IActivityBuilder targetActivity) => Add(targetActivity); - public override IActivityBuilder Then(string activityTypeName, Action? branch = null, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => StartWith(activityTypeName, branch, lineNumber, sourceFile); + public override IActivityBuilder Then(string activityTypeName, Action? branch = null, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => + StartWith(activityTypeName, branch, lineNumber, sourceFile); public IConnectionBuilder Connect( IActivityBuilder source, @@ -147,7 +164,13 @@ namespace Elsa.Builders { var compositeActivityBlueprint = new CompositeActivityBlueprint { - Id = ActivityId + Id = ActivityId, + Name = Name, + Type = ActivityTypeName, + PersistOutput = PersistOutputEnabled, + PersistWorkflow = PersistWorkflowEnabled, + LoadWorkflowContext = LoadWorkflowContextEnabled, + SaveWorkflowContext = SaveWorkflowContextEnabled }; var activityBuilders = ActivityBuilders.ToList(); diff --git a/src/core/Elsa.Core/Expressions/LiteralHandler.cs b/src/core/Elsa.Core/Expressions/LiteralHandler.cs index 117183a22..c18c7a7ce 100644 --- a/src/core/Elsa.Core/Expressions/LiteralHandler.cs +++ b/src/core/Elsa.Core/Expressions/LiteralHandler.cs @@ -20,6 +20,9 @@ namespace Elsa.Expressions if (returnType == typeof(string)) return Task.FromResult(expression); + if (string.IsNullOrWhiteSpace(expression)) + return Task.FromResult((object?) null); + var converter = TypeDescriptor.GetConverter(returnType); var value = converter.CanConvertFrom(typeof(string)) ? converter.ConvertFrom(expression) : default; return Task.FromResult(value)!; diff --git a/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs b/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs index 70aea738d..aef6e5e15 100644 --- a/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs +++ b/src/core/Elsa.Core/Services/WorkflowBlueprintMaterializer.cs @@ -1,10 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Elsa.ActivityProviders; +using Elsa.Builders; using Elsa.Models; using Elsa.Services.Models; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NetBox.Extensions; @@ -13,14 +17,18 @@ namespace Elsa.Services public class WorkflowBlueprintMaterializer : IWorkflowBlueprintMaterializer { private readonly IActivityTypeService _activityTypeService; + private readonly IGetsStartActivitiesForCompositeActivityBlueprint _startingActivitiesProvider; + private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - private readonly IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider; - public WorkflowBlueprintMaterializer(IActivityTypeService activityTypeService, - ILogger logger, - IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider) + public WorkflowBlueprintMaterializer( + IActivityTypeService activityTypeService, + IGetsStartActivitiesForCompositeActivityBlueprint startingActivitiesProvider, + IServiceProvider serviceProvider, + ILogger logger) { - this.startingActivitiesProvider = startingActivitiesProvider ?? throw new System.ArgumentNullException(nameof(startingActivitiesProvider)); + _startingActivitiesProvider = startingActivitiesProvider; + _serviceProvider = serviceProvider; _activityTypeService = activityTypeService; _logger = logger; } @@ -105,7 +113,8 @@ namespace Elsa.Services private async Task> CreateBlueprintsAsync(ActivityDefinition activityDefinition, CancellationToken cancellationToken) { var list = new List(); - + var activityType = await _activityTypeService.GetActivityTypeAsync(activityDefinition.Type, cancellationToken); + if (activityDefinition is CompositeActivityDefinition compositeActivityDefinition) { var manyActivityBlueprints = await Task.WhenAll(compositeActivityDefinition.Activities.Select(async x => await CreateBlueprintsAsync(x, cancellationToken))); @@ -130,7 +139,33 @@ namespace Elsa.Services list.Add(compositeActivityBlueprint); // Connect the composite activity to its starting activities. - var startActivities = startingActivitiesProvider.GetStartActivities(compositeActivityBlueprint).ToList(); + var startActivities = _startingActivitiesProvider.GetStartActivities(compositeActivityBlueprint).ToList(); + compositeActivityBlueprint.Connections.AddRange(startActivities.Select(x => new Connection(compositeActivityBlueprint, x, CompositeActivity.Enter))); + } + else if (typeof(CompositeActivity).IsAssignableFrom(activityType.Type)) + { + var compositeActivity = (CompositeActivity) ActivatorUtilities.CreateInstance(_serviceProvider, activityType.Type); + var compositeActivityBuilder = new CompositeActivityBuilder(_serviceProvider, _startingActivitiesProvider, activityType.Type, activityType.TypeName) + { + ActivityId = activityDefinition.ActivityId, + Name = activityDefinition.Name, + DisplayName = activityDefinition.DisplayName, + PersistOutputEnabled = activityDefinition.PersistOutput, + PersistWorkflowEnabled = activityDefinition.PersistWorkflow, + LoadWorkflowContextEnabled = activityDefinition.LoadWorkflowContext, + SaveWorkflowContextEnabled = activityDefinition.SaveWorkflowContext, + Description = activityDefinition.Description + }; + + compositeActivity.Build(compositeActivityBuilder); + + var compositeActivityBlueprint = compositeActivityBuilder.Build($"{activityDefinition.ActivityId}:activity"); + + list.Add(compositeActivityBlueprint); + list.AddRange(compositeActivityBlueprint.Activities); + + // Connect the composite activity to its starting activities. + var startActivities = _startingActivitiesProvider.GetStartActivities(compositeActivityBlueprint).ToList(); compositeActivityBlueprint.Connections.AddRange(startActivities.Select(x => new Connection(compositeActivityBlueprint, x, CompositeActivity.Enter))); } else diff --git a/src/samples/dashboard/ElsaDashboard.Samples.Monolith/Startup.cs b/src/samples/dashboard/ElsaDashboard.Samples.Monolith/Startup.cs index 8ee158610..fb2e63fcf 100644 --- a/src/samples/dashboard/ElsaDashboard.Samples.Monolith/Startup.cs +++ b/src/samples/dashboard/ElsaDashboard.Samples.Monolith/Startup.cs @@ -39,7 +39,7 @@ namespace ElsaDashboard.Samples.Monolith .AddQuartzTemporalActivities() .AddTelnyx(Configuration.GetSection("Telnyx").Bind) .AddJavaScriptActivities() - .AddWorkflowsFrom() + //.AddWorkflowsFrom() ); services diff --git a/src/samples/dashboard/ElsaDashboard.Samples.Monolith/appsettings.json b/src/samples/dashboard/ElsaDashboard.Samples.Monolith/appsettings.json index e4e987a36..92aafff68 100644 --- a/src/samples/dashboard/ElsaDashboard.Samples.Monolith/appsettings.json +++ b/src/samples/dashboard/ElsaDashboard.Samples.Monolith/appsettings.json @@ -22,6 +22,7 @@ }, "Telnyx": { "ApiUrl": "", - "ApiKey": "" + "ApiKey": "", + "CallControlAppId": "" } } \ No newline at end of file