Merge pull request #6437 from elsa-workflows/task/remove-telnyx

Remove Telnyx module from the codebase
This commit is contained in:
Sipke Schoorstra 2025-02-23 14:48:01 +01:00 committed by GitHub
commit 28ad3b3ed7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
115 changed files with 0 additions and 3991 deletions

View file

@ -76,8 +76,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Common", "src\modules\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.EntityFrameworkCore", "src\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj", "{7CC95512-7F44-45C3-A669-95B7048D4730}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Telnyx", "src\modules\Elsa.Telnyx\Elsa.Telnyx.csproj", "{F09B30F7-AF33-44D7-B835-099D1F72A37E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Email", "src\modules\Elsa.Email\Elsa.Email.csproj", "{1E5BD8D9-55BE-41E2-B389-6AB9F26C22AF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E5482-0482-448C-B9E4-EC67A9474B85}"
@ -511,10 +509,6 @@ Global
{7CC95512-7F44-45C3-A669-95B7048D4730}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CC95512-7F44-45C3-A669-95B7048D4730}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CC95512-7F44-45C3-A669-95B7048D4730}.Release|Any CPU.Build.0 = Release|Any CPU
{F09B30F7-AF33-44D7-B835-099D1F72A37E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F09B30F7-AF33-44D7-B835-099D1F72A37E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F09B30F7-AF33-44D7-B835-099D1F72A37E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F09B30F7-AF33-44D7-B835-099D1F72A37E}.Release|Any CPU.Build.0 = Release|Any CPU
{1E5BD8D9-55BE-41E2-B389-6AB9F26C22AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E5BD8D9-55BE-41E2-B389-6AB9F26C22AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E5BD8D9-55BE-41E2-B389-6AB9F26C22AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -926,7 +920,6 @@ Global
{30B49E42-AE61-4F7C-981F-923F07BB3986} = {5948B0A5-7873-4DBB-BA03-EB283D6EA91B}
{D229105F-6879-4452-9189-75DE060C0F4C} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{7CC95512-7F44-45C3-A669-95B7048D4730} = {9B4F139F-7D26-435C-A561-89E65A67A8E5}
{F09B30F7-AF33-44D7-B835-099D1F72A37E} = {EBD0CF78-C5D7-4B4B-94C9-C5D8C20B6F59}
{1E5BD8D9-55BE-41E2-B389-6AB9F26C22AF} = {EBD0CF78-C5D7-4B4B-94C9-C5D8C20B6F59}
{3246883E-2FA7-4B4A-BDC5-99039A2869BC} = {9B4F139F-7D26-435C-A561-89E65A67A8E5}
{2BBFDE36-28A7-4875-8EBE-CC25C76B97FF} = {EBD0CF78-C5D7-4B4B-94C9-C5D8C20B6F59}

View file

@ -1,32 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
public class AnswerCall : AnswerCallBase
{
/// <inheritdoc />
public AnswerCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The activity to schedule when the call was successfully answered.
/// </summary>
[Port]
public IActivity? Connected { get; set; }
/// <summary>
/// The activity to schedule when the call was no longer active.
/// </summary>
[Port]
public IActivity? Disconnected { get; set; }
/// <inheritdoc />
protected override async ValueTask HandleConnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Connected);
/// <inheritdoc />
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected);
}

View file

@ -1,74 +0,0 @@
using Elsa.Extensions;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Answer an incoming call. You must issue this command before executing subsequent commands on an incoming call.
/// </summary>
[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<CallAnsweredPayload>
{
/// <inheritdoc />
protected AnswerCallBase(string? source = default, int? line = default) : base(source, line)
{
}
/// <summary>
/// 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.
/// </summary>
[Input(DisplayName = "Call Control ID", Description = "The call control ID of the call to answer.", Category = "Advanced")]
public Input<string> CallControlId { get; set; } = default!;
/// <inheritdoc />
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<ITelnyxClient>();
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);
}
}
/// <summary>
/// Invoked when the call was successfully answered.
/// </summary>
protected abstract ValueTask HandleConnectedAsync(ActivityExecutionContext context);
/// <summary>
/// Invoked when the call was no longer active.
/// </summary>
protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context);
private async ValueTask ResumeAsync(ActivityExecutionContext context)
{
var payload = context.GetWorkflowInput<CallAnsweredPayload>();
context.Set(Result, payload);
await HandleConnectedAsync(context);
}
}

View file

@ -1,34 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[PublicAPI]
public class BridgeCalls : BridgeCallsBase
{
/// <inheritdoc />
public BridgeCalls([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The <see cref="IActivity"/> to execute when the source leg call is no longer active.
/// </summary>
[Port]
public IActivity? Disconnected { get; set; }
/// <summary>
/// The <see cref="IActivity"/> to execute when the two calls are bridged.
/// </summary>
[Port]
public IActivity? Bridged { get; set; }
/// <inheritdoc />
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompleted);
/// <inheritdoc />
protected override async ValueTask HandleBridgedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Bridged, OnCompleted);
}

View file

@ -1,98 +0,0 @@
using Elsa.Extensions;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Models;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Bridge two calls.
/// </summary>
[Activity(Constants.Namespace, "Bridge two calls.", Kind = ActivityKind.Task)]
[PublicAPI]
public abstract class BridgeCallsBase : Activity<BridgedCallsOutput>
{
/// <inheritdoc />
protected BridgeCallsBase(string? source = default, int? line = default) : base(source, line)
{
}
/// <summary>
/// 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.
/// </summary>
[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<string> CallControlIdA { get; set; } = default!;
/// <summary>
/// The destination call control ID of the call you want to bridge with.
/// </summary>
[Input(DisplayName = "Call Control ID B", Description = "The destination call control ID of the call you want to bridge with.")]
public Input<string> CallControlIdB { get; set; } = default!;
/// <inheritdoc />
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<ITelnyxClient>();
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(new[] { bookmarkA, bookmarkB }, ResumeAsync, false);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await HandleDisconnectedAsync(context);
}
}
/// <summary>
/// Called when the call is disconnected.
/// </summary>
protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context);
/// <summary>
/// Called when the call is bridged.
/// </summary> <param name="context"></param>
/// <returns></returns>
protected abstract ValueTask HandleBridgedAsync(ActivityExecutionContext context);
/// <summary>
/// Called when the activity is completed.
/// </summary>
protected async ValueTask OnCompleted(ActivityCompletedContext context) => await context.TargetContext.CompleteActivityAsync();
private async ValueTask ResumeAsync(ActivityExecutionContext context)
{
var payload = context.GetWorkflowInput<CallBridgedPayload>()!;
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<CallBridgedPayload>("CallBridgedPayloadA");
var callBridgedPayloadB = context.GetProperty<CallBridgedPayload>("CallBridgedPayloadB");
if (callBridgedPayloadA != null && callBridgedPayloadB != null)
{
context.Set(Result, new BridgedCallsOutput(callBridgedPayloadA, callBridgedPayloadB));
await HandleBridgedAsync(context);
}
}
}

View file

@ -1,54 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Helpers;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Represents a Telnyx webhook event trigger.
/// </summary>
[Activity("Telnyx", "Telnyx", "A Telnyx webhook event that executes when a call is answered.", Kind = ActivityKind.Trigger)]
public class CallAnswered : Activity<CallAnsweredPayload>
{
/// <inheritdoc />
public CallAnswered([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// A list of call control IDs to listen for.
/// </summary>
[Input(Description = "A list of call control IDs to listen for.", UIHint = InputUIHints.MultiText)]
public Input<ICollection<string>> CallControlIds { get; set; } = default!;
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
var callControlIds = CallControlIds.Get(context);
foreach (var callControlId in callControlIds)
{
var payload = new CallAnsweredStimulus(callControlId);
context.CreateBookmark(new CreateBookmarkArgs
{
Stimulus = payload,
Callback = Resume,
BookmarkName = Type,
IncludeActivityInstanceId = false
});
}
}
private async ValueTask Resume(ActivityExecutionContext context)
{
var input = context.GetWorkflowInput<CallAnsweredPayload>(WebhookSerializerOptions.Create());
context.Set(Result, input);
await context.CompleteActivityAsync();
}
}

View file

@ -1,54 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Helpers;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Represents a Telnyx webhook event trigger.
/// </summary>
[Activity("Telnyx", "Telnyx", "A Telnyx webhook event that executes when a call is hangup.", Kind = ActivityKind.Trigger)]
public class CallHangup : Activity<CallHangupPayload>
{
/// <inheritdoc />
public CallHangup([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// A list of call control IDs to listen for.
/// </summary>
[Input(Description = "A list of call control IDs to listen for.", UIHint = InputUIHints.MultiText)]
public Input<ICollection<string>> CallControlIds { get; set; } = default!;
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
var callControlIds = CallControlIds.Get(context);
foreach (var callControlId in callControlIds)
{
var payload = new CallHangupStimulus(callControlId);
context.CreateBookmark(new CreateBookmarkArgs
{
Stimulus = payload,
Callback = Resume,
BookmarkName = Type,
IncludeActivityInstanceId = false
});
}
}
private async ValueTask Resume(ActivityExecutionContext context)
{
var input = context.GetWorkflowInput<CallHangupPayload>(WebhookSerializerOptions.Create());
context.Set(Result, input);
await context.CompleteActivityAsync();
}
}

View file

@ -1,107 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Exceptions;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Options;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
using Microsoft.Extensions.Options;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Dial a number or SIP URI.
/// </summary>
[Activity(Constants.Namespace, "Dial a number or SIP URI.", Kind = ActivityKind.Task)]
public class Dial : CodeActivity<DialResponse>
{
/// <inheritdoc />
public Dial([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The DID or SIP URI to dial out and bridge to the given call.
/// </summary>
[Input(Description = "The DID or SIP URI to dial out and bridge to the given call.")]
public Input<string> To { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
[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<string?> From { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
[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<string?> FromDisplayName { get; set; } = default!;
/// <summary>
/// Enables answering machine detection.
/// </summary>
[Input(
Description = "Enables answering machine detection.",
UIHint = InputUIHints.DropDown,
Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end", "premium" },
DefaultValue = "disabled")]
public Input<string?> AnsweringMachineDetection { get; set; } = new("disabled");
/// <summary>
/// Start recording automatically after an event. Disabled by default.
/// </summary>
[Input(Description = "Start recording automatically after an event. Disabled by default.")]
public Input<bool> Record { get; set; } = default!;
/// <summary>
/// Defines the format of the recording ('wav' or 'mp3') when `record` is specified.
/// </summary>
[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<string> RecordFormat { get; set; } = new("mp3");
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var response = await DialAsync(context);
Result.Set(context, response);
}
private async Task<DialResponse> DialAsync(ActivityExecutionContext context)
{
var telnyxOptions = context.GetRequiredService<IOptions<TelnyxOptions>>().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" : default,
RecordFormat: RecordFormat.GetOrDefault(context) ?? "mp3",
ClientState: clientState
);
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
var response = await telnyxClient.Calls.DialAsync(request, context.CancellationToken);
return response.Data;
}
}

View file

@ -1,132 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Exceptions;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Options;
using Elsa.Telnyx.Payloads.Abstractions;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
using Microsoft.Extensions.Options;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Dial a number or SIP URI.
/// </summary>
[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<CallPayload>
{
/// <inheritdoc />
public DialAndWait([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The DID or SIP URI to dial out and bridge to the given call.
/// </summary>
[Input(Description = "The DID or SIP URI to dial out and bridge to the given call.")]
public Input<string> To { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
[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<string?> From { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
[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<string?> FromDisplayName { get; set; } = default!;
/// <summary>
/// Enables answering machine detection.
/// </summary>
[Input(
Description = "Enables answering machine detection.",
UIHint = InputUIHints.DropDown,
Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end", "premium" },
DefaultValue = "disabled")]
public Input<string?> AnsweringMachineDetection { get; set; } = new("disabled");
/// <summary>
/// Start recording automatically after an event. Disabled by default.
/// </summary>
[Input(Description = "Start recording automatically after an event. Disabled by default.")]
public Input<bool> Record { get; set; } = default!;
/// <summary>
/// Defines the format of the recording ('wav' or 'mp3') when `record` is specified.
/// </summary>
[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<string> RecordFormat { get; set; } = new("mp3");
/// <inheritdoc />
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<CallAnsweredPayload>();
Result.Set(context, payload);
await context.CompleteActivityWithOutcomesAsync("Answered");
}
private async ValueTask OnCallHangup(ActivityExecutionContext context)
{
var payload = context.GetWorkflowInput<CallHangupPayload>();
Result.Set(context, payload);
await context.CompleteActivityWithOutcomesAsync("Hangup");
}
private async Task<DialResponse> DialAsync(ActivityExecutionContext context)
{
var telnyxOptions = context.GetRequiredService<IOptions<TelnyxOptions>>().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" : default,
RecordFormat: RecordFormat.GetOrDefault(context) ?? "mp3",
ClientState: clientState
);
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
var response = await telnyxClient.Calls.DialAsync(request, context.CancellationToken);
return response.Data;
}
}

View file

@ -1,22 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
using Elsa.Workflows.Activities.Flowchart.Models;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Connected", "Disconnected")]
public class FlowAnswerCall : AnswerCallBase
{
/// <inheritdoc />
public FlowAnswerCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
protected override async ValueTask HandleConnectedAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync(new Outcomes("Connected"));
/// <inheritdoc />
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync(new Outcomes("Disconnected"));
}

View file

@ -1,21 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Bridged", "Disconnected")]
public class FlowBridgeCalls : BridgeCallsBase
{
/// <inheritdoc />
public FlowBridgeCalls([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityAsync("Disconnected");
/// <inheritdoc />
protected override ValueTask HandleBridgedAsync(ActivityExecutionContext context) => context.CompleteActivityAsync("Bridged");
}

View file

@ -1,21 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Done", "Disconnected")]
public class FlowHangupCall : HangupCallBase
{
/// <inheritdoc />
public FlowHangupCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
protected override ValueTask HandleDoneAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Done");
/// <inheritdoc />
protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected");
}

View file

@ -1,21 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Playback started", "Disconnected")]
public class FlowPlayAudio : PlayAudioBase
{
/// <inheritdoc />
public FlowPlayAudio([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
protected override ValueTask HandlePlaybackStartedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Playback started");
/// <inheritdoc />
protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected");
}

View file

@ -1,21 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Done", "Finished speaking", "Disconnected")]
public class FlowSpeakText : SpeakTextBase
{
/// <inheritdoc />
public FlowSpeakText([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
protected override async ValueTask HandleDisconnected(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Disconnected", "Done");
/// <inheritdoc />
protected override async ValueTask HandleSpeakingHasFinished(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Finished speaking", "Done");
}

View file

@ -1,21 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Recording finished", "Disconnected")]
public class FlowStartRecording : StartRecordingBase
{
/// <inheritdoc />
public FlowStartRecording([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected");
/// <inheritdoc />
protected override ValueTask HandleCallRecordingSavedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Recording finished");
}

View file

@ -1,21 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Done", "Disconnected")]
public class FlowStopAudioPlayback : StopAudioPlaybackBase
{
/// <inheritdoc />
public FlowStopAudioPlayback([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
protected override ValueTask HandleDoneAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Done");
/// <inheritdoc />
protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected");
}

View file

@ -1,158 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.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.Telnyx.Activities;
/// <summary>
/// Play an audio file on the call until the required DTMF signals are gathered to build interactive menus.
/// </summary>
[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<CallGatherEndedPayload>, IBookmarksPersistedHandler
{
/// <inheritdoc />
public GatherUsingAudio([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// 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.
/// </summary>
[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<string> CallControlId { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
[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<Uri?>? AudioUrl { get; set; } = default!;
/// <summary>
/// The number of milliseconds to wait for input between digits.
/// </summary>
[Input(
DisplayName = "Inter Digit Timeout",
Description = "The number of milliseconds to wait for input between digits.",
Category = "Advanced",
DefaultValue = 5000
)]
public Input<int?>? InterDigitTimeoutMillis { get; set; } = new(5000);
/// <summary>
/// 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.
/// </summary>
[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<Uri?>? InvalidAudioUrl { get; set; }
/// <summary>
/// A list of all digits accepted as valid.
/// </summary>
[Input(
Description = "A list of all digits accepted as valid.",
Category = "Advanced",
DefaultValue = "0123456789#*"
)]
public Input<string?>? ValidDigits { get; set; } = new("0123456789#*");
/// <summary>
/// The minimum number of digits to fetch. This parameter has a minimum value of 1.
/// </summary>
[Input(Description = "The minimum number of digits to fetch. This parameter has a minimum value of 1.", DefaultValue = 1)]
public Input<int?>? MinimumDigits { get; set; } = new(1);
/// <summary>
/// The maximum number of digits to fetch. This parameter has a maximum value of 128.
/// </summary>
[Input(Description = "The maximum number of digits to fetch. This parameter has a maximum value of 128.", DefaultValue = 128)]
public Input<int?>? MaximumDigits { get; set; } = new(128);
/// <summary>
/// The maximum number of times the file should be played if there is no input from the user on the call.
/// </summary>
[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<int?>? MaximumTries { get; set; } = new(3);
/// <summary>
/// The digit used to terminate input if fewer than `maximum_digits` digits have been gathered.
/// </summary>
[Input(Description = "The digit used to terminate input if fewer than `maximum_digits` digits have been gathered.", DefaultValue = "#")]
public Input<string?>? TerminatingDigit { get; set; } = new("#");
/// <summary>
/// The number of milliseconds to wait for a DTMF response after file playback ends before a replaying the sound file.
/// </summary>
[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<int?>? TimeoutMillis { get; set; } = new(60000);
/// <summary>
/// Calls out to Telnyx to actually begin gathering input.
/// </summary>
public async ValueTask BookmarksPersistedAsync(ActivityExecutionContext context)
{
var request = new GatherUsingAudioRequest(
AudioUrl.Get(context) ?? throw new Exception("AudioUrl is required."),
context.CreateCorrelatingClientState(),
default,
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<ITelnyxClient>();
try
{
await telnyxClient.Calls.GatherUsingAudioAsync(callControlId, request, context.CancellationToken);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await context.CompleteActivityWithOutcomesAsync("Disconnected");
}
}
/// <inheritdoc />
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<CallGatherEndedPayload>();
var outcome = payload.Status == "valid" ? "Valid input" : "Invalid input";
context.Set(Result, payload);
await context.CompleteActivityWithOutcomesAsync(outcome);
}
}

View file

@ -1,190 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Convert text to speech and play it on the call until the required DTMF signals are gathered to build interactive menus.
/// </summary>
[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<CallGatherEndedPayload>
{
/// <inheritdoc />
public GatherUsingSpeak([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// 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.
/// </summary>
[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<string> CallControlId { get; set; } = default!;
/// <summary>
/// The language you want spoken.
/// </summary>
[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<string> Language { get; set; } = new("en-US");
/// <summary>
/// The gender of the voice used to speak back the text.
/// </summary>
[Input(
Description = "The gender of the voice used to speak back the text.",
UIHint = InputUIHints.DropDown,
Options = new[] { "female", "male" },
DefaultValue = "female"
)]
public Input<string> Voice { get; set; } = new("female");
/// <summary>
/// The text or SSML to be converted into speech. There is a 5,000 character limit.
/// </summary>
[Input(
Description = "The text or SSML to be converted into speech. There is a 5,000 character limit.",
UIHint = InputUIHints.MultiLine
)]
public Input<string> Payload { get; set; } = default!;
/// <summary>
/// The type of the provided payload. The payload can either be plain text, or Speech Synthesis Markup Language (SSML).
/// </summary>
[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<string?>? PayloadType { get; set; }
/// <summary>
/// "This parameter impacts speech quality, language options and payload types. When using `basic`, only the `en-US` language and payload type `text` are allowed."
/// </summary>
[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<string?>? ServiceLevel { get; set; }
/// <summary>
/// The number of milliseconds to wait for input between digits.
/// </summary>
[Input(
DisplayName = "Inter Digit Timeout",
Description = "The number of milliseconds to wait for input between digits.",
Category = "Advanced",
DefaultValue = 5000
)]
public Input<int?>? InterDigitTimeoutMillis { get; set; } = new(5000);
/// <summary>
/// A list of all digits accepted as valid.
/// </summary>
[Input(
Description = "A list of all digits accepted as valid.",
Category = "Advanced",
DefaultValue = "0123456789#*"
)]
public Input<string?>? ValidDigits { get; set; } = new("0123456789#*");
/// <summary>
/// The minimum number of digits to fetch. This parameter has a minimum value of 1.
/// </summary>
[Input(Description = "The minimum number of digits to fetch. This parameter has a minimum value of 1.", DefaultValue = 1)]
public Input<int?>? MinimumDigits { get; set; } = new(1);
/// <summary>
/// The maximum number of digits to fetch. This parameter has a maximum value of 128.
/// </summary>
[Input(Description = "The maximum number of digits to fetch. This parameter has a maximum value of 128.", DefaultValue = 128)]
public Input<int?>? MaximumDigits { get; set; } = new(128);
/// <summary>
/// The maximum number of times the file should be played if there is no input from the user on the call.
/// </summary>
[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<int?>? MaximumTries { get; set; } = new(3);
/// <summary>
/// The digit used to terminate input if fewer than `maximum_digits` digits have been gathered.
/// </summary>
[Input(Description = "The digit used to terminate input if fewer than `maximum_digits` digits have been gathered.", DefaultValue = "#")]
public Input<string?>? TerminatingDigit { get; set; } = new("#");
/// <summary>
/// The number of milliseconds to wait for a DTMF response after file playback ends before a replaying the sound file.
/// </summary>
[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<int?>? TimeoutMillis { get; set; } = new(60000);
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var callControlId = CallControlId.Get(context);
var request = new GatherUsingSpeakRequest(
Language.Get(context) ?? throw new Exception("Language is required."),
Voice.Get(context) ?? throw new Exception("Voice is required."),
Payload.Get(context) ?? throw new Exception("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<ITelnyxClient>();
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<CallGatherEndedPayload>();
var outcome = payload.Status == "valid" ? "Valid input" : "Invalid input";
context.Set(Result, payload);
await context.CompleteActivityWithOutcomesAsync(outcome);
}
}

View file

@ -1,44 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Services;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Alive", "Dead", "Done")]
[Activity(Constants.Namespace, "Get the status of a call.", Kind = ActivityKind.Task)]
public class GetCallStatus : Activity<bool>
{
/// <inheritdoc />
public GetCallStatus([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// Unique identifier and token for controlling the call.
/// </summary>
[Input(
DisplayName = "Call Control ID",
Description = "Unique identifier and token for controlling the call.",
Category = "Advanced"
)]
public Input<string> CallControlId { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var client = context.GetRequiredService<ITelnyxClient>();
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");
}
}

View file

@ -1,32 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[PublicAPI]
public class HangupCall : HangupCallBase
{
/// <inheritdoc />
public HangupCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The <see cref="IActivity"/> to execute when the call was no longer active.
/// </summary>
[Port] public IActivity? Disconnected { get; set; }
/// <inheritdoc />
protected override async ValueTask HandleDoneAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync(OnCompletedAsync);
/// <inheritdoc />
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync);
/// <summary>
/// Executed when any child activity completed.
/// </summary>
private async ValueTask OnCompletedAsync(ActivityCompletedContext context) => await context.TargetContext.CompleteActivityAsync();
}

View file

@ -1,57 +0,0 @@
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Hang up the call.
/// </summary>
[Activity(Constants.Namespace, "Hang up the call.", Kind = ActivityKind.Task)]
public abstract class HangupCallBase : Activity
{
/// <inheritdoc />
protected HangupCallBase(string? source = default, int? line = default) : base(source, line)
{
}
/// <summary>
/// Unique identifier and token for controlling the call.
/// </summary>
[Input(DisplayName = "Call Control ID", Description = "Unique identifier and token for controlling the call.", Category = "Advanced")]
public Input<string> CallControlId { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var callControlId = CallControlId.Get(context);
var request = new HangupCallRequest(ClientState: context.CreateCorrelatingClientState());
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.HangupCallAsync(callControlId, request, context.CancellationToken);
await HandleDoneAsync(context);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await HandleDisconnectedAsync(context);
}
}
/// <summary>
/// Executed when the call was hangup.
/// </summary>
protected abstract ValueTask HandleDoneAsync(ActivityExecutionContext context);
/// <summary>
/// Executed when the call was no longer active.
/// </summary>
protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context);
}

View file

@ -1,88 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Helpers;
using Elsa.Telnyx.Models;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Triggered when an inbound phone call is received for any of the specified source or destination phone numbers.
/// </summary>
[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<CallInitiatedPayload>
{
/// <inheritdoc />
public IncomingCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// A list of destination numbers to respond to.
/// </summary>
[Input(Description = "A list of destination numbers to respond to.", UIHint = InputUIHints.MultiText)]
public Input<ICollection<string>> To { get; set; } = default!;
/// <summary>
/// A list of source numbers to respond to.
/// </summary>
[Input(Description = "A list of source numbers to respond to.", UIHint = InputUIHints.MultiText)]
public Input<ICollection<string>> From { get; set; } = default!;
/// <summary>
/// Match any inbound calls.
/// </summary>
[Input(Description = "Match any inbound calls.")]
public Input<bool> CatchAll { get; set; } = default!;
/// <inheritdoc />
protected override IEnumerable<object> GetTriggerPayloads(TriggerIndexingContext context) => GetBookmarkPayloads(context.ExpressionExecutionContext);
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
if (context.IsTriggerOfWorkflow())
{
await ResumeAsync(context);
}
else
{
var bookmarkPayloads = GetBookmarkPayloads(context.ExpressionExecutionContext);
foreach (var bookmarkPayload in bookmarkPayloads) context.CreateBookmark(bookmarkPayload, ResumeAsync);
}
}
private async ValueTask ResumeAsync(ActivityExecutionContext context)
{
var webhookModel = context.GetWorkflowInput<TelnyxWebhook>(WebhookSerializerOptions.Create());
var callInitiatedPayload = (CallInitiatedPayload)webhookModel.Data.Payload;
// Store webhook payload as output.
Result.Set(context, callInitiatedPayload);
await context.CompleteActivityAsync();
}
private IEnumerable<object> GetBookmarkPayloads(ExpressionExecutionContext context)
{
var from = context.Get(From) ?? ArraySegment<string>.Empty;
var to = context.Get(To) ?? ArraySegment<string>.Empty;
var catchAll = context.Get(CatchAll);
foreach (var phoneNumber in from) yield return new IncomingCallFromStimulus(phoneNumber);
foreach (var phoneNumber in to) yield return new IncomingCallToStimulus(phoneNumber);
if (catchAll)
yield return new IncomingCallCatchAllStimulus();
}
}

View file

@ -1,48 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Returns information about the provided phone number.
/// </summary>
[Activity(Constants.Namespace, "Returns information about the provided phone number.", Kind = ActivityKind.Task)]
public class LookupNumber : CodeActivity<NumberLookupResponse>
{
/// <inheritdoc />
public LookupNumber([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The phone number to be looked up.
/// </summary>
[Input(Description = "The phone number to be looked up.")]
public Input<string> PhoneNumber { get; set; } = default!;
/// <summary>
/// The types of number lookups to be performed.
/// </summary>
[Input(
Description = "The types of number lookups to be performed.",
UIHint = InputUIHints.CheckList,
Options = new[] { "carrier", "caller-name" }
)]
public Input<ICollection<string>> Types { get; set; } = new(new List<string>());
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
var phoneNumber = PhoneNumber.Get(context) ?? throw new Exception("PhoneNumber is required.");
var types = Types.Get(context);
var response = await telnyxClient.NumberLookup.NumberLookupAsync(phoneNumber, types, context.CancellationToken);
context.Set(Result, response.Data);
}
}

View file

@ -1,30 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[PublicAPI]
public class PlayAudio : PlayAudioBase
{
/// <inheritdoc />
public PlayAudio([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The <see cref="IActivity"/> to execute when the call was no longer active.
/// </summary>
[Port]
public IActivity? Disconnected { get; set; }
/// <inheritdoc />
protected override async ValueTask HandlePlaybackStartedAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync();
/// <inheritdoc />
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync);
private async ValueTask OnCompletedAsync(ActivityCompletedContext context) => await context.TargetContext.CompleteActivityAsync();
}

View file

@ -1,121 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Play an audio file on the call.
/// </summary>
[Activity(Constants.Namespace, "Play an audio file on the call.", Kind = ActivityKind.Task)]
[FlowNode("Playback started", "Disconnected")]
[WebhookDriven(WebhookEventTypes.CallPlaybackStarted)]
public abstract class PlayAudioBase : Activity
{
/// <inheritdoc />
protected PlayAudioBase([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// Unique identifier and token for controlling the call.
/// </summary>
[Input(
DisplayName = "Call Control ID",
Description = "Unique identifier and token for controlling the call.",
Category = "Advanced"
)]
public Input<string> CallControlId { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
[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<Uri> AudioUrl { get; set; } = default!;
/// <summary>
/// The number of times the audio file should be played. If supplied, the value must be an integer between 1 and 100, or the special string 'infinity' for an endless loop.
/// </summary>
[Input(
Description = "The number of times the audio file should be played. If supplied, the value must be an integer between 1 and 100, or the special string 'infinity' for an endless loop.",
DefaultValue = "1",
Category = "Advanced"
)]
public Input<string?> Loop { get; set; } = new("1");
/// <summary>
/// When enabled, audio will be mixed on top of any other audio that is actively being played back. Note that `overlay: true` will only work if there is another audio file already being played on the call.
/// </summary>
[Input(
Description = "When enabled, audio will be mixed on top of any other audio that is actively being played back. Note that `overlay: true` will only work if there is another audio file already being played on the call.",
DefaultValue = false,
Category = "Advanced"
)]
public Input<bool> Overlay { get; set; } = new(false);
/// <summary>
/// Specifies the leg or legs on which audio will be played. If supplied, the value must be either 'self', 'opposite' or 'both'.
/// </summary>
[Input(
Description = "Specifies the leg or legs on which audio will be played. If supplied, the value must be either 'self', 'opposite' or 'both'.",
UIHint = InputUIHints.DropDown,
Options = new[] { "", "self", "opposite", "both" },
Category = "Advanced"
)]
public Input<string?> TargetLegs { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var loop = Loop.GetOrDefault(context);
var request = new PlayAudioRequest(
AudioUrl.Get(context),
Overlay.GetOrDefault(context),
string.IsNullOrWhiteSpace(loop) ? null : loop == "infinity" ? "infinity" : int.Parse(loop),
TargetLegs.GetOrDefault(context).EmptyToNull(),
ClientState: context.CreateCorrelatingClientState(context.Id)
);
var callControlId = CallControlId.Get(context);
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.PlayAudioAsync(callControlId, request, context.CancellationToken);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await HandleDisconnectedAsync(context);
}
context.CreateBookmark(new WebhookEventStimulus(WebhookEventTypes.CallPlaybackStarted, callControlId), ResumeAsync, true);
}
/// <summary>
/// Called when playback has started.
/// </summary>
protected abstract ValueTask HandlePlaybackStartedAsync(ActivityExecutionContext context);
/// <summary>
/// Called when the call was no longer active.
/// </summary>
protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context);
private async ValueTask ResumeAsync(ActivityExecutionContext context) => await HandlePlaybackStartedAsync(context);
}

View file

@ -1,34 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[PublicAPI]
public class SpeakText : SpeakTextBase
{
/// <inheritdoc />
public SpeakText([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The <see cref="IActivity"/> to execute when the call was no longer active.
/// </summary>
[Port]
public IActivity? Disconnected { get; set; }
/// <summary>
/// The <see cref="IActivity"/> to execute when speaking has finished.
/// </summary>
[Port]
public IActivity? FinishedSpeaking { get; set; }
/// <inheritdoc />
protected override async ValueTask HandleDisconnected(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected);
/// <inheritdoc />
protected override async ValueTask HandleSpeakingHasFinished(ActivityExecutionContext context) => await context.ScheduleActivityAsync(FinishedSpeaking);
}

View file

@ -1,128 +0,0 @@
using Elsa.Extensions;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Convert text to speech and play it back on the call.
/// </summary>
[Activity(Constants.Namespace, "Convert text to speech and play it back on the call.", Kind = ActivityKind.Task)]
[WebhookDriven(WebhookEventTypes.CallSpeakEnded)]
public abstract class SpeakTextBase : Activity
{
/// <inheritdoc />
protected SpeakTextBase(string? source = default, int? line = default) : base(source, line)
{
}
/// <summary>
/// Unique identifier and token for controlling the call.
/// </summary>
[Input(
DisplayName = "Call Control ID",
Description = "Unique identifier and token for controlling the call.",
Category = "Advanced"
)]
public Input<string> CallControlId { get; set; } = default!;
/// <summary>
/// The language you want spoken.
/// </summary>
[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<string> Language { get; set; } = new("en-US");
/// <summary>
/// The gender of the voice used to speak back the text.
/// </summary>
[Input(
Description = "The gender of the voice used to speak back the text.",
UIHint = InputUIHints.DropDown,
Options = new[] { "female", "male" },
DefaultValue = "female"
)]
public Input<string> Voice { get; set; } = new("female");
/// <summary>
/// The text or SSML to be converted into speech. There is a 5,000 character limit.
/// </summary>
[Input(
Description = "The text or SSML to be converted into speech. There is a 5,000 character limit.",
UIHint = InputUIHints.MultiLine
)]
public Input<string> Payload { get; set; } = default!;
/// <summary>
/// The type of the provided payload. The payload can either be plain text, or Speech Synthesis Markup Language (SSML).
/// </summary>
[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<string?>? PayloadType { get; set; }
/// <summary>
/// This parameter impacts speech quality, language options and payload types. When using `basic`, only the `en-US` language and payload type `text` are allowed.
/// </summary>
[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<string?> ServiceLevel { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var request = new SpeakTextRequest(
Language.GetOrDefault(context) ?? "en-US",
Voice.GetOrDefault(context) ?? "female",
Payload.Get(context),
PayloadType.GetOrDefault(context).EmptyToNull(),
ServiceLevel.GetOrDefault(context).EmptyToNull(),
ClientState: context.CreateCorrelatingClientState(context.Id)
);
var callControlId = CallControlId.Get(context);
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
// Send the request to Telnyx.
await telnyxClient.Calls.SpeakTextAsync(callControlId, request, context.CancellationToken);
// Create bookmark to resume the workflow when speaking has finished.
context.CreateBookmark(new WebhookEventStimulus(WebhookEventTypes.CallSpeakEnded, callControlId), HandleSpeakingHasFinished, true);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await HandleDisconnected(context);
}
}
/// <summary>
/// Called when the call was no longer active.
/// </summary>
protected abstract ValueTask HandleDisconnected(ActivityExecutionContext context);
/// <summary>
/// Called when speaking has finished.
/// </summary>
protected abstract ValueTask HandleSpeakingHasFinished(ActivityExecutionContext context);
}

View file

@ -1,34 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[PublicAPI]
public class StartRecording : StartRecordingBase
{
/// <inheritdoc />
public StartRecording([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The <see cref="IActivity"/> to execute when recording has finished.
/// </summary>
[Port] public IActivity? RecordingFinished { get; set; }
/// <summary>
/// The <see cref="IActivity"/> to executed when the call was no longer active.
/// </summary>
[Port] public IActivity? Disconnected { get; set; }
/// <inheritdoc />
protected override async ValueTask HandleCallRecordingSavedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(RecordingFinished, OnCompletedAsync);
/// <inheritdoc />
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync);
private async ValueTask OnCompletedAsync(ActivityCompletedContext context) => await context.TargetContext.CompleteActivityAsync();
}

View file

@ -1,109 +0,0 @@
using Elsa.Extensions;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Start recording the call.
/// </summary>
[Activity(Constants.Namespace, "Start recording the call.", Kind = ActivityKind.Task)]
[WebhookDriven(WebhookEventTypes.CallRecordingSaved)]
public abstract class StartRecordingBase : Activity<CallRecordingSavedPayload>
{
/// <inheritdoc />
protected StartRecordingBase(string? source = default, int? line = default) : base(source, line)
{
}
/// <summary>
/// Unique identifier and token for controlling the call.
/// </summary>
[Input(
DisplayName = "Call Control ID",
Description = "Unique identifier and token for controlling the call.",
Category = "Advanced"
)]
public Input<string> CallControlId { get; set; } = default!;
/// <summary>
/// When 'dual', final audio file will be stereo recorded with the first leg on channel A, and the rest on channel B.
/// </summary>
[Input(
Description = "When 'dual', final audio file will be stereo recorded with the first leg on channel A, and the rest on channel B.",
UIHint = InputUIHints.DropDown,
Options = new[] { "single", "dual" },
DefaultValue = "single"
)]
public Input<string> Channels { get; set; } = new("single");
/// <summary>
/// The audio file format used when storing the call recording. Can be either 'mp3' or 'wav'.
/// </summary>
[Input(
Description = "The audio file format used when storing the call recording. Can be either 'mp3' or 'wav'.",
UIHint = InputUIHints.DropDown,
Options = new[] { "wav", "mp3" },
DefaultValue = "wav"
)]
public Input<string> Format { get; set; } = new("wav");
/// <summary>
/// If enabled, a beep sound will be played at the start of a recording.
/// </summary>
[Input(Description = "If enabled, a beep sound will be played at the start of a recording.")]
public Input<bool?>? PlayBeep { get; set; }
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var request = new StartRecordingRequest(
Channels.GetOrDefault(context) ?? "single",
Format.GetOrDefault(context) ?? "wav",
PlayBeep.GetOrDefault(context),
ClientState: context.CreateCorrelatingClientState()
);
var callControlId = CallControlId.Get(context);
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.StartRecordingAsync(callControlId, request, context.CancellationToken);
context.CreateBookmark(new WebhookEventStimulus(WebhookEventTypes.CallRecordingSaved, callControlId), ResumeAsync, false);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await HandleDisconnectedAsync(context);
}
}
/// <summary>
/// Called when the recording was saved.
/// </summary>
protected abstract ValueTask HandleCallRecordingSavedAsync(ActivityExecutionContext context);
/// <summary>
/// Called when the call was no longer active.
/// </summary>
protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context);
private async ValueTask ResumeAsync(ActivityExecutionContext context)
{
var payload = context.GetWorkflowInput<CallRecordingSavedPayload>();
context.Set(Result, payload);
await HandleCallRecordingSavedAsync(context);
}
}

View file

@ -1,27 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
public class StopAudioPlayback : StopAudioPlaybackBase
{
/// <inheritdoc />
public StopAudioPlayback([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The <see cref="IActivity"/> to execute when the call was no longer active.
/// </summary>
[Port] public IActivity? Disconnected { get; set; }
/// <inheritdoc />
protected override async ValueTask HandleDoneAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync();
/// <inheritdoc />
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync);
private async ValueTask OnCompletedAsync(ActivityCompletedContext context) => await context.TargetContext.CompleteActivityAsync();
}

View file

@ -1,70 +0,0 @@
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Stop audio playback.
/// </summary>
[Activity(Constants.Namespace, Description = "Stop audio playback.", Kind = ActivityKind.Task)]
public abstract class StopAudioPlaybackBase : Activity
{
/// <inheritdoc />
protected StopAudioPlaybackBase(string? source = default, int? line = default) : base(source, line)
{
}
/// <summary>
/// Unique identifier and token for controlling the call.
/// </summary>
[Input(
DisplayName = "Call Control ID",
Description = "Unique identifier and token for controlling the call.",
Category = "Advanced"
)]
public Input<string> CallControlId { get; set; } = default!;
/// <summary>
/// Use 'current' to stop only the current audio or 'all' to stop all audios in the queue.
/// </summary>
[Input(
Description = "Use 'current' to stop only the current audio or 'all' to stop all audios in the queue.",
DefaultValue = "all",
Category = "Advanced"
)]
public Input<string?> Stop { get; set; } = new("all");
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var request = new StopAudioPlaybackRequest(Stop.Get(context), context.CreateCorrelatingClientState(context.Id));
var callControlId = CallControlId.Get(context);
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.StopAudioPlaybackAsync(callControlId, request, context.CancellationToken);
await HandleDoneAsync(context);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await HandleDisconnectedAsync(context);
}
}
/// <summary>
/// Called when audio playback is stopping.
/// </summary>
protected abstract ValueTask HandleDoneAsync(ActivityExecutionContext context);
/// <summary>
/// Called when the call was no longer active.
/// </summary>
protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context);
}

View file

@ -1,54 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Stop recording the call.
/// </summary>
[Activity(Constants.Namespace, "Stop recording the call.", Kind = ActivityKind.Task)]
[FlowNode("Recording stopped", "Disconnected")]
public class StopRecording : Activity
{
/// <inheritdoc />
public StopRecording([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// Unique identifier and token for controlling the call.
/// </summary>
[Input(
DisplayName = "Call Control ID",
Description = "Unique identifier and token for controlling the call.",
Category = "Advanced"
)]
public Input<string> CallControlId { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var request = new StopRecordingRequest(context.CreateCorrelatingClientState());
var callControlId = CallControlId.Get(context);
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.StopRecordingAsync(callControlId, request, context.CancellationToken);
await context.CompleteActivityWithOutcomesAsync("Recording stopped");
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await context.CompleteActivityWithOutcomesAsync("Disconnected");
}
}
}

View file

@ -1,162 +0,0 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Payloads.Abstractions;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Transfer a call to a new destination.
/// </summary>
[Activity(Constants.Namespace, "Transfer a call to a new destination.", Kind = ActivityKind.Task)]
[FlowNode("Transferred", "Hangup", "Disconnected")]
[WebhookDriven(WebhookEventTypes.CallInitiated, WebhookEventTypes.CallAnswered, WebhookEventTypes.CallHangup)]
public class TransferCall : Activity<CallPayload>
{
/// <inheritdoc />
public TransferCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// Unique identifier and token for controlling the call.
/// </summary>
[Input(
Name = "Call Control ID",
Description = "Unique identifier and token for controlling the call.",
Category = "Advanced"
)]
public Input<string> CallControlId { get; set; } = default!;
/// <summary>
/// The DID or SIP URI to dial out and bridge to the given call.
/// </summary>
[Input(Description = "The DID or SIP URI to dial out and bridge to the given call.")]
public Input<string> To { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
[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<string?> From { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
[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<string?> FromDisplayName { get; set; } = default!;
/// <summary>
/// Enables Answering Machine Detection.
/// </summary>
[Input(
DisplayName = "Answering Machine Detection",
Description = "Enables Answering Machine Detection.",
UIHint = InputUIHints.DropDown,
Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end" }
)]
public Input<string?> AnsweringMachineDetection { get; set; } = default!;
/// <summary>
/// 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.
/// </summary>
[Input(
DisplayName = "Audio URL",
Description = "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."
)]
public Input<Uri?> AudioUrl { get; set; } = default!;
/// <summary>
/// Sets the maximum duration of a Call Control Leg in seconds.
/// </summary>
[Input(DisplayName = "Time limit", Description = "Sets the maximum duration of a Call Control Leg in seconds.", Category = "Advanced")]
public Input<int?> TimeLimitSecs { get; set; } = default!;
/// <summary>
/// The number of seconds that Telnyx will wait for the call to be answered by the destination to which it is being transferred.
/// </summary>
[Input(
DisplayName = "Timeout",
Description = "The number of seconds that Telnyx will wait for the call to be answered by the destination to which it is being transferred.",
Category = "Advanced"
)]
public Input<int?> TimeoutSecs { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
await TransferCallAsync(context);
var initiatedBookmark = new WebhookEventStimulus(WebhookEventTypes.CallInitiated);
context.CreateBookmark(initiatedBookmark, InitiatedAsync);
}
private ValueTask InitiatedAsync(ActivityExecutionContext context)
{
var payload = context.GetWorkflowInput<CallInitiatedPayload>();
var callControlId = payload.CallControlId;
var answeredBookmark = new WebhookEventStimulus(WebhookEventTypes.CallAnswered, callControlId);
var hangupBookmark = new WebhookEventStimulus(WebhookEventTypes.CallHangup, callControlId);
context.CreateBookmark(answeredBookmark, AnsweredAsync, false);
context.CreateBookmark(hangupBookmark, HangupAsync, false);
return default;
}
private async ValueTask AnsweredAsync(ActivityExecutionContext context)
{
var payload = context.GetWorkflowInput<CallAnsweredPayload>();
Result.Set(context, payload);
await context.CompleteActivityWithOutcomesAsync("Transferred");
}
private async ValueTask HangupAsync(ActivityExecutionContext context)
{
var payload = context.GetWorkflowInput<CallHangupPayload>();
Result.Set(context, payload);
await context.CompleteActivityWithOutcomesAsync("Hangup");
}
private async ValueTask TransferCallAsync(ActivityExecutionContext context)
{
var callControlId = CallControlId.Get(context);
var request = new TransferCallRequest(
To.Get(context) ?? throw new Exception("To is required."),
From.GetOrDefault(context),
FromDisplayName.GetOrDefault(context).SanitizeCallerName(),
AudioUrl.GetOrDefault(context),
AnsweringMachineDetection.GetOrDefault(context),
default,
TimeLimitSecs.GetOrDefault(context),
TimeoutSecs.GetOrDefault(context),
ClientState: context.CreateCorrelatingClientState(context.Id),
TargetLegClientState: context.CreateCorrelatingClientState(context.Id)
);
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.TransferCallAsync(callControlId, request, context.CancellationToken);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync(context.CancellationToken)) throw;
await context.CompleteActivityWithOutcomesAsync("Disconnected");
}
}
}

View file

@ -1,66 +0,0 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Helpers;
using Elsa.Telnyx.Models;
using Elsa.Telnyx.Payloads.Abstractions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Represents a Telnyx webhook event trigger.
/// </summary>
[Activity("Telnyx", "Telnyx", "A Telnyx webhook event that executes when a webhook event is received.", Kind = ActivityKind.Trigger)]
[Browsable(false)]
public class WebhookEvent : Activity<Payload>
{
/// <inheritdoc />
public WebhookEvent([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
public WebhookEvent(string eventType, string activityTypeName, Variable<Payload> result, int version = 1, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default)
: base(activityTypeName, version, source, line)
{
EventType = eventType;
Result = new(result);
}
/// <summary>
/// The Telnyx webhook event type to listen for.
/// </summary>
[Description("The Telnyx webhook event type to listen for")]
public string EventType { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
if (context.IsTriggerOfWorkflow())
await Resume(context);
else
{
var eventType = EventType;
var payload = new WebhookEventStimulus(eventType);
context.CreateBookmark(new CreateBookmarkArgs
{
Stimulus = payload,
Callback = Resume,
BookmarkName = Type
});
}
}
private async ValueTask Resume(ActivityExecutionContext context)
{
var input = context.GetWorkflowInput<TelnyxWebhook>(WebhookSerializerOptions.Create());
context.Set(Result, input.Data.Payload);
await CompleteAsync(context);
}
}

View file

@ -1,33 +0,0 @@
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Attributes;
/// <summary>
/// Contains metadata about the activity descriptor to yield from the annotated payload.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class WebhookActivityAttribute : WebhookAttribute
{
/// <inheritdoc />
public WebhookActivityAttribute(string eventType, string activityType, string displayName, string description) : base(eventType)
{
ActivityType = activityType;
DisplayName = displayName;
Description = description;
}
/// <summary>
/// The activity type name to yield for the annotated <see cref="Payload"/> type.
/// </summary>
public string ActivityType { get; }
/// <summary>
/// The activity display name to yield for the annotated <see cref="Payload"/> type.
/// </summary>
public string DisplayName { get; }
/// <summary>
/// The activity description to yield for the annotated <see cref="Payload"/> type.
/// </summary>
public string Description { get; }
}

View file

@ -1,21 +0,0 @@
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Attributes;
/// <summary>
/// Used to handle a Telnyx webhook event.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class WebhookAttribute : Attribute
{
/// <inheritdoc />
public WebhookAttribute(string eventType)
{
EventType = eventType;
}
/// <summary>
/// The Telnyx event to match to th annotated <see cref="Payload"/> type.
/// </summary>
public string EventType { get; }
}

View file

@ -1,19 +0,0 @@
namespace Elsa.Telnyx.Attributes;
/// <summary>
/// Contains metadata about the activity descriptor to yield from the annotated payload.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class WebhookDrivenAttribute : Attribute
{
/// <inheritdoc />
public WebhookDrivenAttribute(params string[] eventTypes)
{
EventTypes = new HashSet<string>(eventTypes);
}
/// <summary>
/// The Telnyx event to match.
/// </summary>
public ISet<string> EventTypes { get; }
}

View file

@ -1,7 +0,0 @@
namespace Elsa.Telnyx.Bookmarks;
/// <summary>
/// A bookmark payload for the call.answered Telnyx webhook event.
/// </summary>
/// <param name="CallControlId">The call control ID.</param>
public record AnswerCallStimulus(string CallControlId);

View file

@ -1,7 +0,0 @@
namespace Elsa.Telnyx.Bookmarks;
/// <summary>
/// A bookmark payload for the call.answered Telnyx webhook event.
/// </summary>
/// <param name="CallControlId"></param>
public record CallAnsweredStimulus(string CallControlId);

View file

@ -1,7 +0,0 @@
namespace Elsa.Telnyx.Bookmarks;
/// <summary>
/// A bookmark payload for the call.hangup Telnyx webhook event.
/// </summary>
/// <param name="CallControlId"></param>
public record CallHangupStimulus(string CallControlId);

View file

@ -1,3 +0,0 @@
namespace Elsa.Telnyx.Bookmarks;
public record IncomingCallCatchAllStimulus;

View file

@ -1,3 +0,0 @@
namespace Elsa.Telnyx.Bookmarks;
public record IncomingCallFromStimulus(string PhoneNumber);

View file

@ -1,3 +0,0 @@
namespace Elsa.Telnyx.Bookmarks;
public record IncomingCallToStimulus(string PhoneNumber);

View file

@ -1,8 +0,0 @@
namespace Elsa.Telnyx.Bookmarks;
/// <summary>
/// A bookmark payload for Telnyx webhook events.
/// </summary>
/// <param name="EventType">The event type.</param>
/// <param name="CallControlId">An optional call control ID.</param>
public record WebhookEventStimulus(string EventType, string? CallControlId = default);

View file

@ -1,28 +0,0 @@
using Elsa.Telnyx.Client.Services;
namespace Elsa.Telnyx.Client.Implementations;
/// <summary>
/// Represents a Telnyx API client.
/// </summary>
public class TelnyxClient : ITelnyxClient
{
/// <summary>
/// Constructor.
/// </summary>
public TelnyxClient(ICallsApi calls, INumberLookupApi numberLookup)
{
Calls = calls;
NumberLookup = numberLookup;
}
/// <summary>
/// Provides access to the Calls API.
/// </summary>
public ICallsApi Calls { get; }
/// <summary>
/// Provides access to the Number Lookup API.
/// </summary>
public INumberLookupApi NumberLookup { get; }
}

View file

@ -1,13 +0,0 @@
namespace Elsa.Telnyx.Client.Models;
public record AnsweringMachineConfig(
int AfterGreetingSilenceMillis = 800,
int BetweenWordsSilenceMillis = 50,
int GreetingDurationMillis = 3500,
int GreetingTotalAnalysisTimeMillis = 5000,
int InitialSilenceMillis = 3500,
int MaximumNumberOfWords = 5,
int MaximumWordLengthMillis = 3500,
int SilenceThreshold = 256,
int TotalAnalysisTimeMillis = 3500
);

View file

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

View file

@ -1,3 +0,0 @@
namespace Elsa.Telnyx.Client.Models;
public record Header(string Name, string Value);

View file

@ -1,3 +0,0 @@
namespace Elsa.Telnyx.Client.Models;
public record PlayAudioResponse(string Result);

View file

@ -1,126 +0,0 @@
namespace Elsa.Telnyx.Client.Models;
public record AnswerCallRequest(
string? BillingGroupId = default,
string? ClientState = default,
string? CommandId = default,
string? WebhookUrl = default,
string? WebhookUrlMethod = default);
public record HangupCallRequest(string? ClientState = default, string? CommandId = default);
public record GatherUsingAudioRequest(
Uri AudioUrl,
string? ClientState = default,
string? CommandId = default,
int? InterDigitTimeoutMillis = default,
Uri? InvalidAudioUrl = default,
int? MaximumDigits = default,
int? MaximumTries = default,
int? MinimumDigits = default,
string? TerminatingDigit = default,
int? TimeoutMillis = default,
string? ValidDigits = default
);
public record GatherUsingSpeakRequest(
string Language,
string Voice,
string Payload,
string? PayloadType = default,
string? ServiceLevel = default,
int? InterDigitTimeoutMillis = default,
int? MaximumDigits = default,
int? MaximumTries = default,
int? MinimumDigits = default,
string? TerminatingDigit = default,
int? TimeoutMillis = default,
string? ValidDigits = default,
string? ClientState = default,
string? CommandId = default
);
public record TransferCallRequest(
string To,
string? From = default,
string? FromDisplayName = default,
Uri? AudioUrl = default,
string? AnsweringMachineDetection = default,
AnsweringMachineConfig? AnsweringMachineConfig = default,
int? TimeLimitSecs = default,
int? TimeoutSecs = default,
string? TargetLegClientState = default,
IList<Header>? CustomHeaders = default,
string? SipAuthUsername = default,
string? SipAuthPassword = default,
string? ClientState = default,
string? CommandId = default,
string? WebhookUrl = default,
string? WebhookUrlMethod = default
);
public record DialRequest(
string ConnectionId,
string To,
string? From = default,
string? FromDisplayName = default,
string? AnsweringMachineDetection = default,
AnsweringMachineConfig? AnsweringMachineConfig = default,
string? Record = default,
string? RecordFormat = default,
string? ClientState = default,
string? CommandId = default,
IList<Header>? CustomHeaders = default,
string? SipAuthUsername = default,
string? SipAuthPassword = default,
int? TimeLimitSecs = default,
int? TimeoutSecs = default,
string? WebhookUrl = default,
string? WebhookUrlMethod = default
);
public record BridgeCallsRequest(
string CallControlId,
string? ClientState = default,
string? CommandId = default,
string? ParkAfterUnbridge = default
);
public record PlayAudioRequest(
Uri AudioUrl,
bool Overlay,
object? Loop = default,
string? TargetLegs = default,
string? ClientState = default,
string? CommandId = default
);
public record StopAudioPlaybackRequest(
string? Stop = default,
string? ClientState = default,
string? CommandId = default
);
public record StartRecordingRequest(
string Channels,
string Format,
bool? PlayBeep = default,
string? ClientState = default,
string? CommandId = default
);
public record StopRecordingRequest(
string? ClientState = default,
string? CommandId = default
);
public record SpeakTextRequest(
string Language,
string Voice,
string Payload,
string? PayloadType = default,
string? ServiceLevel = default,
string? Stop = default,
string? ClientState = default,
string? CommandId = default
);

View file

@ -1,87 +0,0 @@
using System.Text.Json.Serialization;
namespace Elsa.Telnyx.Client.Models;
public record TelnyxResponse<T>(T Data);
public record CallStatusResponse(
string CallControlId,
string CallLegId,
string CallSessionId,
string ClientState,
bool IsAlive,
string RecordType
);
public record DialResponse(
string CallControlId,
string CallLegId,
string CallSessionId,
bool IsAlive,
string RecordType
)
{
[JsonConstructor]
public DialResponse() : this(default!, default!, default!, default, default!)
{
}
}
public record NumberLookupResponse(
CallerName CallerName,
Carrier Carrier,
string CountryCode,
string Fraud,
string NationalFormat,
string PhoneNumber,
Portability Portability,
string RecordType
)
{
[JsonConstructor]
public NumberLookupResponse() : this(default!, default!, default!, default!, default!, default!, default!, default!)
{
}
}
public record Portability(
string Altspid,
string AltspidCarrierName,
string AltspidCarrierType,
string City,
string LineType,
string Lrn,
string Ocn,
string? PortedDate,
string PortedStatus,
string Spid,
string SpidCarrierName,
string SpidCarrierType,
string State
)
{
[JsonConstructor]
public Portability() : this(default!, default!, default!, default!, default!, default!, default!, default, default!, default!, default!, default!, default!)
{
}
}
public record Carrier(
string ErrorCode,
string MobileCountryCode,
string MobileNetworkCode,
string Name,
string Type
)
{
[JsonConstructor]
public Carrier() : this(default!, default!, default, default!, default!)
{
}
}
public record CallerName
{
[JsonPropertyName("caller_name")] public string Name { get; set; } = default!;
public string ErrorCode { get; set; } = default!;
}

View file

@ -1,46 +0,0 @@
using Elsa.Telnyx.Client.Models;
using Refit;
namespace Elsa.Telnyx.Client.Services;
public interface ICallsApi
{
[Get("/v2/calls/{callControlId}")]
Task<TelnyxResponse<CallStatusResponse>> GetStatusAsync(string callControlId, CancellationToken cancellationToken = default);
[Post("/v2/calls")]
Task<TelnyxResponse<DialResponse>> DialAsync([Body] DialRequest request, CancellationToken cancellationToken = default);
[Post("/v2/calls/{callControlId}/actions/answer")]
Task AnswerCallAsync(string callControlId, [Body] AnswerCallRequest request, CancellationToken cancellationToken = default);
[Post("/v2/calls/{callControlId}/actions/transfer")]
Task TransferCallAsync(string callControlId, [Body] TransferCallRequest request, CancellationToken cancellationToken = default);
[Post("/v2/calls/{callControlId}/actions/hangup")]
Task HangupCallAsync(string callControlId, [Body] HangupCallRequest request, CancellationToken cancellationToken = default);
[Post("/v2/calls/{callControlId}/actions/gather_using_audio")]
Task GatherUsingAudioAsync(string callControlId, [Body] GatherUsingAudioRequest request, CancellationToken cancellationToken = default);
[Post("/v2/calls/{callControlId}/actions/gather_using_speak")]
Task GatherUsingSpeakAsync(string callControlId, [Body] GatherUsingSpeakRequest request, CancellationToken cancellationToken = default);
[Post("/v2/calls/{callControlId}/actions/bridge")]
Task BridgeCallsAsync(string callControlId, [Body] BridgeCallsRequest request, CancellationToken cancellationToken = default);
[Post("/v2/calls/{callControlId}/actions/playback_start")]
Task<TelnyxResponse<PlayAudioResponse>> PlayAudioAsync(string callControlId, [Body] PlayAudioRequest request, CancellationToken cancellationToken = default);
[Post("/v2/calls/{callControlId}/actions/playback_stop")]
Task StopAudioPlaybackAsync(string callControlId, [Body] StopAudioPlaybackRequest request, CancellationToken cancellationToken = default);
[Post("/v2/calls/{callControlId}/actions/record_start")]
Task StartRecordingAsync(string callControlId, [Body] StartRecordingRequest request, CancellationToken cancellationToken = default);
[Post("/v2/calls/{callControlId}/actions/record_stop")]
Task StopRecordingAsync(string callControlId, [Body] StopRecordingRequest request, CancellationToken cancellationToken = default);
[Post("/v2/calls/{callControlId}/actions/speak")]
Task SpeakTextAsync(string callControlId, [Body] SpeakTextRequest request, CancellationToken cancellationToken = default);
}

View file

@ -1,14 +0,0 @@
using Elsa.Telnyx.Client.Models;
using Refit;
namespace Elsa.Telnyx.Client.Services;
public interface INumberLookupApi
{
[Get("/v2/number_lookup/{phoneNumber}")]
Task<TelnyxResponse<NumberLookupResponse>> NumberLookupAsync(
string phoneNumber,
[Query(CollectionFormat.Multi)] [AliasAs("type")]
IEnumerable<string>? types = default,
CancellationToken cancellationToken = default);
}

View file

@ -1,7 +0,0 @@
namespace Elsa.Telnyx.Client.Services;
public interface ITelnyxClient
{
ICallsApi Calls { get; }
INumberLookupApi NumberLookup { get; }
}

View file

@ -1,17 +0,0 @@
namespace Elsa.Telnyx;
/// <summary>
/// Contains constants used by the Telnyx activities.
/// </summary>
public static class Constants
{
/// <summary>
/// The namespace used by the Telnyx activities.
/// </summary>
public const string Namespace = "Telnyx";
/// <summary>
/// The category used by the Telnyx activities.
/// </summary>
public const string Category = "Telnyx";
}

View file

@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Http;
namespace Elsa.Telnyx.Contracts;
internal interface IWebhookHandler
{
Task HandleAsync(HttpContext httpContext);
}

View file

@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Provides integration with Telnyx.
</Description>
<PackageTags>elsa module telephony telnyx</PackageTags>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Humanizer.Core" />
<PackageReference Include="Refit" />
<PackageReference Include="Refit.HttpClientFactory" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
</Project>

View file

@ -1,24 +0,0 @@
using Elsa.Mediator.Contracts;
using Elsa.Telnyx.Models;
namespace Elsa.Telnyx.Events;
/// <summary>
/// Triggered when a Telnyx webhook is received.
/// </summary>
public class TelnyxWebhookReceived : INotification
{
/// <summary>
/// Initializes a new instance of the <see cref="TelnyxWebhookReceived"/> class.
/// </summary>
public TelnyxWebhookReceived(TelnyxWebhook webhook)
{
Webhook = webhook;
}
/// <summary>
/// Gets the webhook.
/// </summary>
public TelnyxWebhook Webhook { get; }
}

View file

@ -1,8 +0,0 @@
namespace Elsa.Telnyx.Exceptions;
public class MissingCallControlAppIdException : TelnyxException
{
public MissingCallControlAppIdException(string message, Exception? innerException = default) : base(message, innerException)
{
}
}

View file

@ -1,8 +0,0 @@
namespace Elsa.Telnyx.Exceptions;
public class MissingCallControlIdException : TelnyxException
{
public MissingCallControlIdException(string message, Exception? innerException = default) : base(message, innerException)
{
}
}

View file

@ -1,8 +0,0 @@
namespace Elsa.Telnyx.Exceptions;
public class MissingFromNumberException : TelnyxException
{
public MissingFromNumberException(string message, Exception? innerException = default) : base(message, innerException)
{
}
}

View file

@ -1,8 +0,0 @@
namespace Elsa.Telnyx.Exceptions;
public class TelnyxException : Exception
{
public TelnyxException(string message, Exception? innerException = default) : base(message, innerException)
{
}
}

View file

@ -1,18 +0,0 @@
using Elsa.Telnyx.Models;
using Elsa.Workflows;
namespace Elsa.Telnyx.Extensions;
/// <summary>
/// Provides extensions on <see cref="ActivityExecutionContext"/>.
/// </summary>
public static class ActivityExecutionExtensions
{
/// <summary>
/// Creates a correlating client state.
/// </summary>
public static string CreateCorrelatingClientState(this ActivityExecutionContext context, string? activityInstanceId = default)
{
return new ClientStatePayload(context.WorkflowExecutionContext.Id, activityInstanceId).ToBase64();
}
}

View file

@ -1,29 +0,0 @@
using Elsa.Telnyx.Client.Models;
using Refit;
namespace Elsa.Telnyx.Extensions;
/// <summary>
/// Provides extensions for <see cref="ApiException"/>.
/// </summary>
public static class ApiExceptionExtensions
{
/// <summary>
/// Reads a <see cref="ErrorResponse"/> from the specified <see cref="ApiException"/>.
/// </summary>
public static async Task<ErrorResponse> GetErrorResponseAsync(this ApiException e, CancellationToken cancellationToken = default)
{
var httpContent = new StringContent(e.Content!);
return (await e.RefitSettings.ContentSerializer.FromHttpContentAsync<ErrorResponse>(httpContent, cancellationToken))!;
}
/// <summary>
/// Returns <code>true</code> if the specified exception represents a failure due to the call no longer being active.
/// </summary>
public static async Task<bool> CallIsNoLongerActiveAsync(this ApiException e, CancellationToken cancellationToken = default)
{
var errorResponse = await e.GetErrorResponseAsync(cancellationToken);
var errors = errorResponse.Errors;
return errors.Any(x => x.Code == ErrorCodes.CallHasAlreadyEnded);
}
}

View file

@ -1,103 +0,0 @@
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Telnyx.Client.Implementations;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Contracts;
using Elsa.Telnyx.Handlers;
using Elsa.Telnyx.Options;
using Elsa.Telnyx.Serialization;
using Elsa.Telnyx.Services;
using Microsoft.Extensions.Options;
using Refit;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Provides service dependency extensions that register required services for Telnyx integration.
/// </summary>
public static class DependencyInjectionExtensions
{
/// <summary>
/// Adds Telnyx services to the service container.
/// </summary>
/// <returns></returns>
public static IServiceCollection AddTelnyx(
this IServiceCollection services,
Action<TelnyxOptions>? configure = default,
Func<IServiceProvider, HttpClient>? httpClientFactory = default,
Action<IHttpClientBuilder>? configureHttpClientBuilder = default)
{
// Telnyx options.
configure ??= options => options.ApiUrl = new Uri("https://api.telnyx.com");
services.Configure(configure);
// Services.
services
.AddNotificationHandlersFrom<TriggerWebhookActivities>()
.AddScoped<IWebhookHandler, WebhookHandler>();
// Telnyx API Client.
var refitSettings = CreateRefitSettings();
services
.AddApiClient<ICallsApi>(refitSettings, httpClientFactory, configureHttpClientBuilder)
.AddApiClient<INumberLookupApi>(refitSettings, httpClientFactory, configureHttpClientBuilder)
.AddTransient<ITelnyxClient, TelnyxClient>();
return services;
}
/// <summary>
/// Registers the specified interface type as a Refit client.
/// </summary>
private static IServiceCollection AddApiClient<T>(
this IServiceCollection services,
RefitSettings refitSettings,
Func<IServiceProvider, HttpClient>? httpClientFactory,
Action<IHttpClientBuilder>? configureHttpClientBuilder) where T : class
{
if (httpClientFactory == null)
{
var httpClientBuilder = services.AddRefitClient<T>(refitSettings).ConfigureHttpClient((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<TelnyxOptions>>().Value;
client.BaseAddress = options.ApiUrl;
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.ApiKey);
});
configureHttpClientBuilder?.Invoke(httpClientBuilder);
}
else
{
services.AddScoped(sp =>
{
var httpClient = httpClientFactory(sp);
var options = sp.GetRequiredService<IOptions<TelnyxOptions>>().Value;
httpClient.BaseAddress ??= options.ApiUrl;
httpClient.DefaultRequestHeaders.Authorization ??= new AuthenticationHeaderValue("Bearer", options.ApiKey);
return RestService.For<T>(httpClient, refitSettings);
});
}
return services;
}
private static RefitSettings CreateRefitSettings()
{
var serializerSettings = new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = new SnakeCaseNamingPolicy(),
};
serializerSettings.Converters.Add(new WebhookDataJsonConverter());
return new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(serializerSettings)
};
}
}

View file

@ -1,31 +0,0 @@
using Elsa.Telnyx.Contracts;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Provides extensions on <see cref="IEndpointRouteBuilder"/>
/// </summary>
[PublicAPI]
public static class EndpointsExtensions
{
/// <summary>
/// Maps the specified route to the Telnyx webhook handler.
/// </summary>
public static IEndpointConventionBuilder UseTelnyxWebhooks(this IEndpointRouteBuilder endpoints, string routePattern = "telnyx-hook")
{
return endpoints.MapPost(routePattern, HandleTelnyxRequest);
}
private static async Task HandleTelnyxRequest(HttpContext context)
{
var services = context.RequestServices;
var webhookHandler = services.GetRequiredService<IWebhookHandler>();
await webhookHandler.HandleAsync(context);
}
}

View file

@ -1,14 +0,0 @@
using Elsa.Features.Services;
using Elsa.Telnyx.Features;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
public static class ModuleExtensions
{
public static IModule UseTelnyx(this IModule module, Action<TelnyxFeature>? configure = default)
{
module.Configure(configure);
return module;
}
}

View file

@ -1,20 +0,0 @@
using Elsa.Telnyx.Models;
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Extensions;
/// <summary>
/// Provides extensions on <see cref="Payload"/>.
/// </summary>
public static class PayloadExtensions
{
/// <summary>
/// Extracts a <see cref="ClientStatePayload"/> from the specified <see cref="Payload"/>.
/// </summary>
public static ClientStatePayload? GetClientStatePayload(this Payload payload)
{
return !string.IsNullOrWhiteSpace(payload.ClientState)
? ClientStatePayload.FromBase64(payload.ClientState)
: default;
}
}

View file

@ -1,19 +0,0 @@
using System.Text.RegularExpressions;
using Humanizer;
namespace Elsa.Telnyx.Extensions;
public static class StringExtensions
{
private static readonly Regex WhiteList = new(@"[^a-zA-Z0-9-_.!~+ ]");
/// <summary>
/// Sanitizes the caller name.
/// </summary>
public static string? SanitizeCallerName(this string? value) => value == null ? null : WhiteList.Replace(value, "").Truncate(128);
/// <summary>
/// Returns <code>null</code> if the specifies string is empty.
/// </summary>
public static string? EmptyToNull(this string? value) => value is "" ? null : value;
}

View file

@ -1,69 +0,0 @@
using System.ComponentModel;
using System.Reflection;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.Telnyx.Activities;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Options;
using Elsa.Telnyx.Payloads.Abstractions;
using Elsa.Telnyx.Providers;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Telnyx.Features;
/// <summary>
/// Enables Telnyx integration.
/// </summary>
public class TelnyxFeature : FeatureBase
{
private const string TelnyxCategoryName = "Telnyx";
/// <inheritdoc />
public TelnyxFeature(IModule module) : base(module)
{
}
/// <summary>
/// Configures Telnyx options.
/// </summary>
public Action<TelnyxOptions> ConfigureTelnyxOptions { get; set; } = _ => { };
/// <summary>
/// Gets or sets a factory that creates an <see cref="HttpClient"/> used to communicate with the Telnyx API.
/// </summary>
public Func<IServiceProvider, HttpClient>? HttpClientFactory { get; set; }
/// <summary>
/// Configures the <see cref="IHttpClientBuilder"/> used to communicate with the Telnyx API.
/// </summary>
public Action<IHttpClientBuilder>? ConfigureHttpClientBuilder { get; set; }
/// <inheritdoc />
public override void Configure()
{
Module.UseWorkflowManagement(management =>
{
management.AddActivitiesFrom<IncomingCall>();
management.AddVariableTypes(typeof(TelnyxFeature).Assembly.ExportedTypes.Where(x =>
{
var browsableAttr = x.GetCustomAttribute<BrowsableAttribute>();
return typeof(Payload).IsAssignableFrom(x) && browsableAttr == null || browsableAttr?.Browsable == true;
}), TelnyxCategoryName);
management.AddVariableType<DialResponse>(TelnyxCategoryName);
management.AddVariableType<NumberLookupResponse>(TelnyxCategoryName);
management.AddVariableType<Carrier>(TelnyxCategoryName);
management.AddVariableType<CallerName>(TelnyxCategoryName);
});
}
/// <inheritdoc />
public override void Apply()
{
Services
.AddTelnyx(ConfigureTelnyxOptions, HttpClientFactory, ConfigureHttpClientBuilder)
.AddActivityProvider<WebhookEventActivityProvider>();
}
}

View file

@ -1,3 +0,0 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -1,56 +0,0 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Telnyx.Activities;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Events;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
namespace Elsa.Telnyx.Handlers;
/// <summary>
/// Triggers all workflows blocked on a <see cref="AnswerCall"/> or <see cref="FlowAnswerCall"/> activity.
/// </summary>
[PublicAPI]
internal class TriggerAnswerCallActivities(IStimulusSender stimulusSender, ILogger<TriggerAnswerCallActivities> logger)
: INotificationHandler<TelnyxWebhookReceived>
{
private readonly ILogger _logger = logger;
public async Task HandleAsync(TelnyxWebhookReceived notification, CancellationToken cancellationToken)
{
var webhook = notification.Webhook;
var payload = webhook.Data.Payload;
if (payload is not CallAnsweredPayload callAnsweredPayload)
return;
var clientStatePayload = callAnsweredPayload.GetClientStatePayload();
var workflowInstanceId = clientStatePayload?.WorkflowInstanceId;
var activityInstanceId = clientStatePayload?.ActivityInstanceId!;
var input = new Dictionary<string, object>().AddInput(callAnsweredPayload);
var callControlId = callAnsweredPayload.CallControlId;
var activityTypeNames = new[]
{
ActivityTypeNameHelper.GenerateTypeName<AnswerCall>(),
ActivityTypeNameHelper.GenerateTypeName<FlowAnswerCall>(),
};
foreach (var activityTypeName in activityTypeNames)
{
var stimulus = new AnswerCallStimulus(callControlId);
var metadata = new StimulusMetadata
{
WorkflowInstanceId = workflowInstanceId,
ActivityInstanceId = activityInstanceId,
Input = input
};
await stimulusSender.SendAsync(activityTypeName, stimulus, metadata, cancellationToken);
}
}
}

View file

@ -1,45 +0,0 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Telnyx.Activities;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Events;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
namespace Elsa.Telnyx.Handlers;
/// <summary>
/// Triggers all workflows starting with or blocked on a <see cref="CallAnswered"/> activity.
/// </summary>
[PublicAPI]
internal class TriggerCallAnsweredActivities(IStimulusSender stimulusSender, ILogger<TriggerCallAnsweredActivities> logger) : INotificationHandler<TelnyxWebhookReceived>
{
private readonly ILogger _logger = logger;
public async Task HandleAsync(TelnyxWebhookReceived notification, CancellationToken cancellationToken)
{
var webhook = notification.Webhook;
var payload = webhook.Data.Payload;
if (payload is not CallAnsweredPayload callAnsweredPayload)
return;
var clientStatePayload = callAnsweredPayload.GetClientStatePayload();
var workflowInstanceId = clientStatePayload?.WorkflowInstanceId;
var activityInstanceId = clientStatePayload?.ActivityInstanceId!;
var input = new Dictionary<string, object>().AddInput(callAnsweredPayload);
var callControlId = callAnsweredPayload.CallControlId;
var stimulus = new CallAnsweredStimulus(callControlId);
var metadata = new StimulusMetadata
{
WorkflowInstanceId = workflowInstanceId,
ActivityInstanceId = activityInstanceId,
Input = input
};
await stimulusSender.SendAsync<CallAnswered>(stimulus, metadata, cancellationToken);
}
}

View file

@ -1,49 +0,0 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Telnyx.Activities;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Events;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Handlers;
/// <summary>
/// Triggers all workflows starting with or blocked on a <see cref="CallAnswered"/> activity.
/// </summary>
[PublicAPI]
internal class TriggerCallBridgedActivities(IStimulusSender stimulusSender) : INotificationHandler<TelnyxWebhookReceived>
{
public async Task HandleAsync(TelnyxWebhookReceived notification, CancellationToken cancellationToken)
{
var webhook = notification.Webhook;
var payload = webhook.Data.Payload;
if (payload is not CallBridgedPayload callBridgedPayload)
return;
var clientStatePayload = callBridgedPayload.GetClientStatePayload();
var workflowInstanceId = clientStatePayload?.WorkflowInstanceId;
var input = new Dictionary<string, object>().AddInput(callBridgedPayload);
var callControlId = callBridgedPayload.CallControlId;
var activityTypeNames = new[]
{
ActivityTypeNameHelper.GenerateTypeName<BridgeCalls>(),
ActivityTypeNameHelper.GenerateTypeName<FlowBridgeCalls>(),
};
foreach (var activityTypeName in activityTypeNames)
{
var stimulus = new WebhookEventStimulus(WebhookEventTypes.CallBridged, callControlId);
var metadata = new StimulusMetadata
{
WorkflowInstanceId = workflowInstanceId,
Input = input
};
await stimulusSender.SendAsync(activityTypeName, stimulus, metadata, cancellationToken);
}
}
}

View file

@ -1,40 +0,0 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Telnyx.Activities;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Events;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Handlers;
/// <summary>
/// Triggers all workflows starting with or blocked on a <see cref="CallHangup"/> activity.
/// </summary>
[PublicAPI]
internal class TriggerCallHangupActivities(IStimulusSender stimulusSender)
: INotificationHandler<TelnyxWebhookReceived>
{
public async Task HandleAsync(TelnyxWebhookReceived notification, CancellationToken cancellationToken)
{
var webhook = notification.Webhook;
var payload = webhook.Data.Payload;
if (payload is not CallHangupPayload callHangupPayload)
return;
var clientStatePayload = callHangupPayload.GetClientStatePayload();
var workflowInstanceId = clientStatePayload?.WorkflowInstanceId;
var input = new Dictionary<string, object>().AddInput(callHangupPayload);
var callControlId = callHangupPayload.CallControlId;
var stimulus = new CallHangupStimulus(callControlId);
var metadata = new StimulusMetadata
{
WorkflowInstanceId = workflowInstanceId,
Input = input
};
await stimulusSender.SendAsync<CallHangup>(stimulus, metadata, cancellationToken);
}
}

View file

@ -1,55 +0,0 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Telnyx.Activities;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Events;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Handlers;
/// <summary>
/// Triggers all workflows starting with or blocked on a <see cref="IncomingCall"/> activity.
/// </summary>
[PublicAPI]
internal class TriggerIncomingCallActivities(IStimulusSender stimulusSender)
: INotificationHandler<TelnyxWebhookReceived>
{
public async Task HandleAsync(TelnyxWebhookReceived notification, CancellationToken cancellationToken)
{
var webhook = notification.Webhook;
var payload = webhook.Data.Payload;
if (payload is not CallInitiatedPayload callInitiatedPayload)
return;
// Only trigger workflows for incoming calls.
if (callInitiatedPayload.Direction != "incoming" || callInitiatedPayload.ClientState != null)
return;
var input = new Dictionary<string, object>().AddInput(webhook);
var stimulusMetadata = new StimulusMetadata
{
Input = input
};
// Trigger all workflows matching the From number.
var fromNumber = callInitiatedPayload.From;
var fromStimulus = new IncomingCallFromStimulus(fromNumber);
var fromResults = await stimulusSender.SendAsync<IncomingCall>(fromStimulus, stimulusMetadata, cancellationToken);
// Trigger all workflows matching the To number.
var toNumber = callInitiatedPayload.To;
var toStimulus = new IncomingCallToStimulus(toNumber);
var toResults = await stimulusSender.SendAsync<IncomingCall>(toStimulus, stimulusMetadata, cancellationToken);
// If any workflows were triggered, don't trigger the catch-all workflows.
if (fromResults.WorkflowInstanceResponses.Any() || toResults.WorkflowInstanceResponses.Any())
return;
// Trigger all catch-all workflows.
var catchAllStimulus = new IncomingCallCatchAllStimulus();
await stimulusSender.SendAsync<IncomingCall>(catchAllStimulus, stimulusMetadata, cancellationToken);
}
}

View file

@ -1,44 +0,0 @@
using System.Reflection;
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Telnyx.Activities;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Events;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Payloads.Abstractions;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Handlers;
/// <summary>
/// Triggers all workflows starting with or blocked on a <see cref="WebhookEvent"/> activity.
/// </summary>
[PublicAPI]
internal class TriggerWebhookActivities(IStimulusSender stimulusSender) : INotificationHandler<TelnyxWebhookReceived>
{
public async Task HandleAsync(TelnyxWebhookReceived notification, CancellationToken cancellationToken)
{
var webhook = notification.Webhook;
var eventType = webhook.Data.EventType;
var payload = webhook.Data.Payload;
var activityType = payload.GetType().GetCustomAttribute<WebhookActivityAttribute>()?.ActivityType;
if (activityType == null)
return;
var workflowInstanceId = ((Payload)webhook.Data.Payload).GetClientStatePayload()?.WorkflowInstanceId;
var callControlId = (webhook.Data.Payload as CallPayload)?.CallControlId;
var stimulus = new WebhookEventStimulus(eventType, callControlId);
var input = new Dictionary<string, object>().AddInput(webhook);
var metadata = new StimulusMetadata
{
Input = input,
WorkflowInstanceId = workflowInstanceId,
};
await stimulusSender.SendAsync(activityType, stimulus, metadata, cancellationToken);
}
}

View file

@ -1,51 +0,0 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Events;
using Elsa.Telnyx.Extensions;
using Elsa.Telnyx.Payloads.Abstractions;
using Elsa.Workflows;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Handlers;
/// <summary>
/// Resumes all workflows blocked on activities that are waiting for a given webhook.
/// </summary>
[PublicAPI]
internal class TriggerWebhookDrivenActivities(IStimulusSender stimulusSender, IActivityRegistry activityRegistry)
: INotificationHandler<TelnyxWebhookReceived>
{
public async Task HandleAsync(TelnyxWebhookReceived notification, CancellationToken cancellationToken)
{
var webhook = notification.Webhook;
var eventType = webhook.Data.EventType;
var eventPayload = webhook.Data.Payload;
var callPayload = eventPayload as CallPayload;
var callControlId = callPayload?.CallControlId;
var input = new Dictionary<string, object>().AddInput(eventPayload.GetType().Name, eventPayload);
var activityDescriptors = FindActivityDescriptors(eventType).ToList();
var clientStatePayload = ((Payload)webhook.Data.Payload).GetClientStatePayload();
var activityInstanceId = clientStatePayload?.ActivityInstanceId;
var workflowInstanceId = clientStatePayload?.WorkflowInstanceId;
var bookmarkPayloadWithCallControl = new WebhookEventStimulus(eventType, callControlId);
foreach (var activityDescriptor in activityDescriptors)
{
var metadata = new StimulusMetadata
{
WorkflowInstanceId = workflowInstanceId,
ActivityInstanceId = activityInstanceId,
Input = input
};
await stimulusSender.SendAsync(activityDescriptor.TypeName, bookmarkPayloadWithCallControl, metadata, cancellationToken);
}
}
private IEnumerable<ActivityDescriptor> FindActivityDescriptors(string eventType) =>
activityRegistry.FindMany(descriptor => descriptor.GetAttribute<WebhookDrivenAttribute>()?.EventTypes.Contains(eventType) == true);
}

View file

@ -1,14 +0,0 @@
using System.Text.Json;
using Elsa.Telnyx.Payloads;
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Helpers;
internal static class PayloadSerializer
{
public static Payload Deserialize(string eventType, JsonElement dataModel, JsonSerializerOptions? options = null)
{
var payloadType = WebhookPayloadTypes.PayloadTypeDictionary.TryGetValue(eventType, out var value) ? value : typeof(UnsupportedPayload);
return (Payload)dataModel.Deserialize(payloadType, options)!;
}
}

View file

@ -1,34 +0,0 @@
using System.Reflection;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Helpers;
/// <summary>
/// A helper class related to webhook event payload discovery.
/// </summary>
public static class WebhookPayloadTypes
{
/// <summary>
/// A list of <see cref="Payload"/> types.
/// </summary>
public static readonly ICollection<Type> PayloadTypes;
/// <summary>
/// A dictionary of event types mapped to payload types.
/// </summary>
public static readonly IDictionary<string, Type> PayloadTypeDictionary;
static WebhookPayloadTypes()
{
PayloadTypes = typeof(WebhookPayloadTypes).Assembly.GetTypes().Where(x => typeof(Payload).IsAssignableFrom(x) && x.GetCustomAttribute<WebhookAttribute>(true) != null).ToList();
var query =
from payloadType in PayloadTypes
let payloadAttribute = payloadType.GetCustomAttribute<WebhookAttribute>()
where payloadAttribute != null
select (payloadType, payloadAttribute);
PayloadTypeDictionary = query.ToDictionary(x => x.payloadAttribute!.EventType, x => x.payloadType);
}
}

View file

@ -1,19 +0,0 @@
using System.Text.Json;
using Elsa.Telnyx.Serialization;
namespace Elsa.Telnyx.Helpers;
internal static class WebhookSerializerOptions
{
public static JsonSerializerOptions Create()
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = new SnakeCaseNamingPolicy()
};
options.Converters.Add(new WebhookDataJsonConverter());
return options;
}
}

View file

@ -1,11 +0,0 @@
using Elsa.Telnyx.Activities;
using Elsa.Telnyx.Payloads.Call;
namespace Elsa.Telnyx.Models;
/// <summary>
/// Contains output of the <see cref="BridgeCalls"/> activity.
/// </summary>
/// <param name="PayloadA">The payload from leg A.</param>
/// <param name="PayloadB">The payload from leg B.</param>
public record BridgedCallsOutput(CallBridgedPayload PayloadA, CallBridgedPayload PayloadB);

View file

@ -1,3 +0,0 @@
namespace Elsa.Telnyx.Models;
public record CallRecordingUrls(string? Wav, string? Mp3);

View file

@ -1,33 +0,0 @@
using System.Text;
using System.Text.Json;
namespace Elsa.Telnyx.Models;
/// <summary>
/// Represents the client state payload to correlate commands and events.
/// </summary>
/// <param name="WorkflowInstanceId">The correlation ID.</param>
/// <param name="ActivityInstanceId">An optional activity instance ID.</param>
public record ClientStatePayload(string WorkflowInstanceId, string? ActivityInstanceId = default)
{
/// <summary>
/// Deserializes a <see cref="ClientStatePayload"/> from the specified base64 string.
/// </summary>
public static ClientStatePayload FromBase64(string base64)
{
var bytes = Convert.FromBase64String(base64);
var json = Encoding.UTF8.GetString(bytes);
return JsonSerializer.Deserialize<ClientStatePayload>(json)!;
}
/// <summary>
/// Serializes the <see cref="ClientStatePayload"/> to a base64 string.
/// </summary>
/// <returns></returns>
public string ToBase64()
{
var json = JsonSerializer.Serialize(this);
var bytes = Encoding.UTF8.GetBytes(json);
return Convert.ToBase64String(bytes);
}
}

View file

@ -1,3 +0,0 @@
namespace Elsa.Telnyx.Models;
public abstract record TelnyxRecord(string RecordType);

View file

@ -1,8 +0,0 @@
namespace Elsa.Telnyx.Models;
[Serializable]
public class TelnyxWebhook
{
public TelnyxWebhookMeta Meta { get; set; } = default!;
public TelnyxWebhookData Data { get; set; } = default!;
}

View file

@ -1,6 +0,0 @@
namespace Elsa.Telnyx.Models;
// TODO: The Payload property is of type object instead of Payload, this in order for the JSON serializer to serialize derived type properties.
// Once moved to .NET 7, we have more control over polymorphism.
// https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-7-0
public record TelnyxWebhookData(string EventType, Guid Id, DateTimeOffset OccurredAt, string RecordType, object Payload) : TelnyxRecord(EventType);

View file

@ -1,7 +0,0 @@
namespace Elsa.Telnyx.Models;
public class TelnyxWebhookMeta
{
public int Attempt { get; set; }
public string DeliveredTo { get; set; } = default!;
}

View file

@ -1,8 +0,0 @@
namespace Elsa.Telnyx.Options;
public class TelnyxOptions
{
public Uri ApiUrl { get; set; } = new("https://api.telnyx.com");
public string ApiKey { get; set; } = default!;
public string? CallControlAppId { get; set; }
}

View file

@ -1,12 +0,0 @@
namespace Elsa.Telnyx.Payloads.Abstractions;
/// <summary>
/// A base class for payloads that are related to a call.
/// </summary>
public abstract record CallPayload : Payload
{
public string CallControlId { get; init; } = default!;
public string CallLegId { get; init; } = default!;
public string CallSessionId { get; init; } = default!;
public string ConnectionId { get; init; } = default!;
}

View file

@ -1,7 +0,0 @@
namespace Elsa.Telnyx.Payloads.Abstractions;
public abstract record CallPlayback : CallPayload
{
public Uri MediaUrl { get; init; } = default!;
public bool Overlay { get; set; }
}

View file

@ -1,6 +0,0 @@
namespace Elsa.Telnyx.Payloads.Abstractions;
public abstract record Payload
{
public string? ClientState { get; set; }
}

View file

@ -1,26 +0,0 @@
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Payloads.Call;
/// <summary>
/// Represents the payload of a webhook event that is triggered when an incoming call is answered.
/// </summary>
[Webhook(WebhookEventTypes.CallAnswered)]
public sealed record CallAnsweredPayload : CallPayload
{
/// <summary>
/// The from number.
/// </summary>
public string From { get; init; } = default!;
/// <summary>
/// The to number.
/// </summary>
public string To { get; init; } = default!;
/// <summary>
/// Call state.
/// </summary>
public string State { get; init; } = default!;
}

View file

@ -1,12 +0,0 @@
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Payloads.Call;
[WebhookActivity(WebhookEventTypes.CallBridged, WebhookActivityTypeNames.CallBridged, "Call Bridged", "Triggered when an a call is bridged.")]
public sealed record CallBridgedPayload : CallPayload
{
public string From { get; init; } = default!;
public string To { get; init; } = default!;
public string State { get; init; } = default!;
}

View file

@ -1,12 +0,0 @@
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Payloads.Call;
[WebhookActivity(WebhookEventTypes.CallDtmfReceived, WebhookActivityTypeNames.CallDtmfReceived, "Call DTMF Received", "Triggered when DTMF input is received.")]
public sealed record CallDtmfReceivedPayload : CallPayload
{
public string Digit { get; set; } = default!;
public string From { get; set; } = default!;
public string To { get; set; } = default!;
}

View file

@ -1,13 +0,0 @@
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Payloads.Call;
[WebhookActivity(WebhookEventTypes.CallGatherEnded, WebhookActivityTypeNames.CallGatherEnded, "Call Gather Ended", "Triggered when an call gather has ended.")]
public sealed record CallGatherEndedPayload : CallPayload
{
public string Digits { get; set; } = default!;
public string From { get; set; } = default!;
public string To { get; set; } = default!;
public string Status { get; set; } = default!;
}

View file

@ -1,19 +0,0 @@
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Payloads.Call;
/// <summary>
/// A payload representing the call.hangup Telnyx webhook event.
/// </summary>
[Webhook(WebhookEventTypes.CallHangup)]
public sealed record CallHangupPayload : CallPayload
{
public DateTimeOffset StartTime { get; init; }
public DateTimeOffset EndTime { get; init; }
public string SipHangupCause { get; init; } = default!;
public string HangupSource { get; init; } = default!;
public string HangupCause { get; init; } = default!;
public string From { get; set; } = default!;
public string To { get; set; } = default!;
}

View file

@ -1,14 +0,0 @@
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Payloads.Call;
[Webhook(WebhookEventTypes.CallInitiated)]
public sealed record CallInitiatedPayload : CallPayload
{
public string Direction { get; init; } = default!;
public string State { get; init; } = default!;
public string To { get; init; } = default!;
public string From { get; init; } = default!;
public DateTimeOffset StartTime { get; set; } = default!;
}

View file

@ -1,8 +0,0 @@
using Elsa.Telnyx.Payloads.Abstractions;
namespace Elsa.Telnyx.Payloads.Call;
public record CallMachineDetectionEndedBase : CallPayload
{
public string Result { get; set; } = default!;
}

View file

@ -1,6 +0,0 @@
using Elsa.Telnyx.Attributes;
namespace Elsa.Telnyx.Payloads.Call;
[WebhookActivity(WebhookEventTypes.CallMachineGreetingEnded, WebhookActivityTypeNames.CallMachineGreetingEnded, "Call Machine Greeting Ended", "Triggered when a machine greeting has ended.")]
public sealed record CallMachineGreetingEnded : CallMachineGreetingEndedBase;

Some files were not shown because too many files have changed in this diff Show more