Merge remote-tracking branch 'upstream/master' into elsa-2.0/fileactivities

This commit is contained in:
Cameron Cowen 2021-06-06 10:07:40 +01:00
commit b3cf8dda72
39 changed files with 529 additions and 222 deletions

View file

@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Elsa.Activities.Telnyx.Client.Models;
using Elsa.Activities.Telnyx.Client.Services;
using Elsa.Activities.Telnyx.Extensions;
using Elsa.Activities.Telnyx.Webhooks.Payloads.Call;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Builders;
@ -18,7 +21,7 @@ namespace Elsa.Activities.Telnyx.Activities
[Action(
Category = Constants.Category,
Description = "Bridge two call control calls.",
Outcomes = new[] { OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive },
Outcomes = new[] {TelnyxOutcomeNames.Bridging, TelnyxOutcomeNames.Bridged, TelnyxOutcomeNames.LegABridged, TelnyxOutcomeNames.LegBBridged, OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive},
DisplayName = "Bridge Calls"
)]
public class BridgeCalls : Activity
@ -30,40 +33,52 @@ namespace Elsa.Activities.Telnyx.Activities
_telnyxClient = telnyxClient;
}
[ActivityInput(Label = "Call Control ID A", Hint = "Unique identifier and token for controlling the call.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? CallControlIdA { get; set; } = default!;
[ActivityInput(Label = "Call Control ID A", Hint = "Unique identifier and token for controlling the call.", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string? CallControlIdA { get; set; }
[ActivityInput(Label = "Call Control ID B", Hint = "The Call Control ID of the call you want to bridge with.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string CallControlIdB { get; set; } = default!;
[ActivityInput(Label = "Call Control ID B", Hint = "The Call Control ID of the call you want to bridge with.", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string? CallControlIdB { get; set; }
[ActivityInput(
Label = "Command ID",
Hint = "Use this field to avoid duplicate commands. Telnyx will ignore commands with the same Command ID.",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string? CommandId { get; set; }
[ActivityInput(
Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.",
Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string? ClientState { get; set; }
[ActivityInput(
Label = "Park After Unbridged",
Hint = "HTTP request type used for Webhook URL",
UIHint = ActivityInputUIHints.Dropdown,
Options = new[] { "", "self" },
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
Label = "Park After Unbridged",
Hint = "HTTP request type used for Webhook URL",
UIHint = ActivityInputUIHints.Dropdown,
Options = new[] {"", "self"},
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string? ParkAfterUnbridged { get; set; }
[ActivityOutput] public CallBridgedPayload? CallBridgedPayloadA { get; set; }
[ActivityOutput] public CallBridgedPayload? CallBridgedPayloadB { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
var callControlIdA = context.GetCallControlId(CallControlIdA);
CallBridgedPayloadA = null;
CallBridgedPayloadB = null;
var callControlIdA = CallControlIdA = context.GetCallControlId(CallControlIdA);
var callControlIdB = CallControlIdB = GetCallControlB(context);
if (callControlIdB == null)
throw new WorkflowException("Cannot bridge calls because the second leg's call control ID was not specified and no incoming activities provided this value");
CallControlIdA = callControlIdA;
var request = new BridgeCallsRequest(
CallControlIdB,
callControlIdB,
ClientState,
CommandId,
ParkAfterUnbridged
@ -72,7 +87,7 @@ namespace Elsa.Activities.Telnyx.Activities
try
{
await _telnyxClient.Calls.BridgeCallsAsync(callControlIdA, request, context.CancellationToken);
return Done();
return Combine(Outcome(TelnyxOutcomeNames.Bridging), Suspend());
}
catch (ApiException e)
{
@ -82,6 +97,50 @@ namespace Elsa.Activities.Telnyx.Activities
throw new WorkflowException(e.Content ?? e.Message, e);
}
}
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
var payload = context.GetInput<CallBridgedPayload>()!;
var results = new List<IActivityExecutionResult>();
if (payload.CallControlId == CallControlIdA)
{
CallBridgedPayloadA = payload;
results.Add(Outcome(TelnyxOutcomeNames.LegABridged, payload));
}
if (payload.CallControlId == CallControlIdB)
{
CallBridgedPayloadB = payload;
results.Add(Outcome(TelnyxOutcomeNames.LegBBridged, payload));
}
if (CallBridgedPayloadA != null && CallBridgedPayloadB != null)
{
results.Add(Outcome(TelnyxOutcomeNames.Bridged));
}
else
{
results.Add(Suspend());
}
return Combine(results);
}
private string? GetCallControlB(ActivityExecutionContext context)
{
if (!string.IsNullOrWhiteSpace(CallControlIdB))
return CallControlIdB;
var input = context.GetInput<CallAnsweredPayload>();
if (input != null)
return input.CallControlId;
var inboundCallActivityId = context.WorkflowExecutionContext.GetInboundConnections(Id).Where(x => x.Source.Activity.Type == nameof(Dial)).Select(x => x.Source.Activity.Id).FirstOrDefault();
var inboundCallActivityResponse = inboundCallActivityId != null ? context.WorkflowExecutionContext.GetActivityProperty<Dial, DialResponse>(inboundCallActivityId, x => x.DialResponse) : default;
return inboundCallActivityResponse != null ? inboundCallActivityResponse.CallControlId : null;
}
}
public static class BridgeCallsExtensions

View file

@ -26,7 +26,7 @@ namespace Elsa.Activities.Telnyx.Activities
[Action(
Category = Constants.Category,
Description = "Call a ring group.",
Outcomes = new[] { "Connected", "No Response" },
Outcomes = new[] {TelnyxOutcomeNames.Connected, TelnyxOutcomeNames.NoResponse},
DisplayName = "Call Ring Group"
)]
public class CallRingGroup : CompositeActivity, IActivityPropertyDefaultValueProvider
@ -38,14 +38,14 @@ namespace Elsa.Activities.Telnyx.Activities
_logger = logger;
}
[ActivityInput(UIHint = ActivityInputUIHints.MultiText, DefaultSyntax = SyntaxNames.Json, SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(UIHint = ActivityInputUIHints.MultiText, DefaultSyntax = SyntaxNames.Json, SupportedSyntaxes = new[] {SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public IList<string> Extensions
{
get => GetState<IList<string>>(() => new List<string>());
set => SetState(value);
}
[ActivityInput(Label = "Call Control ID", Hint = "Unique identifier and token for controlling the call.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Label = "Call Control ID", Hint = "Unique identifier and token for controlling the call.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string CallControlId
{
get => GetState<string>()!;
@ -56,7 +56,7 @@ namespace Elsa.Activities.Telnyx.Activities
Label = "Call Control App ID",
Hint = "The ID of the Call Control App (formerly ID of the connection) to be used when dialing the destination.",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string? CallControlAppId
{
@ -64,7 +64,7 @@ namespace Elsa.Activities.Telnyx.Activities
set => SetState(value);
}
[ActivityInput(SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public RingGroupStrategy Strategy
{
get => GetState<RingGroupStrategy>();
@ -73,7 +73,7 @@ namespace Elsa.Activities.Telnyx.Activities
[ActivityInput(
Hint = "The 'from' number to be used as the caller id presented to the destination ('To' number). The number should be in +E164 format. This attribute will default to the 'From' number of the original call if omitted.",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string? From
{
@ -84,7 +84,7 @@ namespace Elsa.Activities.Telnyx.Activities
[ActivityInput(
Hint =
"The string to be used as the caller id name (SIP From Display Name) presented to the destination ('To' number). The string should have a maximum of 128 characters, containing only letters, numbers, spaces, and -_~!.+ special characters. If omitted, the display name will be the same as the number in the 'From' field.",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string? FromDisplayName
{
@ -92,18 +92,21 @@ namespace Elsa.Activities.Telnyx.Activities
set => SetState(value);
}
[ActivityInput(DefaultValueProvider = typeof(CallRingGroup), SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(DefaultValueProvider = typeof(CallRingGroup), SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public Duration RingTime
{
get => GetState(() => Duration.FromSeconds(20));
set => SetState(value);
}
private string? DialedControlId
{
get => GetState<string?>();
set => SetState(value);
}
[ActivityInput(
Hint = "Enables Answering Machine Detection.",
UIHint = ActivityInputUIHints.Dropdown,
Options = new[] {"disabled", "detect", "detect_beep", "detect_words", "greeting_end"},
DefaultValue = "disabled",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string? AnsweringMachineDetection { get; set; } = "disabled";
private CallAnsweredPayload? CallAnsweredPayload
{
@ -148,64 +151,53 @@ namespace Elsa.Activities.Telnyx.Activities
private void BuildPrioritizedHuntFlow(IOutcomeBuilder builder) =>
builder
.ForEach(() => Extensions, iterate => iterate
.Then<Dial>(a => a
.WithConnectionId(() => CallControlAppId)
.WithTo(async context => await ResolveExtensionAsync(context, context.GetInput<string>()!))
.WithTimeoutSecs(() => (int) RingTime.TotalSeconds)
.WithFrom(() => From)
.WithFromDisplayName(() => FromDisplayName)
.WithClientState(context => new ClientStatePayload(context.CorrelationId!).ToBase64())
.Then<Dial>(dial => dial
.WithConnectionId(() => CallControlAppId)
.WithTo(ResolveExtensionAsync)
.WithTimeoutSecs(() => (int) RingTime.TotalSeconds)
.WithFrom(() => From)
.WithFromDisplayName(() => FromDisplayName),
dial =>
{
dial
.When(TelnyxOutcomeNames.Answered)
.Then<BridgeCalls>(bridgeCalls =>
bridgeCalls.When(TelnyxOutcomeNames.Bridged)
.Finish(TelnyxOutcomeNames.Connected));
}
)
.Then(context => DialedControlId = context.GetInput<DialResponse>()!.CallControlId)
.Then<Fork>(fork => fork.WithBranches("Connected", "No Response"), fork =>
{
fork
.When("Connected")
.ThenTypeNamed(CallAnsweredPayload.ActivityTypeName)
.Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput<TelnyxWebhook>()!.Data.Payload)
.Then<BridgeCalls>(bridge => bridge
.WithCallControlIdA(() => CallControlId)
.WithCallControlIdB(() => DialedControlId))
.ThenTypeNamed(CallBridgedPayload.ActivityTypeName)
.ThenTypeNamed(CallBridgedPayload.ActivityTypeName)
.Then<Finish>(finish => finish.WithOutcome("Connected").WithOutput(() => CallAnsweredPayload));
fork
.When("No Response")
.ThenTypeNamed(CallHangupPayload.ActivityTypeName);
})
)
.Finish("No Response");
.Finish(TelnyxOutcomeNames.NoResponse);
private void BuildRingAllFlow(IOutcomeBuilder builder) =>
builder
.Then<Fork>(fork => fork.WithBranches("Connected", "Timeout", "Dial Everyone"), fork =>
{
fork
.When("Connected")
.When(TelnyxOutcomeNames.Connected)
.ThenTypeNamed(CallAnsweredPayload.ActivityTypeName)
.Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput<TelnyxWebhook>()!.Data.Payload)
.Then<BridgeCalls>(bridge => bridge
.WithCallControlIdA(() => CallControlId)
.WithCallControlIdB(() => CallAnsweredPayload!.CallControlId))
.ThenTypeNamed(CallBridgedPayload.ActivityTypeName)
.Then<Finish>(finish => finish.WithOutcome("Connected").WithOutput(() => CallAnsweredPayload));
.WithCallControlIdB(() => CallAnsweredPayload!.CallControlId), bridge => bridge
.When(TelnyxOutcomeNames.Bridged)
.Finish(TelnyxOutcomeNames.Connected));
fork
.When("Timeout")
.StartIn(() => RingTime)
.Finish("No Response");
.Finish(TelnyxOutcomeNames.NoResponse);
fork
.When("Dial Everyone")
.ParallelForEach(() => Extensions, iterate => iterate
.Then<Dial>(a => a
.WithSuspendWorkflow(false)
.WithConnectionId(() => CallControlAppId)
.WithTo(async context => await ResolveExtensionAsync(context, context.GetInput<string>()!))
.WithTo(ResolveExtensionAsync)
.WithTimeoutSecs(() => (int) RingTime.TotalSeconds)
.WithFrom(() => From)
.WithFromDisplayName(() => FromDisplayName)
.WithClientState(context => new ClientStatePayload(context.CorrelationId!).ToBase64())
)
.Then(CollectCallControlIds));
});
@ -217,9 +209,13 @@ namespace Elsa.Activities.Telnyx.Activities
collection.Add(dialResponse);
CollectedDialResponses = collection;
}
private static async Task<string> ResolveExtensionAsync(ActivityExecutionContext context, string extension)
private static async ValueTask<string> ResolveExtensionAsync(ActivityExecutionContext context)
{
if (context.Resuming)
return context.GetActivityProperty<Dial, string>(x => x.To)!;
var extension = context.GetInput<string>()!;
var extensionProvider = context.GetService<IExtensionProvider>();
var resolvedExtension = await extensionProvider.GetAsync(extension, context.CancellationToken);
return resolvedExtension?.Number ?? extension;

View file

@ -5,7 +5,9 @@ using Elsa.Activities.Telnyx.Client.Models;
using Elsa.Activities.Telnyx.Client.Services;
using Elsa.Activities.Telnyx.Exceptions;
using Elsa.Activities.Telnyx.Extensions;
using Elsa.Activities.Telnyx.Models;
using Elsa.Activities.Telnyx.Options;
using Elsa.Activities.Telnyx.Webhooks.Payloads.Call;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Builders;
@ -21,7 +23,7 @@ namespace Elsa.Activities.Telnyx.Activities
[Action(
Category = Constants.Category,
Description = "Dial a number or SIP URI from a given connection.",
Outcomes = new[] { OutcomeNames.Done },
Outcomes = new[] { TelnyxOutcomeNames.Dialing, TelnyxOutcomeNames.CallInitiated, TelnyxOutcomeNames.Answered, TelnyxOutcomeNames.Hangup, OutcomeNames.Done },
DisplayName = "Dial"
)]
public class Dial : Activity
@ -58,9 +60,10 @@ namespace Elsa.Activities.Telnyx.Activities
[ActivityInput(
Hint = "Enables Answering Machine Detection.",
UIHint = ActivityInputUIHints.Dropdown,
Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end" },
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? AnsweringMachineDetection { get; set; }
Options = new[] {"disabled", "detect", "detect_beep", "detect_words", "greeting_end"},
DefaultValue = "disabled",
SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string? AnsweringMachineDetection { get; set; } = "disabled";
[ActivityInput(
Label = "Answering Machine Detection Configuration",
@ -76,12 +79,6 @@ namespace Elsa.Activities.Telnyx.Activities
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? CommandId { get; set; }
[ActivityInput(
Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? ClientState { get; set; }
[ActivityInput(Label = "Custom Headers", Hint = "Custom headers to be added to the SIP INVITE.", Category = PropertyCategories.Advanced, UIHint = ActivityInputUIHints.Json)]
public IList<Header>? CustomHeaders { get; set; }
@ -116,10 +113,36 @@ namespace Elsa.Activities.Telnyx.Activities
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? WebhookUrlMethod { get; set; }
[ActivityInput(
Hint = "A flag indicating whether this activity should complete immediately or suspend the workflow.",
Category = PropertyCategories.Advanced,
DefaultValue = true,
SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public bool SuspendWorkflow { get; set; } = true;
[ActivityOutput] public DialResponse DialResponse { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
var response = await DialAsync(context);
return Done(response);
DialResponse = response;
return !SuspendWorkflow
? Done(response)
: Combine(Outcome(TelnyxOutcomeNames.Dialing, response), Suspend());
}
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
var payload = context.GetInput<CallPayload>();
return payload switch
{
CallAnsweredPayload callAnsweredPayload => Outcome(TelnyxOutcomeNames.Answered, callAnsweredPayload),
CallHangupPayload callHangupPayload => Outcome(TelnyxOutcomeNames.Hangup, callHangupPayload),
CallInitiatedPayload callInitiatedPayload => Combine(Outcome(TelnyxOutcomeNames.CallInitiated, callInitiatedPayload), Suspend()),
_ => throw new ArgumentOutOfRangeException(nameof(payload))
};
}
private async Task<DialResponse> DialAsync(ActivityExecutionContext context)
@ -131,6 +154,8 @@ namespace Elsa.Activities.Telnyx.Activities
var fromNumber = context.GetFromNumber(From);
var clientState = new ClientStatePayload(context.CorrelationId!).ToBase64();
var request = new DialRequest(
connectionId,
To,
@ -138,7 +163,7 @@ namespace Elsa.Activities.Telnyx.Activities
FromDisplayName,
AnsweringMachineDetection,
AnsweringMachineDetectionConfig,
ClientState,
clientState,
CommandId,
CustomHeaders,
SipAuthUsername,
@ -207,12 +232,6 @@ namespace Elsa.Activities.Telnyx.Activities
public static ISetupActivity<Dial> WithCommandId(this ISetupActivity<Dial> setup, Func<string?> value) => setup.Set(x => x.CommandId, value);
public static ISetupActivity<Dial> WithCommandId(this ISetupActivity<Dial> setup, string? value) => setup.Set(x => x.CommandId, value);
public static ISetupActivity<Dial> WithClientState(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, ValueTask<string?>> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<Dial> WithClientState(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<Dial> WithClientState(this ISetupActivity<Dial> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<Dial> WithClientState(this ISetupActivity<Dial> setup, Func<string?> value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<Dial> WithClientState(this ISetupActivity<Dial> setup, string? value) => setup.Set(x => x.ClientState, value);
public static ISetupActivity<Dial> WithCustomHeaders(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, ValueTask<IList<Header>?>> value) => setup.Set(x => x.CustomHeaders, value);
public static ISetupActivity<Dial> WithCustomHeaders(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, IList<Header>?> value) => setup.Set(x => x.CustomHeaders, value);
public static ISetupActivity<Dial> WithCustomHeaders(this ISetupActivity<Dial> setup, Func<ValueTask<IList<Header>?>> value) => setup.Set(x => x.CustomHeaders, value);
@ -254,5 +273,11 @@ namespace Elsa.Activities.Telnyx.Activities
public static ISetupActivity<Dial> WithWebhookUrlMethod(this ISetupActivity<Dial> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.WebhookUrlMethod, value);
public static ISetupActivity<Dial> WithWebhookUrlMethod(this ISetupActivity<Dial> setup, Func<string?> value) => setup.Set(x => x.WebhookUrlMethod, value);
public static ISetupActivity<Dial> WithWebhookUrlMethod(this ISetupActivity<Dial> setup, string? value) => setup.Set(x => x.WebhookUrlMethod, value);
public static ISetupActivity<Dial> WithSuspendWorkflow(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, ValueTask<bool>> value) => setup.Set(x => x.SuspendWorkflow, value);
public static ISetupActivity<Dial> WithSuspendWorkflow(this ISetupActivity<Dial> setup, Func<ActivityExecutionContext, bool> value) => setup.Set(x => x.SuspendWorkflow, value);
public static ISetupActivity<Dial> WithSuspendWorkflow(this ISetupActivity<Dial> setup, Func<ValueTask<bool>> value) => setup.Set(x => x.SuspendWorkflow, value);
public static ISetupActivity<Dial> WithSuspendWorkflow(this ISetupActivity<Dial> setup, Func<bool> value) => setup.Set(x => x.SuspendWorkflow, value);
public static ISetupActivity<Dial> WithSuspendWorkflow(this ISetupActivity<Dial> setup, bool value) => setup.Set(x => x.SuspendWorkflow, value);
}
}

View file

@ -18,7 +18,7 @@ namespace Elsa.Activities.Telnyx.Activities
[Job(
Category = Constants.Category,
Description = "Play an audio file on the call until the required DTMF signals are gathered to build interactive menus.",
Outcomes = new[] { TelnyxOutcomeNames.Pending, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive },
Outcomes = new[] { TelnyxOutcomeNames.GatheringInput, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive },
DisplayName = "Gather Using Audio"
)]
public class GatherUsingAudio : Activity
@ -121,7 +121,7 @@ namespace Elsa.Activities.Telnyx.Activities
try
{
await _telnyxClient.Calls.GatherUsingAudioAsync(callControlId, request, context.CancellationToken);
return Combine(Outcome(TelnyxOutcomeNames.Pending), Suspend());
return Combine(Outcome(TelnyxOutcomeNames.GatheringInput), Suspend());
}
catch (ApiException e)
{

View file

@ -21,7 +21,7 @@ namespace Elsa.Activities.Telnyx.Activities
[Action(
Category = Constants.Category,
Description = "Convert text to speech and play it on the call until the required DTMF signals are gathered to build interactive menus.",
Outcomes = new[] { TelnyxOutcomeNames.Pending, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive },
Outcomes = new[] { TelnyxOutcomeNames.GatheringInput, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive },
DisplayName = "Gather Using Speak"
)]
public class GatherUsingSpeak : Activity
@ -151,7 +151,7 @@ namespace Elsa.Activities.Telnyx.Activities
try
{
await _telnyxClient.Calls.GatherUsingSpeakAsync(callControlId, request, context.CancellationToken);
return Combine(Outcome(TelnyxOutcomeNames.Pending), Suspend());
return Combine(Outcome(TelnyxOutcomeNames.GatheringInput), Suspend());
}
catch (ApiException e)
{

View file

@ -109,7 +109,7 @@ namespace Elsa.Activities.Telnyx.Activities
try
{
await _telnyxClient.Calls.SpeakTextAsync(callControlId, request, context.CancellationToken);
return Combine(Outcome(TelnyxOutcomeNames.Pending), Suspend());
return Combine(Outcome(TelnyxOutcomeNames.Speaking), Suspend());
}
catch (ApiException e)
{

View file

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Elsa.Activities.Telnyx.Client.Models;
using Elsa.Activities.Telnyx.Client.Services;
using Elsa.Activities.Telnyx.Extensions;
using Elsa.Activities.Telnyx.Webhooks.Payloads.Call;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Builders;
@ -19,7 +20,14 @@ namespace Elsa.Activities.Telnyx.Activities
[Action(
Category = Constants.Category,
Description = "Transfer a call to a new destination",
Outcomes = new[] { OutcomeNames.Done },
Outcomes = new[]
{
TelnyxOutcomeNames.Transferring,
TelnyxOutcomeNames.CallInitiated,
TelnyxOutcomeNames.Bridged,
TelnyxOutcomeNames.Answered,
TelnyxOutcomeNames.Hangup
},
DisplayName = "Transfer Call"
)]
public class TransferCall : Activity
@ -35,23 +43,23 @@ namespace Elsa.Activities.Telnyx.Activities
Label = "Call Control ID",
Hint = "Unique identifier and token for controlling the call.",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string? CallControlId { get; set; } = default!;
[ActivityInput(Label = "To", Hint = "The DID or SIP URI to dial out and bridge to the given call.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Label = "To", Hint = "The DID or SIP URI to dial out and bridge to the given call.", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string To { get; set; } = default!;
[ActivityInput(
Hint = "The 'from' number to be used as the caller id presented to the destination ('To' number). The number should be in +E164 format. This attribute will default to the 'From' number of the original call if omitted.",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string? From { get; set; }
[ActivityInput(
Hint =
"The string to be used as the caller id name (SIP From Display Name) presented to the destination ('To' number). The string should have a maximum of 128 characters, containing only letters, numbers, spaces, and -_~!.+ special characters. If omitted, the display name will be the same as the number in the 'From' field.",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string? FromDisplayName { get; set; }
@ -59,8 +67,8 @@ namespace Elsa.Activities.Telnyx.Activities
Label = "Answering Machine Detection",
Hint = "Enables Answering Machine Detection.",
UIHint = ActivityInputUIHints.Dropdown,
Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end" },
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid }
Options = new[] {"disabled", "detect", "detect_beep", "detect_words", "greeting_end"},
SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string? AnsweringMachineDetection { get; set; }
@ -76,46 +84,46 @@ namespace Elsa.Activities.Telnyx.Activities
Label = "Command ID",
Hint = "Use this field to avoid duplicate commands. Telnyx will ignore commands with the same Command ID.",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string? CommandId { get; set; }
[ActivityInput(
Label = "Audio URL",
Hint = "Audio URL to be played back when the transfer destination answers before bridging the call. The URL can point to either a WAV or MP3 file.",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public Uri? AudioUrl { get; set; }
[ActivityInput(
Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string? ClientState { get; set; }
[ActivityInput(
Hint = "Use this field to add state to every subsequent webhook for the new leg. It must be a valid Base-64 encoded string.",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string? TargetLegClientState { get; set; }
[ActivityInput(Hint = "Custom headers to be added to the SIP INVITE.", Category = PropertyCategories.Advanced, UIHint = ActivityInputUIHints.Json)]
public IList<Header>? CustomHeaders { get; set; }
[ActivityInput(Label = "SIP Authentication Username", Hint = "SIP Authentication username used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Label = "SIP Authentication Username", Hint = "SIP Authentication username used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string? SipAuthUsername { get; set; }
[ActivityInput(Label = "SIP Authentication Password", Hint = "SIP Authentication password used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Label = "SIP Authentication Password", Hint = "SIP Authentication password used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public string? SipAuthPassword { get; set; }
[ActivityInput(Label = "Time Limit", Hint = "Sets the maximum duration of a Call Control Leg in seconds.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Label = "Time Limit", Hint = "Sets the maximum duration of a Call Control Leg in seconds.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})]
public int? TimeLimitSecs { get; set; }
[ActivityInput(
Label = "Timeout",
Hint = "The number of seconds that Telnyx will wait for the call to be answered by the destination to which it is being transferred.",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public int? TimeoutSecs { get; set; }
@ -123,7 +131,7 @@ namespace Elsa.Activities.Telnyx.Activities
Label = "Webhook URL",
Hint = "Use this field to override the URL for which Telnyx will send subsequent webhooks to for this call.",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string? WebhookUrl { get; set; }
@ -131,22 +139,36 @@ namespace Elsa.Activities.Telnyx.Activities
Label = "Webhook URL Method",
Hint = "HTTP request type used for Webhook URL",
UIHint = ActivityInputUIHints.Dropdown,
Options = new[] { "GET", "POST" },
Options = new[] {"GET", "POST"},
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string? WebhookUrlMethod { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
await TransferCallAsync(context);
return Done();
return Combine(Outcome(TelnyxOutcomeNames.Transferring), Suspend());
}
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
var payload = context.GetInput<CallPayload>();
return payload switch
{
CallAnsweredPayload callAnsweredPayload => Outcome(TelnyxOutcomeNames.Answered, callAnsweredPayload),
CallBridgedPayload callBridgedPayload => Outcome(TelnyxOutcomeNames.Bridged, callBridgedPayload),
CallHangupPayload callHangupPayload => Outcome(TelnyxOutcomeNames.Hangup, callHangupPayload),
CallInitiatedPayload callInitiatedPayload => Outcome(TelnyxOutcomeNames.CallInitiated, callInitiatedPayload),
_ => throw new ArgumentOutOfRangeException(nameof(payload))
};
}
private async ValueTask TransferCallAsync(ActivityExecutionContext context)
{
var fromNumber = context.GetFromNumber(From);
var request = new TransferCallRequest(
To,
fromNumber,

View file

@ -0,0 +1,13 @@
using Elsa.Activities.Telnyx.Activities;
using Elsa.Activities.Telnyx.Webhooks.Payloads.Call;
using Elsa.Services;
namespace Elsa.Activities.Telnyx.Handlers
{
public class ResumeBridgeCalls : ResumeWebhookDrivenActivity<BridgeCalls, CallBridgedPayload>
{
public ResumeBridgeCalls(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad)
{
}
}
}

View file

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using Elsa.Activities.Telnyx.Activities;
using Elsa.Activities.Telnyx.Webhooks.Payloads.Call;
using Elsa.Services;
namespace Elsa.Activities.Telnyx.Handlers
{
public class ResumeDial : ResumeWebhookDrivenActivity<Dial>
{
public ResumeDial(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad)
{
}
protected override IEnumerable<Type> GetSupportedPayloadTypes() => new[] {typeof(CallInitiatedPayload), typeof(CallAnsweredPayload), typeof(CallHangupPayload)};
}
}

View file

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using Elsa.Activities.Telnyx.Activities;
using Elsa.Activities.Telnyx.Webhooks.Payloads.Call;
using Elsa.Services;
namespace Elsa.Activities.Telnyx.Handlers
{
public class ResumeTransferCall : ResumeWebhookDrivenActivity<TransferCall>
{
public ResumeTransferCall(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad)
{
}
protected override IEnumerable<Type> GetSupportedPayloadTypes() => new[] {typeof(CallAnsweredPayload), typeof(CallInitiatedPayload), typeof(CallBridgedPayload), typeof(CallHangupPayload)};
}
}

View file

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Telnyx.Activities;
using Elsa.Activities.Telnyx.Models;
using Elsa.Activities.Telnyx.Providers.Bookmarks;
using Elsa.Activities.Telnyx.Webhooks.Events;
@ -11,27 +13,41 @@ using MediatR;
namespace Elsa.Activities.Telnyx.Handlers
{
public abstract class ResumeWebhookDrivenActivity<TActivity, TPayload> : INotificationHandler<TelnyxWebhookReceived> where TPayload: CallPayload
public abstract class ResumeWebhookDrivenActivity<TActivity, TPayload> : ResumeWebhookDrivenActivity<TActivity> where TPayload : CallPayload
{
protected ResumeWebhookDrivenActivity(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad)
{
}
protected override IEnumerable<Type> GetSupportedPayloadTypes() => new[] {typeof(TPayload)};
}
public abstract class ResumeWebhookDrivenActivity<TActivity> : INotificationHandler<TelnyxWebhookReceived>
{
private readonly IWorkflowLaunchpad _workflowLaunchpad;
protected ResumeWebhookDrivenActivity(IWorkflowLaunchpad workflowLaunchpad) => _workflowLaunchpad = workflowLaunchpad;
protected virtual string ActivityTypeName => typeof(TActivity).Name;
protected abstract IEnumerable<Type> GetSupportedPayloadTypes();
public async Task Handle(TelnyxWebhookReceived notification, CancellationToken cancellationToken)
{
if (notification.Webhook.Data.Payload is not TPayload payload)
var supportedPayloadTypes = GetSupportedPayloadTypes().ToHashSet();
var receivedPayload = (CallPayload) notification.Webhook.Data.Payload;
var receivedPayloadType = receivedPayload.GetType();
if (!supportedPayloadTypes.Contains(receivedPayloadType))
return;
var correlationId = GetCorrelationId(payload);
var correlationId = GetCorrelationId(receivedPayload);
var trigger = CreateBookmark();
var bookmark = CreateBookmark();
var context = new CollectWorkflowsContext(ActivityTypeName, bookmark, trigger, correlationId);
await _workflowLaunchpad.CollectAndDispatchWorkflowsAsync(context, payload, cancellationToken);
await _workflowLaunchpad.CollectAndDispatchWorkflowsAsync(context, receivedPayload, cancellationToken);
}
protected virtual IBookmark CreateBookmark() => new GatherUsingSpeakBookmark();
private string GetCorrelationId(TPayload payload)
private string GetCorrelationId(CallPayload payload)
{
if (!string.IsNullOrWhiteSpace(payload.ClientState))
{

View file

@ -4,9 +4,21 @@
{
public const string CallIsNoLongerActive = "Call Is No Longer Active";
public const string Pending = "Pending";
public const string Connected = "Connected";
public const string NoResponse = "No Response";
public const string Dialing = "Dialing";
public const string CallInitiated = "CallInitiated";
public const string Answered = "Answered";
public const string Hangup = "Hangup";
public const string GatheringInput = "Gathering Input";
public const string GatherCompleted = "Gather Completed";
public const string InputReceived = "Input Received";
public const string Transferring = "Transferring";
public const string Bridging = "Bridging";
public const string Bridged = "Bridged";
public const string LegABridged = "Call A Answered";
public const string LegBBridged = "Call B Answered";
public const string MachineDetectionEnded = "Machine Detection Ended";
public const string MachineGreetingEnded = "Machine Greeting Ended";
public const string FinishedSpeaking = "Finished Speaking";
public const string Speaking = "Speaking";
public const string Recording = "Recording";

View file

@ -0,0 +1,13 @@
using Elsa.Activities.Telnyx.Activities;
using Elsa.Bookmarks;
namespace Elsa.Activities.Telnyx.Providers.Bookmarks
{
public class BridgeCallsBookmark : IBookmark
{
}
public class BridgeCallsBookmarkProvider : DefaultBookmarkProvider<BridgeCallsBookmark, BridgeCalls>
{
}
}

View file

@ -0,0 +1,13 @@
using Elsa.Activities.Telnyx.Activities;
using Elsa.Bookmarks;
namespace Elsa.Activities.Telnyx.Providers.Bookmarks
{
public class DialBookmark : IBookmark
{
}
public class DialBookmarkProvider : DefaultBookmarkProvider<DialBookmark, Dial>
{
}
}

View file

@ -0,0 +1,13 @@
using Elsa.Activities.Telnyx.Activities;
using Elsa.Bookmarks;
namespace Elsa.Activities.Telnyx.Providers.Bookmarks
{
public class TransferCallBookmark : IBookmark
{
}
public class TransferCallBookmarkProvider : DefaultBookmarkProvider<TransferCallBookmark, TransferCall>
{
}
}

View file

@ -3,7 +3,6 @@ using Elsa.Services.Models;
namespace Elsa.ActivityResults
{
[Obsolete("Use activity output properties to return output.")]
public class OutputResult : ActivityExecutionResult
{
public OutputResult(object? output) => Output = output;

View file

@ -1,4 +1,5 @@
using System;
using Elsa.Serialization.Converters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NodaTime;
@ -19,6 +20,16 @@ namespace Elsa
return item.ToObject<T>(CreateSerializer())!;
}
public static object? GetState(this JObject? state, string key, Type targetType)
{
var item = state?.GetValue(key, StringComparison.OrdinalIgnoreCase);
if (item == null || item.Type == JTokenType.Null)
return null;
return item.ToObject(targetType, CreateSerializer())!;
}
public static T? GetState<T>(this JObject? state, Type type, string key) => state.GetState<T>(type, key, () => default!);
@ -32,16 +43,19 @@ namespace Elsa
public static bool HasKey(this JObject state, string key) => state.ContainsKey(key);
private static JsonSerializer CreateSerializer()
public static JObject CreateState(object value) => JObject.FromObject(value, CreateSerializer());
public static JsonSerializer CreateSerializer()
{
var serializer = new JsonSerializer();
serializer.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
serializer.NullValueHandling = NullValueHandling.Ignore;
serializer.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
serializer.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
serializer.TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple;
serializer.TypeNameHandling = TypeNameHandling.Auto;
serializer.Converters.Add(new TypeJsonConverter());
return serializer;
}

View file

@ -34,18 +34,12 @@ namespace Elsa.Services
protected virtual IActivityExecutionResult OnResume() => Done();
protected virtual NoopResult Noop() => new();
protected virtual OutcomeResult Done() => Outcome(OutcomeNames.Done);
[Obsolete("Do not use methods that accept an output. Use Done() instead and activity output properties to return output.")]
protected virtual CombinedResult Done(object? output) => Combine(Output(output), Done());
protected virtual OutcomeResult Outcomes(IEnumerable<string> outcomes) => new(outcomes);
protected virtual OutcomeResult Outcomes(params string[] outcomes) => Outcomes((IEnumerable<string>) outcomes);
protected virtual OutcomeResult Outcome(string outcome) => Outcomes(outcome);
[Obsolete("Do not use methods that accept an output. Use Outcome(string outcome) instead and activity output properties to return output.")]
protected virtual CombinedResult Outcome(string outcome, object? output) => Combine(Output(output), Outcome(outcome));
[Obsolete("Use activity output properties to return output.")]
protected virtual OutputResult Output(object? output) => new(output);
protected virtual SuspendResult Suspend() => new();

View file

@ -50,7 +50,7 @@ namespace Elsa.Services
public async ValueTask SetActivityPropertiesAsync(IActivity activity, ActivityExecutionContext activityExecutionContext, CancellationToken cancellationToken = default)
{
var properties = activity.GetType().GetProperties().Where(IsActivityProperty).ToList();
var properties = activity.GetType().GetProperties().Where(IsActivityInputProperty).ToList();
var providers = GetProviders(activity.Id);
foreach (var property in properties)
@ -85,7 +85,7 @@ namespace Elsa.Services
}
}
private bool IsActivityProperty(PropertyInfo property) => property.GetCustomAttribute<ActivityInputAttribute>() != null;
private bool IsActivityInputProperty(PropertyInfo property) => property.GetCustomAttribute<ActivityInputAttribute>() != null;
public IEnumerator<KeyValuePair<string, IDictionary<string, IActivityPropertyValueProvider>>> GetEnumerator() => _providers.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

View file

@ -40,17 +40,18 @@ namespace Elsa.Services.Models
get => WorkflowExecutionContext.ContextId;
set => WorkflowExecutionContext.ContextId = value;
}
public IReadOnlyCollection<string> Outcomes { get; set; }
public object? Input { get; }
public bool Resuming { get; }
public bool IsFirstPass => WorkflowExecutionContext.IsFirstPass;
public string? CorrelationId
{
get => WorkflowExecutionContext.CorrelationId;
set => WorkflowExecutionContext.CorrelationId = value;
}
public CancellationToken CancellationToken { get; }
public JObject GetData() => WorkflowInstance.ActivityData.GetItem(ActivityBlueprint.Id, () => new JObject());
@ -60,19 +61,25 @@ namespace Elsa.Services.Models
var data = GetData();
data.SetState(propertyName, value);
}
public T? GetState<T>(string propertyName)
{
var data = GetData();
return data.GetState<T>(propertyName);
}
public T GetState<T>(string propertyName, Func<T> defaultValue)
{
var data = GetData();
return data.GetState<T>(propertyName, defaultValue);
}
public object? GetState(string propertyName, Type targetType)
{
var data = GetData();
return data.GetState(propertyName, targetType);
}
public T? GetState<TActivity, T>(Expression<Func<TActivity, T>> propertyExpression) where TActivity : IActivity
{
var expression = (MemberExpression) propertyExpression.Body;
@ -81,27 +88,27 @@ namespace Elsa.Services.Models
}
public T? GetContainerState<T>() => GetContainerState<T>(typeof(T).Name);
public T? GetContainerState<T>(string key)
{
var parentActivityId = ActivityBlueprint.Parent?.Id;
if (parentActivityId == null)
return default;
var parentData = WorkflowExecutionContext.WorkflowInstance.ActivityData.GetItem(parentActivityId);
return parentData.GetState<T>(key);
}
public void SetContainerState<T>(object? value) => SetContainerState(typeof(T).Name, value);
public void SetContainerState<T>(object? value) => SetContainerState(typeof(T).Name, value);
public void SetContainerState(string key, object? value)
{
var parentActivityId = ActivityBlueprint.Parent?.Id;
if (parentActivityId == null)
return;
var parentData = WorkflowExecutionContext.WorkflowInstance.ActivityData.GetItem(parentActivityId);
parentData?.SetState(key, value);
}
@ -119,7 +126,7 @@ namespace Elsa.Services.Models
public ActivityScope GetNamedScope(string activityName) => WorkflowExecutionContext.GetNamedScope(activityName);
public void SetVariable(string name, object? value) => WorkflowExecutionContext.SetVariable(name, value);
public T? SetVariable<T>(string name, Func<T?, T?> updater)
{
var value = GetVariable<T>(name);
@ -151,7 +158,7 @@ namespace Elsa.Services.Models
var activityTypeService = ServiceProvider.GetRequiredService<IActivityTypeService>();
return await activityTypeService.ActivateActivityAsync(ActivityBlueprint, cancellationToken);
}
public T? GetInput<T>() => Input.ConvertTo<T>();
public T? GetInput<T>(Func<T?> defaultValue) => Input != null ? Input.ConvertTo<T>() : defaultValue();
public T? GetInput<T>(T? defaultValue) => Input != null ? Input.ConvertTo<T>() : defaultValue;
@ -167,6 +174,7 @@ namespace Elsa.Services.Models
public T GetWorkflowContext<T>() => WorkflowExecutionContext.GetWorkflowContext<T>();
public JObject GetActivityData() => GetActivityData(ActivityId);
public JObject GetActivityData(string activityId) => WorkflowExecutionContext.GetActivityData(activityId);
public T? GetActivityProperty<TActivity, T>(Expression<Func<TActivity, T>> propertyExpression) where TActivity : IActivity => WorkflowExecutionContext.GetActivityProperty<TActivity, T>(ActivityId, propertyExpression);
public void Fault(Exception exception) => WorkflowExecutionContext.Fault(exception, ActivityId, Input, Resuming);
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Events;
@ -203,6 +204,20 @@ namespace Elsa.Services.Models
}
public T GetOutputFrom<T>(string activityName) => (T) GetOutputFrom(activityName)!;
public T? GetActivityProperty<TActivity, T>(string activityId, Expression<Func<TActivity, T>> propertyExpression) where TActivity : IActivity
{
var expression = (MemberExpression) propertyExpression.Body;
string propertyName = expression.Member.Name;
return GetActivityProperty<T>(activityId, propertyName);
}
public T? GetActivityProperty<T>(string activityId, string propertyName)
{
var data = GetActivityData(activityId);
return data.GetState<T>(propertyName);
}
public void SetWorkflowContext(object? value) => WorkflowContext = value;
public object? GetWorkflowContext() => WorkflowContext;
public T GetWorkflowContext<T>() => (T) WorkflowContext!;

View file

@ -41,7 +41,6 @@ namespace Elsa.Handlers
{
Output = activityExecutionContext.Output,
Outcomes = activityExecutionContext.Outcomes,
State = activityExecutionContext.GetActivityData()
};
var resuming = activityExecutionContext.Resuming;

View file

@ -1,7 +1,10 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.ControlFlow;
using Elsa.Attributes;
using Elsa.Services.Models;
namespace Elsa.Services
@ -14,15 +17,17 @@ namespace Elsa.Services
{
_elsaOptions = options;
}
public async Task<IActivity> ActivateActivityAsync(ActivityExecutionContext context, Type type, CancellationToken cancellationToken = default)
{
var activity = _elsaOptions.ActivityFactory.CreateService(type, context.ServiceProvider);
activity.Data = context.GetData();
activity.Id = context.ActivityId;
ApplyStoredValues(context, activity);
// TODO: Make extensible / apply open/closed.
if(ShouldSetProperties(activity))
if (ShouldSetProperties(context, activity))
{
// TODO: Figure out how to deal with dynamically defined properties and what it means to set values to these.
// ActivityTypes can have dynamic properties, so they need to be able to "intercept" when values are being applied.
@ -30,11 +35,24 @@ namespace Elsa.Services
//var activityType = await _activityTypeService.GetActivityTypeAsync(activity.Type, cancellationToken);
await context.WorkflowExecutionContext.WorkflowBlueprint.ActivityPropertyProviders.SetActivityPropertiesAsync(activity, context, context.CancellationToken);
}
return activity;
}
private bool ShouldSetProperties(IActivity activity)
private void ApplyStoredValues(ActivityExecutionContext context, IActivity activity)
{
var properties = activity.GetType().GetProperties().Where(IsActivityProperty).ToList();
foreach (var property in properties)
{
var value = context.GetState(property.Name, property.PropertyType);
if (value != null)
property.SetValue(activity, value);
}
}
private bool ShouldSetProperties(ActivityExecutionContext context, IActivity activity)
{
if (IsReturningComposite(activity))
return false;
@ -44,7 +62,7 @@ namespace Elsa.Services
activity.Data.SetState("Unwinding", false);
return false;
}
if (IsReturningSwitch(activity))
{
activity.Data.SetState("Unwinding", false);
@ -53,9 +71,10 @@ namespace Elsa.Services
return true;
}
private bool IsReturningComposite(IActivity activity) => activity is CompositeActivity && activity.Data.GetState<bool>(nameof(CompositeActivity.IsScheduled));
private bool IsReturningIf(IActivity activity) => activity is If && activity.Data.GetState<bool>("Unwinding");
private bool IsReturningSwitch(IActivity activity) => activity is Switch && activity.Data.GetState<bool>("Unwinding");
private bool IsActivityProperty(PropertyInfo property) => property.GetCustomAttribute<ActivityInputAttribute>() != null || property.GetCustomAttribute<ActivityOutputAttribute>() != null;
}
}

View file

@ -247,20 +247,23 @@ namespace Elsa.Services
var correlationId = context.CorrelationId!;
var lockKey = correlationId;
await using var handle = await AcquireLockAsync(lockKey, cancellationToken);
var correlatedWorkflowInstanceCount = !string.IsNullOrWhiteSpace(correlationId)
? await _workflowInstanceStore.CountAsync(new CorrelationIdSpecification<WorkflowInstance>(correlationId).WithStatus(WorkflowStatus.Suspended), cancellationToken)
: 0;
_logger.LogDebug("Found {CorrelatedWorkflowCount} workflows with correlation ID {CorrelationId}", correlatedWorkflowInstanceCount, correlationId);
if (correlatedWorkflowInstanceCount > 0)
await using (var handle = await AcquireLockAsync(lockKey, cancellationToken))
{
var bookmarkResults = context.Bookmark != null ? await _bookmarkFinder.FindBookmarksAsync(context.ActivityType, context.Bookmark, correlationId, context.TenantId, cancellationToken).ToList() : new List<BookmarkFinderResult>();
_logger.LogDebug("Found {BookmarkCount} bookmarks for activity type {ActivityType}", bookmarkResults.Count, context.ActivityType);
return bookmarkResults.Select(x => new PendingWorkflow(x.WorkflowInstanceId, x.ActivityId)).ToList();
}
var correlatedWorkflowInstanceCount = !string.IsNullOrWhiteSpace(correlationId)
? await _workflowInstanceStore.CountAsync(new CorrelationIdSpecification<WorkflowInstance>(correlationId).WithStatus(WorkflowStatus.Suspended), cancellationToken)
: 0;
_logger.LogDebug("Found {CorrelatedWorkflowCount} workflows with correlation ID {CorrelationId}", correlatedWorkflowInstanceCount, correlationId);
if (correlatedWorkflowInstanceCount > 0)
{
var bookmarkResults = context.Bookmark != null
? await _bookmarkFinder.FindBookmarksAsync(context.ActivityType, context.Bookmark, correlationId, context.TenantId, cancellationToken).ToList()
: new List<BookmarkFinderResult>();
_logger.LogDebug("Found {BookmarkCount} bookmarks for activity type {ActivityType}", bookmarkResults.Count, context.ActivityType);
return bookmarkResults.Select(x => new PendingWorkflow(x.WorkflowInstanceId, x.ActivityId)).ToList();
}
} // This ensures the lock handle is released before calling the next line (CollectStartableWorkflowsAsync), which also acquires a lock on the correlation ID.
var startableWorkflows = await CollectStartableWorkflowsAsync(context, cancellationToken);
return startableWorkflows.Select(x => new PendingWorkflow(x.WorkflowInstance.Id, x.ActivityId)).ToList();

View file

@ -1,4 +1,4 @@
<elsa-workflows-studio server-url="@ServerUrl" monaco-lib-path="@MonacoLibPath"></elsa-workflows-studio>
<elsa-studio-root server-url="@ServerUrl" monaco-lib-path="@MonacoLibPath"></elsa-studio-root>
@code{

View file

@ -12,7 +12,7 @@ export function initializeMonacoWorker(libPath?: string) {
return;
const origin = document.location.origin;
const baseUrl = `${origin}/${libPath}`;
const baseUrl = libPath.startsWith('http') ? libPath : `${origin}/${libPath}`;
require.config({paths: {'vs': `${baseUrl}/vs`}});
win.MonacoEnvironment = {getWorkerUrl: () => proxy};

View file

@ -12,7 +12,7 @@
<script type="module" src="/build/elsa-workflows-studio.esm.js"></script>
<script nomodule src="/build/elsa-workflows-studio.js"></script>
</head>
<body class="elsa-h-screen" style="background-size: 30px 30px; background-image: url(/build/assets/images/tile.png); background-color: #FBFBFB;">
<body>
<!--<elsa-studio-root server-url="https://localhost:11000" monaco-lib-path="build/assets/js/monaco-editor/min"></elsa-studio-root>-->
<elsa-studio-root server-url="https://localhost:6082" monaco-lib-path="build/assets/js/monaco-editor/min"></elsa-studio-root>

View file

@ -12,6 +12,8 @@ namespace ElsaDashboard.Samples.AspNetCore.Monolith
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
.ConfigureWebHostDefaults(webBuilder => { webBuilder
.UseStaticWebAssets()
.UseStartup<Startup>(); });
}
}

View file

@ -1,22 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\FONT-LICENSE" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\css\open-iconic-bootstrap.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.eot" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.otf" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.svg" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.ttf" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.woff" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\ICON-LICENSE" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\README.md" />
<_ContentIncludedByDefault Remove="wwwroot\css\site.css" />
<ProjectReference Include="..\..\..\..\designer\bindings\aspnet\Elsa.Designer.Components.Web\Elsa.Designer.Components.Web.csproj" />
</ItemGroup>
</Project>

View file

@ -1,3 +1,3 @@
@page "/"
<elsa-studio-root server-url="https://localhost:11000" monaco-lib-path="https://unpkg.com/browse/monaco-editor/min"></elsa-studio-root>
<WorkflowStudio ServerUrl="https://localhost:11000"/>

View file

@ -12,12 +12,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>ElsaDashboard.Samples.BlazorServer</title>
<base href="~/"/>
<link rel="icon" type="image/png" sizes="32x32" href="https://unpkg.com/@@elsa-workflows/elsa-workflows-studio@2.0.0/dist/elsa-workflows-studio/assets/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://unpkg.com/@@elsa-workflows/elsa-workflows-studio@2.0.0/dist/elsa-workflows-studio/assets/images/favicon-16x16.png">
<link rel="stylesheet" href="https://unpkg.com/@@elsa-workflows/elsa-workflows-studio@2.0.0/dist/elsa-workflows-studio/assets/fonts/inter/inter.css">
<link href="https://unpkg.com/@@elsa-workflows/elsa-workflows-studio@2.0.0/dist/elsa-workflows-studio/assets/styles/tailwind.css" rel="stylesheet">
<script src="https://unpkg.com/browse/monaco-editor/min/vs/loader.js"></script>
<script type="module" src="https://unpkg.com/@@elsa-workflows/elsa-workflows-studio@2.0.0/dist/elsa-workflows-studio/elsa-workflows-studio.esm.js"></script>
<link rel="icon" type="image/png" sizes="32x32" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/favicon-16x16.png">
<link rel="stylesheet" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/fonts/inter/inter.css">
<link rel="stylesheet" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/styles/tailwind.css">
<script src="/_content/Elsa.Designer.Components.Web/monaco-editor/min/vs/loader.js"></script>
<script type="module" src="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/elsa-workflows-studio.esm.js"></script>
</head>
<body>
<component type="typeof(App)" render-mode="ServerPrerendered"/>

View file

@ -6,5 +6,6 @@
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Elsa.Designer.Components.Web
@using ElsaDashboard.Samples.BlazorServer
@using ElsaDashboard.Samples.BlazorServer.Shared

View file

@ -1,29 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0-preview.2.21154.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0-preview.2.21154.6" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.6" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\css\app.css" />
<_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css.map" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\FONT-LICENSE" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\css\open-iconic-bootstrap.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.eot" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.otf" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.svg" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.ttf" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.woff" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\ICON-LICENSE" />
<_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\README.md" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\designer\bindings\aspnet\Elsa.Designer.Components.Web\Elsa.Designer.Components.Web.csproj" />
</ItemGroup>

View file

@ -1,3 +1,3 @@
@page "/"
<elsa-studio-root server-url="https://localhost:11000" monaco-lib-path="https://unpkg.com/browse/monaco-editor/min"></elsa-studio-root>
<WorkflowStudio ServerUrl="https://localhost:11000"/>

View file

@ -6,5 +6,6 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using Elsa.Designer.Components.Web
@using ElsaDashboard.Samples.BlazorWasm
@using ElsaDashboard.Samples.BlazorWasm.Shared

View file

@ -22,7 +22,10 @@ namespace Elsa.Scripting.JavaScript.Extensions
where value != null && value.GetType() == typeof(JArray)
select key).ToList();
jArrayKeys.ForEach(key => result[key] = ((JArray) result[key]).Values().Select(x => ((JValue) x).Value).ToArray());
jArrayKeys.ForEach(key => result[key] = ((JArray) result[key]).Values().Select(x =>
{
return x is JValue jValue ? jValue.Value : x is JProperty jProperty ? ((JValue) jProperty.Value).Value : x;
}).ToArray());
jObjectKeys.ForEach(key => result[key] = ToDictionary((JObject) result[key]));
return result;

View file

@ -82,19 +82,18 @@ namespace Elsa.Scripting.JavaScript.Handlers
foreach (var variable in variables.Data)
engine.SetValue(variable.Key, variable.Value);
// TODO: Deprecated. Remove with next breaking version.
// Activity outputs.
foreach (var activity in workflowBlueprint.Activities.Where(x => x.Name is not null and not "" && workflowInstance.ActivityOutput.ContainsKey(x.Id)))
{
var output = new { Output = workflowInstance.ActivityOutput[activity.Id!] };
engine.SetValue(activity.Name, output);
}
// Named activities.
foreach (var activity in workflowBlueprint.Activities.Where(x => !string.IsNullOrWhiteSpace(x.Name)))
{
var state = activityExecutionContext.GetActivityData(activity.Id);
var dictionary = state.ToDictionary();
// Output.
if (workflowInstance.ActivityOutput.ContainsKey(activity.Id))
{
var output = workflowInstance.ActivityOutput[activity.Id];
dictionary["Output"] = output;
}
engine.SetValue(activity.Name, dictionary);
}

View file

@ -76,11 +76,10 @@ namespace Elsa.Scripting.Liquid.Handlers
{
var activityExecutionContext = activityModel.ActivityExecutionContext;
// Deprecated.
if (name == "Output")
{
var output = activityExecutionContext.GetOutputFrom(activityModel.ActivityName!);
return output != null ? new JObject(output) : default;
return output != null ? JObjectExtensions.CreateState(output) : default;
}
var workflowExecutionContext = activityExecutionContext.WorkflowExecutionContext;

View file

@ -1,13 +1,74 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Elsa.Services.Models;
namespace Elsa.Server.Api.Endpoints.Signals
{
public record DispatchSignalRequest(string? WorkflowInstanceId, string? CorrelationId, object? Input);
public record DispatchSignalRequest
{
// Need this for Swagger.
public DispatchSignalRequest()
{
}
public DispatchSignalRequest(string? workflowInstanceId, string? correlationId, object? input)
{
WorkflowInstanceId = workflowInstanceId;
CorrelationId = correlationId;
Input = input;
}
public record DispatchSignalResponse(ICollection<PendingWorkflow> StartedWorkflows);
public string? WorkflowInstanceId { get; init; }
public string? CorrelationId { get; init; }
public object? Input { get; init; }
}
public record ExecuteSignalRequest(string? WorkflowInstanceId, string? CorrelationId, object? Input);
public record DispatchSignalResponse
{
// Need this for Swagger.
public DispatchSignalResponse()
{
}
public DispatchSignalResponse(ICollection<PendingWorkflow> startedWorkflows)
{
StartedWorkflows = startedWorkflows;
}
public record ExecuteSignalResponse(ICollection<StartedWorkflow> StartedWorkflows);
public ICollection<PendingWorkflow> StartedWorkflows { get; init; } = default!;
}
public record ExecuteSignalRequest
{
// Need this for Swagger.
public ExecuteSignalRequest()
{
}
public ExecuteSignalRequest(string? workflowInstanceId, string? correlationId, object? input)
{
WorkflowInstanceId = workflowInstanceId;
CorrelationId = correlationId;
Input = input;
}
public string? WorkflowInstanceId { get; init; }
public string? CorrelationId { get; init; }
public object? Input { get; init; }
}
public record ExecuteSignalResponse
{
// Need this for Swagger.
public ExecuteSignalResponse()
{
}
public ExecuteSignalResponse(ICollection<StartedWorkflow> startedWorkflows)
{
StartedWorkflows = startedWorkflows;
}
public ICollection<StartedWorkflow> StartedWorkflows { get; init; } = new List<StartedWorkflow>();
}
}