diff --git a/Directory.Packages.props b/Directory.Packages.props
index 93412d02..2eac0ef7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,6 +22,7 @@
+
@@ -30,6 +31,8 @@
+
+
diff --git a/Elsa.Integrations.sln b/Elsa.Integrations.sln
index 3acc9ed7..7cce4bba 100644
--- a/Elsa.Integrations.sln
+++ b/Elsa.Integrations.sln
@@ -28,6 +28,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A99FA26E-2
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Slack.Tests", "test\Elsa.Integrations.Slack.Tests\Elsa.Integrations.Slack.Tests.csproj", "{861E1230-F9CB-450C-845E-04DFDA259E26}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Telnyx", "src\Elsa.Integrations.Telnyx\Elsa.Integrations.Telnyx.csproj", "{128B2FC3-81A7-4327-9665-9155B05F21DA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -46,6 +48,10 @@ Global
{861E1230-F9CB-450C-845E-04DFDA259E26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{861E1230-F9CB-450C-845E-04DFDA259E26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{861E1230-F9CB-450C-845E-04DFDA259E26}.Release|Any CPU.Build.0 = Release|Any CPU
+ {128B2FC3-81A7-4327-9665-9155B05F21DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {128B2FC3-81A7-4327-9665-9155B05F21DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {128B2FC3-81A7-4327-9665-9155B05F21DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {128B2FC3-81A7-4327-9665-9155B05F21DA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -54,5 +60,6 @@ Global
{A8666FC4-66E1-4766-A22B-C410D42D03DD} = {527248D6-B851-4C8D-8667-E2FB0A91DABF}
{9732E404-11B5-48DB-B5D9-97997F018830} = {527248D6-B851-4C8D-8667-E2FB0A91DABF}
{861E1230-F9CB-450C-845E-04DFDA259E26} = {A99FA26E-2098-403A-BD04-6BBCFBE3AC7D}
+ {128B2FC3-81A7-4327-9665-9155B05F21DA} = {527248D6-B851-4C8D-8667-E2FB0A91DABF}
EndGlobalSection
EndGlobal
diff --git a/Elsa.Integrations.sln.DotSettings b/Elsa.Integrations.sln.DotSettings
new file mode 100644
index 00000000..f72dc16e
--- /dev/null
+++ b/Elsa.Integrations.sln.DotSettings
@@ -0,0 +1,2 @@
+
+ True
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/AnswerCall.cs b/src/Elsa.Integrations.Telnyx/Activities/AnswerCall.cs
new file mode 100644
index 00000000..d3efcdd2
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/AnswerCall.cs
@@ -0,0 +1,30 @@
+using System.Runtime.CompilerServices;
+using Elsa.Workflows;
+using Elsa.Workflows.Attributes;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+public class AnswerCall : AnswerCallBase
+{
+ ///
+ public AnswerCall([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// The activity to schedule when the call was successfully answered.
+ ///
+ [Port] public IActivity? Connected { get; set; }
+
+ ///
+ /// The activity to schedule when the call was no longer active.
+ ///
+ [Port] public IActivity? Disconnected { get; set; }
+
+ ///
+ protected override async ValueTask HandleConnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Connected);
+
+ ///
+ protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected);
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/AnswerCallBase.cs b/src/Elsa.Integrations.Telnyx/Activities/AnswerCallBase.cs
new file mode 100644
index 00000000..ff11ed6c
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/AnswerCallBase.cs
@@ -0,0 +1,71 @@
+using Elsa.Extensions;
+using Elsa.Integrations.Telnyx.Bookmarks;
+using Elsa.Integrations.Telnyx.Client.Models;
+using Elsa.Integrations.Telnyx.Client.Services;
+using Elsa.Integrations.Telnyx.Extensions;
+using Elsa.Integrations.Telnyx.Payloads.Call;
+using Elsa.Workflows;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using Refit;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+/// Answer an incoming call. You must issue this command before executing subsequent commands on an incoming call.
+///
+[Activity(Constants.Namespace, "Answer an incoming call. You must issue this command before executing subsequent commands on an incoming call.", Kind = ActivityKind.Task)]
+public abstract class AnswerCallBase : Activity
+{
+ ///
+ protected AnswerCallBase(string? source = null, int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// The call control ID to answer. Leave blank when the workflow is driven by an incoming call and you wish to pick up that one.
+ ///
+ [Input(DisplayName = "Call Control ID", Description = "The call control ID of the call to answer.", Category = "Advanced")]
+ public Input CallControlId { get; set; } = null!;
+
+ ///
+ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
+ {
+ var callControlId = CallControlId.Get(context);
+
+ var request = new AnswerCallRequest { ClientState = context.CreateCorrelatingClientState(context.Id) };
+
+ var telnyxClient = context.GetRequiredService();
+
+ try
+ {
+ // Send a request to Telnyx to answer the call.
+ await telnyxClient.Calls.AnswerCallAsync(callControlId, request, context.CancellationToken);
+
+ // Create a bookmark so we can resume the workflow when the call is answered.
+ context.CreateBookmark(new AnswerCallStimulus(callControlId), ResumeAsync, true);
+ }
+ catch (ApiException e)
+ {
+ if (!await e.CallIsNoLongerActiveAsync()) throw;
+ await HandleDisconnectedAsync(context);
+ }
+ }
+
+ ///
+ /// Invoked when the call was successfully answered.
+ ///
+ protected abstract ValueTask HandleConnectedAsync(ActivityExecutionContext context);
+
+ ///
+ /// Invoked when the call was no longer active.
+ ///
+ protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context);
+
+ private async ValueTask ResumeAsync(ActivityExecutionContext context)
+ {
+ var payload = context.GetWorkflowInput();
+ context.Set(Result, payload);
+ await HandleConnectedAsync(context);
+ }
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/BridgeCalls.cs b/src/Elsa.Integrations.Telnyx/Activities/BridgeCalls.cs
new file mode 100644
index 00000000..b48d7bed
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/BridgeCalls.cs
@@ -0,0 +1,32 @@
+using System.Runtime.CompilerServices;
+using Elsa.Workflows;
+using Elsa.Workflows.Attributes;
+using JetBrains.Annotations;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+[PublicAPI]
+public class BridgeCalls : BridgeCallsBase
+{
+ ///
+ public BridgeCalls([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// The to execute when the source leg call is no longer active.
+ ///
+ [Port] public IActivity? Disconnected { get; set; }
+
+ ///
+ /// The to execute when the two calls are bridged.
+ ///
+ [Port] public IActivity? Bridged { get; set; }
+
+ ///
+ protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompleted);
+
+ ///
+ protected override async ValueTask HandleBridgedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Bridged, OnCompleted);
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/BridgeCallsBase.cs b/src/Elsa.Integrations.Telnyx/Activities/BridgeCallsBase.cs
new file mode 100644
index 00000000..bd6ce83a
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/BridgeCallsBase.cs
@@ -0,0 +1,99 @@
+using Elsa.Extensions;
+using Elsa.Integrations.Telnyx.Bookmarks;
+using Elsa.Integrations.Telnyx.Client.Models;
+using Elsa.Integrations.Telnyx.Client.Services;
+using Elsa.Integrations.Telnyx.Extensions;
+using Elsa.Integrations.Telnyx.Models;
+using Elsa.Integrations.Telnyx.Payloads.Call;
+using Elsa.Workflows;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using JetBrains.Annotations;
+using Refit;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+/// Bridge two calls.
+///
+[Activity(Constants.Namespace, "Bridge two calls.", Kind = ActivityKind.Task)]
+[PublicAPI]
+public abstract class BridgeCallsBase : Activity
+{
+ ///
+ protected BridgeCallsBase(string? source = null, int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// The source call control ID of one of the call to bridge with. Leave empty to use the ambient inbound call control Id, if there is one.
+ ///
+ [Input(DisplayName = "Call Control ID A", Description = "The source call control ID of one of the call to bridge with. Leave empty to use the ambient inbound call control Id, if there is one.")]
+ public Input CallControlIdA { get; set; } = null!;
+
+ ///
+ /// The destination call control ID of the call you want to bridge with.
+ ///
+ [Input(DisplayName = "Call Control ID B", Description = "The destination call control ID of the call you want to bridge with.")]
+ public Input CallControlIdB { get; set; } = null!;
+
+ ///
+ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
+ {
+ var callControlIdA = CallControlIdA.Get(context);
+ var callControlIdB = CallControlIdB.Get(context);
+ var request = new BridgeCallsRequest(callControlIdB, ClientState: context.CreateCorrelatingClientState(context.Id));
+ var telnyxClient = context.GetRequiredService();
+
+ try
+ {
+ await telnyxClient.Calls.BridgeCallsAsync(callControlIdA, request, context.CancellationToken);
+
+ var bookmarkA = new WebhookEventStimulus(WebhookEventTypes.CallBridged, callControlIdA);
+ var bookmarkB = new WebhookEventStimulus(WebhookEventTypes.CallBridged, callControlIdB);
+ context.CreateBookmarks([bookmarkA, bookmarkB], ResumeAsync, false);
+ }
+ catch (ApiException e)
+ {
+ if (!await e.CallIsNoLongerActiveAsync()) throw;
+
+ await HandleDisconnectedAsync(context);
+ }
+ }
+
+ ///
+ /// Called when the call is disconnected.
+ ///
+ protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context);
+
+ ///
+ /// Called when the call is bridged.
+ ///
+ ///
+ protected abstract ValueTask HandleBridgedAsync(ActivityExecutionContext context);
+
+ ///
+ /// Called when the activity is completed.
+ ///
+ protected async ValueTask OnCompleted(ActivityCompletedContext context) => await context.TargetContext.CompleteActivityAsync();
+
+ private async ValueTask ResumeAsync(ActivityExecutionContext context)
+ {
+ var payload = context.GetWorkflowInput()!;
+ var callControlIdA = CallControlIdA.Get(context);
+ var callControlIdB = CallControlIdB.Get(context);
+ ;
+
+ if (payload.CallControlId == callControlIdA) context.SetProperty("CallBridgedPayloadA", payload);
+ if (payload.CallControlId == callControlIdB) context.SetProperty("CallBridgedPayloadB", payload);
+
+ var callBridgedPayloadA = context.GetProperty("CallBridgedPayloadA");
+ var callBridgedPayloadB = context.GetProperty("CallBridgedPayloadB");
+
+ if (callBridgedPayloadA != null && callBridgedPayloadB != null)
+ {
+ context.Set(Result, new(callBridgedPayloadA, callBridgedPayloadB));
+ await HandleBridgedAsync(context);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/CallAnswered.cs b/src/Elsa.Integrations.Telnyx/Activities/CallAnswered.cs
new file mode 100644
index 00000000..3c460eee
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/CallAnswered.cs
@@ -0,0 +1,54 @@
+using System.Runtime.CompilerServices;
+using Elsa.Extensions;
+using Elsa.Integrations.Telnyx.Bookmarks;
+using Elsa.Integrations.Telnyx.Helpers;
+using Elsa.Integrations.Telnyx.Payloads.Call;
+using Elsa.Workflows;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using Elsa.Workflows.UIHints;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+/// Represents a Telnyx webhook event trigger.
+///
+[Activity("Telnyx", "Telnyx", "A Telnyx webhook event that executes when a call is answered.", Kind = ActivityKind.Trigger)]
+public class CallAnswered : Activity
+{
+ ///
+ public CallAnswered([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// A list of call control IDs to listen for.
+ ///
+ [Input(Description = "A list of call control IDs to listen for.", UIHint = InputUIHints.MultiText)]
+ public Input> CallControlIds { get; set; } = null!;
+
+ ///
+ protected override void Execute(ActivityExecutionContext context)
+ {
+ var callControlIds = CallControlIds.Get(context);
+
+ foreach (var callControlId in callControlIds)
+ {
+ var payload = new CallAnsweredStimulus(callControlId);
+ context.CreateBookmark(new()
+ {
+ Stimulus = payload,
+ Callback = Resume,
+ BookmarkName = Type,
+ IncludeActivityInstanceId = false
+ });
+ }
+ }
+
+ private async ValueTask Resume(ActivityExecutionContext context)
+ {
+ var input = context.GetWorkflowInput(WebhookSerializerOptions.Create());
+ context.Set(Result, input);
+ await context.CompleteActivityAsync();
+ }
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/CallHangup.cs b/src/Elsa.Integrations.Telnyx/Activities/CallHangup.cs
new file mode 100644
index 00000000..259a3612
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/CallHangup.cs
@@ -0,0 +1,54 @@
+using System.Runtime.CompilerServices;
+using Elsa.Extensions;
+using Elsa.Integrations.Telnyx.Bookmarks;
+using Elsa.Integrations.Telnyx.Helpers;
+using Elsa.Integrations.Telnyx.Payloads.Call;
+using Elsa.Workflows;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using Elsa.Workflows.UIHints;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+/// Represents a Telnyx webhook event trigger.
+///
+[Activity("Telnyx", "Telnyx", "A Telnyx webhook event that executes when a call is hangup.", Kind = ActivityKind.Trigger)]
+public class CallHangup : Activity
+{
+ ///
+ public CallHangup([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// A list of call control IDs to listen for.
+ ///
+ [Input(Description = "A list of call control IDs to listen for.", UIHint = InputUIHints.MultiText)]
+ public Input> CallControlIds { get; set; } = null!;
+
+ ///
+ protected override void Execute(ActivityExecutionContext context)
+ {
+ var callControlIds = CallControlIds.Get(context);
+
+ foreach (var callControlId in callControlIds)
+ {
+ var payload = new CallHangupStimulus(callControlId);
+ context.CreateBookmark(new()
+ {
+ Stimulus = payload,
+ Callback = Resume,
+ BookmarkName = Type,
+ IncludeActivityInstanceId = false
+ });
+ }
+ }
+
+ private async ValueTask Resume(ActivityExecutionContext context)
+ {
+ var input = context.GetWorkflowInput(WebhookSerializerOptions.Create());
+ context.Set(Result, input);
+ await context.CompleteActivityAsync();
+ }
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/Dial.cs b/src/Elsa.Integrations.Telnyx/Activities/Dial.cs
new file mode 100644
index 00000000..64326d22
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/Dial.cs
@@ -0,0 +1,107 @@
+using System.Runtime.CompilerServices;
+using Elsa.Extensions;
+using Elsa.Integrations.Telnyx.Client.Models;
+using Elsa.Integrations.Telnyx.Client.Services;
+using Elsa.Integrations.Telnyx.Exceptions;
+using Elsa.Integrations.Telnyx.Extensions;
+using Elsa.Integrations.Telnyx.Options;
+using Elsa.Workflows;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using Elsa.Workflows.UIHints;
+using Microsoft.Extensions.Options;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+/// Dial a number or SIP URI.
+///
+[Activity(Constants.Namespace, "Dial a number or SIP URI.", Kind = ActivityKind.Task)]
+public class Dial : CodeActivity
+{
+ ///
+ public Dial([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// The DID or SIP URI to dial out and bridge to the given call.
+ ///
+ [Input(Description = "The DID or SIP URI to dial out and bridge to the given call.")]
+ public Input To { get; set; } = null!;
+
+ ///
+ /// 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.
+ ///
+ [Input(Description = "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.")]
+ public Input From { get; set; } = null!;
+
+ ///
+ /// 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.
+ ///
+ [Input(Description =
+ "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.")]
+ public Input FromDisplayName { get; set; } = null!;
+
+ ///
+ /// Enables answering machine detection.
+ ///
+ [Input(
+ Description = "Enables answering machine detection.",
+ UIHint = InputUIHints.DropDown,
+ Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end", "premium" },
+ DefaultValue = "disabled")]
+ public Input AnsweringMachineDetection { get; set; } = new("disabled");
+
+ ///
+ /// Start recording automatically after an event. Disabled by default.
+ ///
+ [Input(Description = "Start recording automatically after an event. Disabled by default.")]
+ public Input Record { get; set; } = null!;
+
+ ///
+ /// Defines the format of the recording ('wav' or 'mp3') when `record` is specified.
+ ///
+ [Input(
+ Description = "Defines the format of the recording ('wav' or 'mp3') when `record` is specified.",
+ UIHint = InputUIHints.DropDown,
+ Options = new[] { "wav", "mp3" },
+ DefaultValue = "mp3"
+ )]
+ public Input RecordFormat { get; set; } = new("mp3");
+
+ ///
+ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
+ {
+ var response = await DialAsync(context);
+ Result.Set(context, response);
+ }
+
+ private async Task DialAsync(ActivityExecutionContext context)
+ {
+ var telnyxOptions = context.GetRequiredService>().Value;
+ var callControlAppId = telnyxOptions.CallControlAppId;
+
+ if (callControlAppId == null)
+ throw new MissingCallControlAppIdException("No Call Control ID configured");
+
+ var fromNumber = From.GetOrDefault(context);
+ var clientState = context.CreateCorrelatingClientState();
+
+ var request = new DialRequest(
+ callControlAppId,
+ To.Get(context),
+ fromNumber,
+ FromDisplayName.GetOrDefault(context).SanitizeCallerName(),
+ AnsweringMachineDetection.GetOrDefault(context),
+ Record: Record.GetOrDefault(context) ? "record-from-answer" : null,
+ RecordFormat: RecordFormat.GetOrDefault(context) ?? "mp3",
+ ClientState: clientState
+ );
+
+ var telnyxClient = context.GetRequiredService();
+ var response = await telnyxClient.Calls.DialAsync(request, context.CancellationToken);
+
+ return response.Data;
+ }
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/DialAndWait.cs b/src/Elsa.Integrations.Telnyx/Activities/DialAndWait.cs
new file mode 100644
index 00000000..b186df3d
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/DialAndWait.cs
@@ -0,0 +1,132 @@
+using System.Runtime.CompilerServices;
+using Elsa.Extensions;
+using Elsa.Integrations.Telnyx.Attributes;
+using Elsa.Integrations.Telnyx.Bookmarks;
+using Elsa.Integrations.Telnyx.Client.Models;
+using Elsa.Integrations.Telnyx.Client.Services;
+using Elsa.Integrations.Telnyx.Exceptions;
+using Elsa.Integrations.Telnyx.Extensions;
+using Elsa.Integrations.Telnyx.Options;
+using Elsa.Integrations.Telnyx.Payloads.Abstractions;
+using Elsa.Integrations.Telnyx.Payloads.Call;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Attributes;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using Elsa.Workflows.UIHints;
+using Microsoft.Extensions.Options;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+/// Dial a number or SIP URI.
+///
+[Activity(Constants.Namespace, "Dial a number or SIP URI and wait for an event.", Kind = ActivityKind.Task)]
+[FlowNode("Answered", "Hangup")]
+[WebhookDriven(WebhookEventTypes.CallAnswered, WebhookEventTypes.CallHangup)]
+public class DialAndWait : Activity
+{
+ ///
+ public DialAndWait([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// The DID or SIP URI to dial out and bridge to the given call.
+ ///
+ [Input(Description = "The DID or SIP URI to dial out and bridge to the given call.")]
+ public Input To { get; set; } = null!;
+
+ ///
+ /// 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.
+ ///
+ [Input(Description = "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.")]
+ public Input From { get; set; } = null!;
+
+ ///
+ /// 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.
+ ///
+ [Input(Description =
+ "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.")]
+ public Input FromDisplayName { get; set; } = null!;
+
+ ///
+ /// Enables answering machine detection.
+ ///
+ [Input(
+ Description = "Enables answering machine detection.",
+ UIHint = InputUIHints.DropDown,
+ Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end", "premium" },
+ DefaultValue = "disabled")]
+ public Input AnsweringMachineDetection { get; set; } = new("disabled");
+
+ ///
+ /// Start recording automatically after an event. Disabled by default.
+ ///
+ [Input(Description = "Start recording automatically after an event. Disabled by default.")]
+ public Input Record { get; set; } = null!;
+
+ ///
+ /// Defines the format of the recording ('wav' or 'mp3') when `record` is specified.
+ ///
+ [Input(
+ Description = "Defines the format of the recording ('wav' or 'mp3') when `record` is specified.",
+ UIHint = InputUIHints.DropDown,
+ Options = new[] { "wav", "mp3" },
+ DefaultValue = "mp3"
+ )]
+ public Input RecordFormat { get; set; } = new("mp3");
+
+ ///
+ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
+ {
+ var response = await DialAsync(context);
+ var answeredBookmark = new WebhookEventStimulus(WebhookEventTypes.CallAnswered, response.CallControlId);
+ var hangupBookmark = new WebhookEventStimulus(WebhookEventTypes.CallHangup, response.CallControlId);
+
+ context.CreateBookmark(answeredBookmark, OnCallAnswered, false);
+ context.CreateBookmark(hangupBookmark, OnCallHangup, false);
+ }
+
+ private async ValueTask OnCallAnswered(ActivityExecutionContext context)
+ {
+ var payload = context.GetWorkflowInput();
+ Result.Set(context, payload);
+ await context.CompleteActivityWithOutcomesAsync("Answered");
+ }
+
+ private async ValueTask OnCallHangup(ActivityExecutionContext context)
+ {
+ var payload = context.GetWorkflowInput();
+ Result.Set(context, payload);
+ await context.CompleteActivityWithOutcomesAsync("Hangup");
+ }
+
+ private async Task DialAsync(ActivityExecutionContext context)
+ {
+ var telnyxOptions = context.GetRequiredService>().Value;
+ var callControlAppId = telnyxOptions.CallControlAppId;
+
+ if (callControlAppId == null)
+ throw new MissingCallControlAppIdException("No Call Control ID configured");
+
+ var fromNumber = From.GetOrDefault(context);
+ var clientState = context.CreateCorrelatingClientState();
+
+ var request = new DialRequest(
+ callControlAppId,
+ To.Get(context),
+ fromNumber,
+ FromDisplayName.GetOrDefault(context).SanitizeCallerName(),
+ AnsweringMachineDetection.GetOrDefault(context),
+ Record: Record.GetOrDefault(context) ? "record-from-answer" : null,
+ RecordFormat: RecordFormat.GetOrDefault(context) ?? "mp3",
+ ClientState: clientState
+ );
+
+ var telnyxClient = context.GetRequiredService();
+ var response = await telnyxClient.Calls.DialAsync(request, context.CancellationToken);
+
+ return response.Data;
+ }
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/FlowAnswerCall.cs b/src/Elsa.Integrations.Telnyx/Activities/FlowAnswerCall.cs
new file mode 100644
index 00000000..05594bca
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/FlowAnswerCall.cs
@@ -0,0 +1,22 @@
+using System.Runtime.CompilerServices;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Attributes;
+using Elsa.Workflows.Activities.Flowchart.Models;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+[FlowNode("Connected", "Disconnected")]
+public class FlowAnswerCall : AnswerCallBase
+{
+ ///
+ public FlowAnswerCall([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ protected override async ValueTask HandleConnectedAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync(new Outcomes("Connected"));
+
+ ///
+ protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync(new Outcomes("Disconnected"));
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/FlowBridgeCalls.cs b/src/Elsa.Integrations.Telnyx/Activities/FlowBridgeCalls.cs
new file mode 100644
index 00000000..c61b2be7
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/FlowBridgeCalls.cs
@@ -0,0 +1,21 @@
+using System.Runtime.CompilerServices;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Attributes;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+[FlowNode("Bridged", "Disconnected")]
+public class FlowBridgeCalls : BridgeCallsBase
+{
+ ///
+ public FlowBridgeCalls([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityAsync("Disconnected");
+
+ ///
+ protected override ValueTask HandleBridgedAsync(ActivityExecutionContext context) => context.CompleteActivityAsync("Bridged");
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/FlowHangupCall.cs b/src/Elsa.Integrations.Telnyx/Activities/FlowHangupCall.cs
new file mode 100644
index 00000000..184e4df7
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/FlowHangupCall.cs
@@ -0,0 +1,21 @@
+using System.Runtime.CompilerServices;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Attributes;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+[FlowNode("Done", "Disconnected")]
+public class FlowHangupCall : HangupCallBase
+{
+ ///
+ public FlowHangupCall([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ protected override ValueTask HandleDoneAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Done");
+
+ ///
+ protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected");
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/FlowPlayAudio.cs b/src/Elsa.Integrations.Telnyx/Activities/FlowPlayAudio.cs
new file mode 100644
index 00000000..835746f2
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/FlowPlayAudio.cs
@@ -0,0 +1,21 @@
+using System.Runtime.CompilerServices;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Attributes;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+[FlowNode("Playback started", "Disconnected")]
+public class FlowPlayAudio : PlayAudioBase
+{
+ ///
+ public FlowPlayAudio([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ protected override ValueTask HandlePlaybackStartedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Playback started");
+
+ ///
+ protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected");
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/FlowSpeakText.cs b/src/Elsa.Integrations.Telnyx/Activities/FlowSpeakText.cs
new file mode 100644
index 00000000..d324f307
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/FlowSpeakText.cs
@@ -0,0 +1,21 @@
+using System.Runtime.CompilerServices;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Attributes;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+[FlowNode("Done", "Finished speaking", "Disconnected")]
+public class FlowSpeakText : SpeakTextBase
+{
+ ///
+ public FlowSpeakText([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ protected override async ValueTask HandleDisconnected(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Disconnected", "Done");
+
+ ///
+ protected override async ValueTask HandleSpeakingHasFinished(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Finished speaking", "Done");
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/FlowStartRecording.cs b/src/Elsa.Integrations.Telnyx/Activities/FlowStartRecording.cs
new file mode 100644
index 00000000..fa7a6119
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/FlowStartRecording.cs
@@ -0,0 +1,21 @@
+using System.Runtime.CompilerServices;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Attributes;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+[FlowNode("Recording finished", "Disconnected")]
+public class FlowStartRecording : StartRecordingBase
+{
+ ///
+ public FlowStartRecording([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected");
+
+ ///
+ protected override ValueTask HandleCallRecordingSavedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Recording finished");
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/FlowStopAudioPlayback.cs b/src/Elsa.Integrations.Telnyx/Activities/FlowStopAudioPlayback.cs
new file mode 100644
index 00000000..8c277f24
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/FlowStopAudioPlayback.cs
@@ -0,0 +1,21 @@
+using System.Runtime.CompilerServices;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Attributes;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+[FlowNode("Done", "Disconnected")]
+public class FlowStopAudioPlayback : StopAudioPlaybackBase
+{
+ ///
+ public FlowStopAudioPlayback([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ protected override ValueTask HandleDoneAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Done");
+
+ ///
+ protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected");
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/GatherUsingAudio.cs b/src/Elsa.Integrations.Telnyx/Activities/GatherUsingAudio.cs
new file mode 100644
index 00000000..df359707
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/GatherUsingAudio.cs
@@ -0,0 +1,158 @@
+using System.Runtime.CompilerServices;
+using Elsa.Extensions;
+using Elsa.Integrations.Telnyx.Attributes;
+using Elsa.Integrations.Telnyx.Bookmarks;
+using Elsa.Integrations.Telnyx.Client.Models;
+using Elsa.Integrations.Telnyx.Client.Services;
+using Elsa.Integrations.Telnyx.Extensions;
+using Elsa.Integrations.Telnyx.Payloads.Call;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Attributes;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using Elsa.Workflows.Runtime;
+using Refit;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+/// Play an audio file on the call until the required DTMF signals are gathered to build interactive menus.
+///
+[Activity(Constants.Namespace, "Play an audio file on the call until the required DTMF signals are gathered to build interactive menus.", Kind = ActivityKind.Task)]
+[FlowNode("Valid input", "Invalid input", "Disconnected")]
+[WebhookDriven(WebhookEventTypes.CallGatherEnded)]
+public class GatherUsingAudio : Activity, IBookmarksPersistedHandler
+{
+ ///
+ public GatherUsingAudio([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// The call control ID of the call from which to gather input. Leave empty to use the ambient call control ID, if there is any.
+ ///
+ [Input(DisplayName = "Call Control ID", Description = "The call control ID of the call from which to gather input. Leave empty to use the ambient call control ID, if there is any.", Category = "Advanced")]
+ public Input CallControlId { get; set; } = null!;
+
+ ///
+ /// The URL of a file to be played back at the beginning of each prompt. The URL can point to either a WAV or MP3 file.
+ ///
+ [Input(
+ DisplayName = "Audio URL",
+ Description = "The URL of a file to be played back at the beginning of each prompt. The URL can point to either a WAV or MP3 file."
+ )]
+ public Input? AudioUrl { get; set; } = null!;
+
+ ///
+ /// The number of milliseconds to wait for input between digits.
+ ///
+ [Input(
+ DisplayName = "Inter Digit Timeout",
+ Description = "The number of milliseconds to wait for input between digits.",
+ Category = "Advanced",
+ DefaultValue = 5000
+ )]
+ public Input? InterDigitTimeoutMillis { get; set; } = new(5000);
+
+ ///
+ /// The URL of a file to play when digits don't match the Valid Digits setting or the number of digits is not between Min and Max. The URL can point to either a WAV or MP3 file.
+ ///
+ [Input(
+ DisplayName = "Invalid Audio Url",
+ Description = "The URL of a file to play when digits don't match the Valid Digits setting or the number of digits is not between Min and Max. The URL can point to either a WAV or MP3 file."
+ )]
+ public Input? InvalidAudioUrl { get; set; }
+
+ ///
+ /// A list of all digits accepted as valid.
+ ///
+ [Input(
+ Description = "A list of all digits accepted as valid.",
+ Category = "Advanced",
+ DefaultValue = "0123456789#*"
+ )]
+ public Input? ValidDigits { get; set; } = new("0123456789#*");
+
+ ///
+ /// The minimum number of digits to fetch. This parameter has a minimum value of 1.
+ ///
+ [Input(Description = "The minimum number of digits to fetch. This parameter has a minimum value of 1.", DefaultValue = 1)]
+ public Input? MinimumDigits { get; set; } = new(1);
+
+ ///
+ /// The maximum number of digits to fetch. This parameter has a maximum value of 128.
+ ///
+ [Input(Description = "The maximum number of digits to fetch. This parameter has a maximum value of 128.", DefaultValue = 128)]
+ public Input? MaximumDigits { get; set; } = new(128);
+
+ ///
+ /// The maximum number of times the file should be played if there is no input from the user on the call.
+ ///
+ [Input(Description = "The maximum number of times the file should be played if there is no input from the user on the call.", DefaultValue = 3)]
+ public Input? MaximumTries { get; set; } = new(3);
+
+ ///
+ /// The digit used to terminate input if fewer than `maximum_digits` digits have been gathered.
+ ///
+ [Input(Description = "The digit used to terminate input if fewer than `maximum_digits` digits have been gathered.", DefaultValue = "#")]
+ public Input? TerminatingDigit { get; set; } = new("#");
+
+ ///
+ /// The number of milliseconds to wait for a DTMF response after file playback ends before a replaying the sound file.
+ ///
+ [Input(
+ DisplayName = "Timeout",
+ Description = "The number of milliseconds to wait for a DTMF response after file playback ends before a replaying the sound file.",
+ Category = "Advanced",
+ DefaultValue = 60000
+ )]
+ public Input? TimeoutMillis { get; set; } = new(60000);
+
+ ///
+ /// Calls out to Telnyx to actually begin gathering input.
+ ///
+ public async ValueTask BookmarksPersistedAsync(ActivityExecutionContext context)
+ {
+ var request = new GatherUsingAudioRequest(
+ AudioUrl.Get(context) ?? throw new("AudioUrl is required."),
+ context.CreateCorrelatingClientState(),
+ null,
+ InterDigitTimeoutMillis.Get(context),
+ InvalidAudioUrl.Get(context),
+ MaximumDigits.Get(context),
+ MaximumTries.Get(context),
+ MinimumDigits.Get(context),
+ TerminatingDigit.Get(context).EmptyToNull(),
+ TimeoutMillis.Get(context),
+ ValidDigits.Get(context).EmptyToNull()
+ );
+
+ var callControlId = CallControlId.Get(context);
+ var telnyxClient = context.GetRequiredService();
+
+ try
+ {
+ await telnyxClient.Calls.GatherUsingAudioAsync(callControlId, request, context.CancellationToken);
+ }
+ catch (ApiException e)
+ {
+ if (!await e.CallIsNoLongerActiveAsync()) throw;
+ await context.CompleteActivityWithOutcomesAsync("Disconnected");
+ }
+ }
+
+ ///
+ protected override void Execute(ActivityExecutionContext context)
+ {
+ var callControlId = CallControlId.Get(context);
+ context.CreateBookmark(new WebhookEventStimulus(WebhookEventTypes.CallGatherEnded, callControlId), ResumeAsync);
+ }
+
+ private async ValueTask ResumeAsync(ActivityExecutionContext context)
+ {
+ var payload = context.GetWorkflowInput();
+ var outcome = payload.Status == "valid" ? "Valid input" : "Invalid input";
+ context.Set(Result, payload);
+ await context.CompleteActivityWithOutcomesAsync(outcome);
+ }
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/GatherUsingSpeak.cs b/src/Elsa.Integrations.Telnyx/Activities/GatherUsingSpeak.cs
new file mode 100644
index 00000000..fd704105
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/GatherUsingSpeak.cs
@@ -0,0 +1,190 @@
+using System.Runtime.CompilerServices;
+using Elsa.Extensions;
+using Elsa.Integrations.Telnyx.Attributes;
+using Elsa.Integrations.Telnyx.Bookmarks;
+using Elsa.Integrations.Telnyx.Client.Models;
+using Elsa.Integrations.Telnyx.Client.Services;
+using Elsa.Integrations.Telnyx.Extensions;
+using Elsa.Integrations.Telnyx.Payloads.Call;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Attributes;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using Elsa.Workflows.UIHints;
+using Refit;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+/// Convert text to speech and play it on the call until the required DTMF signals are gathered to build interactive menus.
+///
+[Activity(Constants.Namespace, "Convert text to speech and play it on the call until the required DTMF signals are gathered to build interactive menus.", Kind = ActivityKind.Task)]
+[FlowNode("Valid input", "Invalid input", "Disconnected")]
+[WebhookDriven(WebhookEventTypes.CallGatherEnded)]
+public class GatherUsingSpeak : Activity
+{
+ ///
+ public GatherUsingSpeak([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// The call control ID of the call from which to gather input. Leave empty to use the ambient call control ID, if there is any.
+ ///
+ [Input(DisplayName = "Call Control ID", Description = "The call control ID of the call from which to gather input. Leave empty to use the ambient call control ID, if there is any.", Category = "Advanced")]
+ public Input CallControlId { get; set; } = null!;
+
+ ///
+ /// The language you want spoken.
+ ///
+ [Input(
+ Description = "The language you want spoken.",
+ UIHint = InputUIHints.DropDown,
+ Options = new[] { "en-US", "en-AU", "nl-NL", "es-ES", "ru-RU" },
+ DefaultValue = "en-US"
+ )]
+ public Input Language { get; set; } = new("en-US");
+
+ ///
+ /// The gender of the voice used to speak back the text.
+ ///
+ [Input(
+ Description = "The gender of the voice used to speak back the text.",
+ UIHint = InputUIHints.DropDown,
+ Options = new[] { "female", "male" },
+ DefaultValue = "female"
+ )]
+ public Input Voice { get; set; } = new("female");
+
+ ///
+ /// The text or SSML to be converted into speech. There is a 5,000 character limit.
+ ///
+ [Input(
+ Description = "The text or SSML to be converted into speech. There is a 5,000 character limit.",
+ UIHint = InputUIHints.MultiLine
+ )]
+ public Input Payload { get; set; } = null!;
+
+ ///
+ /// The type of the provided payload. The payload can either be plain text, or Speech Synthesis Markup Language (SSML).
+ ///
+ [Input(
+ Description = "The type of the provided payload. The payload can either be plain text, or Speech Synthesis Markup Language (SSML).",
+ UIHint = InputUIHints.DropDown,
+ Options = new[] { "", "text", "ssml" }
+ )]
+ public Input? PayloadType { get; set; }
+
+ ///
+ /// "This parameter impacts speech quality, language options and payload types. When using `basic`, only the `en-US` language and payload type `text` are allowed."
+ ///
+ [Input(
+ Description = "This parameter impacts speech quality, language options and payload types. When using `basic`, only the `en-US` language and payload type `text` are allowed.",
+ UIHint = InputUIHints.DropDown,
+ Options = new[] { "", "basic", "premium" },
+ Category = "Advanced"
+ )]
+ public Input? ServiceLevel { get; set; }
+
+ ///
+ /// The number of milliseconds to wait for input between digits.
+ ///
+ [Input(
+ DisplayName = "Inter Digit Timeout",
+ Description = "The number of milliseconds to wait for input between digits.",
+ Category = "Advanced",
+ DefaultValue = 5000
+ )]
+ public Input? InterDigitTimeoutMillis { get; set; } = new(5000);
+
+ ///
+ /// A list of all digits accepted as valid.
+ ///
+ [Input(
+ Description = "A list of all digits accepted as valid.",
+ Category = "Advanced",
+ DefaultValue = "0123456789#*"
+ )]
+ public Input? ValidDigits { get; set; } = new("0123456789#*");
+
+ ///
+ /// The minimum number of digits to fetch. This parameter has a minimum value of 1.
+ ///
+ [Input(Description = "The minimum number of digits to fetch. This parameter has a minimum value of 1.", DefaultValue = 1)]
+ public Input? MinimumDigits { get; set; } = new(1);
+
+ ///
+ /// The maximum number of digits to fetch. This parameter has a maximum value of 128.
+ ///
+ [Input(Description = "The maximum number of digits to fetch. This parameter has a maximum value of 128.", DefaultValue = 128)]
+ public Input? MaximumDigits { get; set; } = new(128);
+
+ ///
+ /// The maximum number of times the file should be played if there is no input from the user on the call.
+ ///
+ [Input(Description = "The maximum number of times the file should be played if there is no input from the user on the call.", DefaultValue = 3)]
+ public Input? MaximumTries { get; set; } = new(3);
+
+ ///
+ /// The digit used to terminate input if fewer than `maximum_digits` digits have been gathered.
+ ///
+ [Input(Description = "The digit used to terminate input if fewer than `maximum_digits` digits have been gathered.", DefaultValue = "#")]
+ public Input? TerminatingDigit { get; set; } = new("#");
+
+ ///
+ /// The number of milliseconds to wait for a DTMF response after file playback ends before a replaying the sound file.
+ ///
+ [Input(
+ DisplayName = "Timeout",
+ Description = "The number of milliseconds to wait for a DTMF response after file playback ends before a replaying the sound file.",
+ Category = "Advanced",
+ DefaultValue = 60000
+ )]
+ public Input? TimeoutMillis { get; set; } = new(60000);
+
+ ///
+ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
+ {
+ var callControlId = CallControlId.Get(context);
+
+ var request = new GatherUsingSpeakRequest(
+ Language.Get(context) ?? throw new("Language is required."),
+ Voice.Get(context) ?? throw new("Voice is required."),
+ Payload.Get(context) ?? throw new("Payload is required."),
+ PayloadType.GetOrDefault(context),
+ ServiceLevel.GetOrDefault(context),
+ InterDigitTimeoutMillis.GetOrDefault(context),
+ MaximumDigits.GetOrDefault(context),
+ MaximumTries.GetOrDefault(context),
+ MinimumDigits.GetOrDefault(context),
+ TerminatingDigit.GetOrDefault(context).EmptyToNull(),
+ TimeoutMillis.GetOrDefault(context),
+ ValidDigits.GetOrDefault(context).EmptyToNull(),
+ context.CreateCorrelatingClientState(context.Id)
+ );
+
+ var telnyxClient = context.GetRequiredService();
+
+ try
+ {
+ // Send the request to Telnyx.
+ await telnyxClient.Calls.GatherUsingSpeakAsync(callControlId, request, context.CancellationToken);
+
+ // Create a bookmark so we can resume this activity when the call.gather.ended webhook comes back.
+ context.CreateBookmark(new WebhookEventStimulus(WebhookEventTypes.CallGatherEnded, callControlId), ResumeAsync, true);
+ }
+ catch (ApiException e)
+ {
+ if (!await e.CallIsNoLongerActiveAsync()) throw;
+ await context.CompleteActivityWithOutcomesAsync("Disconnected");
+ }
+ }
+
+ private async ValueTask ResumeAsync(ActivityExecutionContext context)
+ {
+ var payload = context.GetWorkflowInput();
+ var outcome = payload.Status == "valid" ? "Valid input" : "Invalid input";
+ context.Set(Result, payload);
+ await context.CompleteActivityWithOutcomesAsync(outcome);
+ }
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/GetCallStatus.cs b/src/Elsa.Integrations.Telnyx/Activities/GetCallStatus.cs
new file mode 100644
index 00000000..6dd96d9e
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/GetCallStatus.cs
@@ -0,0 +1,44 @@
+using System.Runtime.CompilerServices;
+using Elsa.Extensions;
+using Elsa.Integrations.Telnyx.Client.Services;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Attributes;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+[FlowNode("Alive", "Dead", "Done")]
+[Activity(Constants.Namespace, "Get the status of a call.", Kind = ActivityKind.Task)]
+public class GetCallStatus : Activity
+{
+ ///
+ public GetCallStatus([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// Unique identifier and token for controlling the call.
+ ///
+ [Input(
+ DisplayName = "Call Control ID",
+ Description = "Unique identifier and token for controlling the call.",
+ Category = "Advanced"
+ )]
+ public Input CallControlId { get; set; } = null!;
+
+ ///
+ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
+ {
+ var client = context.GetRequiredService();
+ var callControlId = CallControlId.Get(context);
+ var response = await client.Calls.GetStatusAsync(callControlId, context.CancellationToken);
+ var isAlive = response.Data.IsAlive;
+ var outcome = isAlive ? "Alive" : "Dead";
+
+ Result.Set(context, isAlive);
+
+ await context.CompleteActivityWithOutcomesAsync(outcome, "Done");
+ }
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/HangupCall.cs b/src/Elsa.Integrations.Telnyx/Activities/HangupCall.cs
new file mode 100644
index 00000000..405a06eb
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/HangupCall.cs
@@ -0,0 +1,32 @@
+using System.Runtime.CompilerServices;
+using Elsa.Workflows;
+using Elsa.Workflows.Attributes;
+using JetBrains.Annotations;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+[PublicAPI]
+public class HangupCall : HangupCallBase
+{
+ ///
+ public HangupCall([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// The to execute when the call was no longer active.
+ ///
+ [Port] public IActivity? Disconnected { get; set; }
+
+ ///
+ protected override async ValueTask HandleDoneAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync(OnCompletedAsync);
+
+ ///
+ protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync);
+
+ ///
+ /// Executed when any child activity completed.
+ ///
+ private async ValueTask OnCompletedAsync(ActivityCompletedContext context) => await context.TargetContext.CompleteActivityAsync();
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/HangupCallBase.cs b/src/Elsa.Integrations.Telnyx/Activities/HangupCallBase.cs
new file mode 100644
index 00000000..ee803f85
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/HangupCallBase.cs
@@ -0,0 +1,57 @@
+using Elsa.Extensions;
+using Elsa.Integrations.Telnyx.Client.Models;
+using Elsa.Integrations.Telnyx.Client.Services;
+using Elsa.Integrations.Telnyx.Extensions;
+using Elsa.Workflows;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using Refit;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+/// Hang up the call.
+///
+[Activity(Constants.Namespace, "Hang up the call.", Kind = ActivityKind.Task)]
+public abstract class HangupCallBase : Activity
+{
+ ///
+ protected HangupCallBase(string? source = null, int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// Unique identifier and token for controlling the call.
+ ///
+ [Input(DisplayName = "Call Control ID", Description = "Unique identifier and token for controlling the call.", Category = "Advanced")]
+ public Input CallControlId { get; set; } = null!;
+
+ ///
+ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
+ {
+ var callControlId = CallControlId.Get(context);
+ var request = new HangupCallRequest(ClientState: context.CreateCorrelatingClientState());
+ var telnyxClient = context.GetRequiredService();
+
+ try
+ {
+ await telnyxClient.Calls.HangupCallAsync(callControlId, request, context.CancellationToken);
+ await HandleDoneAsync(context);
+ }
+ catch (ApiException e)
+ {
+ if (!await e.CallIsNoLongerActiveAsync()) throw;
+ await HandleDisconnectedAsync(context);
+ }
+ }
+
+ ///
+ /// Executed when the call was hangup.
+ ///
+ protected abstract ValueTask HandleDoneAsync(ActivityExecutionContext context);
+
+ ///
+ /// Executed when the call was no longer active.
+ ///
+ protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context);
+}
\ No newline at end of file
diff --git a/src/Elsa.Integrations.Telnyx/Activities/IncomingCall.cs b/src/Elsa.Integrations.Telnyx/Activities/IncomingCall.cs
new file mode 100644
index 00000000..99895da9
--- /dev/null
+++ b/src/Elsa.Integrations.Telnyx/Activities/IncomingCall.cs
@@ -0,0 +1,88 @@
+using System.Runtime.CompilerServices;
+using Elsa.Expressions.Models;
+using Elsa.Extensions;
+using Elsa.Integrations.Telnyx.Bookmarks;
+using Elsa.Integrations.Telnyx.Helpers;
+using Elsa.Integrations.Telnyx.Models;
+using Elsa.Integrations.Telnyx.Payloads.Call;
+using Elsa.Workflows;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using Elsa.Workflows.UIHints;
+
+namespace Elsa.Integrations.Telnyx.Activities;
+
+///
+/// Triggered when an inbound phone call is received for any of the specified source or destination phone numbers.
+///
+[Activity(
+ "Telnyx",
+ "Telnyx",
+ "Triggered when an inbound phone call is received for any of the specified source or destination phone numbers.",
+ Kind = ActivityKind.Trigger)]
+public class IncomingCall : Trigger
+{
+ ///
+ public IncomingCall([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
+ {
+ }
+
+ ///
+ /// A list of destination numbers to respond to.
+ ///
+ [Input(Description = "A list of destination numbers to respond to.", UIHint = InputUIHints.MultiText)]
+ public Input> To { get; set; } = null!;
+
+ ///
+ /// A list of source numbers to respond to.
+ ///
+ [Input(Description = "A list of source numbers to respond to.", UIHint = InputUIHints.MultiText)]
+ public Input> From { get; set; } = null!;
+
+ ///
+ /// Match any inbound calls.
+ ///
+ [Input(Description = "Match any inbound calls.")]
+ public Input CatchAll { get; set; } = null!;
+
+ ///
+ protected override IEnumerable