Merge pull request #6437 from elsa-workflows/task/remove-telnyx
Remove Telnyx module from the codebase
This commit is contained in:
commit
28ad3b3ed7
7
Elsa.sln
7
Elsa.sln
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
namespace Elsa.Telnyx.Bookmarks;
|
||||
|
||||
public record IncomingCallCatchAllStimulus;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
namespace Elsa.Telnyx.Bookmarks;
|
||||
|
||||
public record IncomingCallFromStimulus(string PhoneNumber);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
namespace Elsa.Telnyx.Bookmarks;
|
||||
|
||||
public record IncomingCallToStimulus(string PhoneNumber);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
namespace Elsa.Telnyx.Client.Models;
|
||||
|
||||
public record Header(string Name, string Value);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
namespace Elsa.Telnyx.Client.Models;
|
||||
|
||||
public record PlayAudioResponse(string Result);
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
namespace Elsa.Telnyx.Client.Services;
|
||||
|
||||
public interface ITelnyxClient
|
||||
{
|
||||
ICallsApi Calls { get; }
|
||||
INumberLookupApi NumberLookup { get; }
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Telnyx.Contracts;
|
||||
|
||||
internal interface IWebhookHandler
|
||||
{
|
||||
Task HandleAsync(HttpContext httpContext);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace Elsa.Telnyx.Exceptions;
|
||||
|
||||
public class MissingCallControlAppIdException : TelnyxException
|
||||
{
|
||||
public MissingCallControlAppIdException(string message, Exception? innerException = default) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace Elsa.Telnyx.Exceptions;
|
||||
|
||||
public class MissingCallControlIdException : TelnyxException
|
||||
{
|
||||
public MissingCallControlIdException(string message, Exception? innerException = default) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace Elsa.Telnyx.Exceptions;
|
||||
|
||||
public class MissingFromNumberException : TelnyxException
|
||||
{
|
||||
public MissingFromNumberException(string message, Exception? innerException = default) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace Elsa.Telnyx.Exceptions;
|
||||
|
||||
public class TelnyxException : Exception
|
||||
{
|
||||
public TelnyxException(string message, Exception? innerException = default) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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)!;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
namespace Elsa.Telnyx.Models;
|
||||
|
||||
public record CallRecordingUrls(string? Wav, string? Mp3);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
namespace Elsa.Telnyx.Models;
|
||||
|
||||
public abstract record TelnyxRecord(string RecordType);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace Elsa.Telnyx.Models;
|
||||
|
||||
[Serializable]
|
||||
public class TelnyxWebhook
|
||||
{
|
||||
public TelnyxWebhookMeta Meta { get; set; } = default!;
|
||||
public TelnyxWebhookData Data { get; set; } = default!;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
namespace Elsa.Telnyx.Models;
|
||||
|
||||
public class TelnyxWebhookMeta
|
||||
{
|
||||
public int Attempt { get; set; }
|
||||
public string DeliveredTo { get; set; } = default!;
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
namespace Elsa.Telnyx.Payloads.Abstractions;
|
||||
|
||||
public abstract record Payload
|
||||
{
|
||||
public string? ClientState { get; set; }
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using Elsa.Telnyx.Payloads.Abstractions;
|
||||
|
||||
namespace Elsa.Telnyx.Payloads.Call;
|
||||
|
||||
public record CallMachineDetectionEndedBase : CallPayload
|
||||
{
|
||||
public string Result { get; set; } = default!;
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue