diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs b/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs new file mode 100644 index 000000000..bd8efe642 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs @@ -0,0 +1,110 @@ +using System; +using System.Threading.Tasks; +using Elsa.Activities.Telnyx.Client.Models; +using Elsa.Activities.Telnyx.Client.Services; +using Elsa.Activities.Telnyx.Extensions; +using Elsa.ActivityResults; +using Elsa.Attributes; +using Elsa.Design; +using Elsa.Exceptions; +using Elsa.Services; +using Elsa.Services.Models; +using Refit; + +namespace Elsa.Activities.Telnyx.Activities +{ + [Action( + Category = Constants.Category, + Description = "Convert text to speech and play it back on the call.", + Outcomes = new[] { OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive }, + DisplayName = "Speak Text" + )] + public class SpeakText : Activity + { + private readonly ITelnyxClient _telnyxClient; + public SpeakText(ITelnyxClient telnyxClient) => _telnyxClient = telnyxClient; + + [ActivityProperty( + Label = "Call Control ID", + Hint = "Unique identifier and token for controlling the call", + Category = PropertyCategories.Advanced + )] + public string CallControlId { get; set; } = default!; + + [ActivityProperty( + Hint = "The language you want spoken.", + UIHint = ActivityPropertyUIHints.Dropdown, + Options = new[] { "en-US", "en-AU", "nl-NL", "es-ES", "ru-RU" } + )] + public string Language { get; set; } = default!; + + [ActivityProperty( + Hint = "The gender of the voice used to speak back the text.", + UIHint = ActivityPropertyUIHints.Dropdown, + Options = new[] { "female", "male" } + )] + public string Voice { get; set; } = default!; + + [ActivityProperty(Hint = "The text or SSML to be converted into speech. There is a 5,000 character limit.")] + public string Payload { get; set; } + + [ActivityProperty( + Hint = "The type of the provided payload. The payload can either be plain text, or Speech Synthesis Markup Language (SSML).", + UIHint = ActivityPropertyUIHints.Dropdown, + Options = new[] { "", "text", "ssml" } + )] + public string? PayloadType { get; set; } + + [ActivityProperty( + Hint = "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 = ActivityPropertyUIHints.Dropdown, + Options = new[] { "", "basic", "premium" }, + Category = PropertyCategories.Advanced + )] + public string? ServiceLevel { get; set; } + + [ActivityProperty( + Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", + Category = PropertyCategories.Advanced + )] + public string? ClientState { get; set; } + + [ActivityProperty( + Label = "Command ID", + Hint = "Use this field to avoid duplicate commands. Telnyx will ignore commands with the same Command ID.", + Category = PropertyCategories.Advanced + )] + public string? CommandId { get; set; } + + protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) + { + var request = new SpeakTextRequest( + Language, + Voice, + Payload, + EmptyToNull(PayloadType), + EmptyToNull(ClientState), + EmptyToNull(CommandId), + EmptyToNull(ServiceLevel), + null + ); + + var callControlId = CallControlId; + + try + { + await _telnyxClient.Calls.SpeakTextAsync(callControlId, request, context.CancellationToken); + return Done(); + } + catch (ApiException e) + { + if (await e.CallIsNoLongerActiveAsync()) + return Outcome(TelnyxOutcomeNames.CallIsNoLongerActive); + + throw new WorkflowException(e.Content ?? e.Message, e); + } + } + + private static string? EmptyToNull(string? value) => value is "" ? null : value; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/StartRecording.cs b/src/activities/Elsa.Activities.Telnyx/Activities/StartRecording.cs new file mode 100644 index 000000000..353d3b0b1 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Activities/StartRecording.cs @@ -0,0 +1,91 @@ +using System.Threading.Tasks; +using Elsa.Activities.Telnyx.Client.Models; +using Elsa.Activities.Telnyx.Client.Services; +using Elsa.Activities.Telnyx.Extensions; +using Elsa.ActivityResults; +using Elsa.Attributes; +using Elsa.Design; +using Elsa.Exceptions; +using Elsa.Services; +using Elsa.Services.Models; +using Refit; + +namespace Elsa.Activities.Telnyx.Activities +{ + [Action( + Category = Constants.Category, + Description = "Start recording the call.", + Outcomes = new[] { OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive }, + DisplayName = "Start Recording" + )] + public class StartRecording : Activity + { + private readonly ITelnyxClient _telnyxClient; + public StartRecording(ITelnyxClient telnyxClient) => _telnyxClient = telnyxClient; + + [ActivityProperty( + Label = "Call Control ID", + Hint = "Unique identifier and token for controlling the call", + Category = PropertyCategories.Advanced + )] + public string CallControlId { get; set; } = default!; + + [ActivityProperty( + Hint = "When 'dual', final audio file will be stereo recorded with the first leg on channel A, and the rest on channel B.", + UIHint = ActivityPropertyUIHints.Dropdown, + Options = new[] { "single", "dual" } + )] + public string Channels { get; set; } = default!; + + [ActivityProperty( + Hint = "The audio file format used when storing the call recording. Can be either 'mp3' or 'wav'.", + UIHint = ActivityPropertyUIHints.Dropdown, + Options = new[] { "wav", "mp3" } + )] + public string Format { get; set; } = default!; + + [ActivityProperty( + Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", + Category = PropertyCategories.Advanced + )] + public string? ClientState { get; set; } + + [ActivityProperty( + Label = "Command ID", + Hint = "Use this field to avoid duplicate commands. Telnyx will ignore commands with the same Command ID.", + Category = PropertyCategories.Advanced + )] + public string? CommandId { get; set; } + + [ActivityProperty(Hint = "If enabled, a beep sound will be played at the start of a recording.")] + public bool? PlayBeep { get; set; } + + protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) + { + var request = new StartRecordingRequest( + Channels, + Format, + EmptyToNull(ClientState), + EmptyToNull(CommandId), + PlayBeep + ); + + var callControlId = CallControlId; + + try + { + await _telnyxClient.Calls.StartRecordingAsync(callControlId, request, context.CancellationToken); + return Done(); + } + catch (ApiException e) + { + if (await e.CallIsNoLongerActiveAsync()) + return Outcome(TelnyxOutcomeNames.CallIsNoLongerActive); + + throw new WorkflowException(e.Content ?? e.Message, e); + } + } + + private static string? EmptyToNull(string? value) => value is "" ? null : value; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Client/Models/Requests.cs b/src/activities/Elsa.Activities.Telnyx/Client/Models/Requests.cs index 545d88e9c..c84dbb2ec 100644 --- a/src/activities/Elsa.Activities.Telnyx/Client/Models/Requests.cs +++ b/src/activities/Elsa.Activities.Telnyx/Client/Models/Requests.cs @@ -71,5 +71,25 @@ namespace Elsa.Activities.Telnyx.Client.Models string? CommandId, object? Loop, bool Overlay, - string? TargetLegs); + string? TargetLegs + ); + + public record StartRecordingRequest( + string Channels, + string Format, + string? ClientState, + string? CommandId, + bool? PlayBeep + ); + + public record SpeakTextRequest( + string Language, + string Voice, + string Payload, + string? PayloadType, + string? ClientState, + string? CommandId, + string? ServiceLevel, + string? Stop + ); } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs b/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs index e0bad30af..9d21ffdea 100644 --- a/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs +++ b/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs @@ -27,5 +27,11 @@ namespace Elsa.Activities.Telnyx.Client.Services [Post("/v2/calls/{callControlId}/actions/playback_start")] Task PlayAudioAsync(string callControlId, [Body]PlayAudioRequest 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/speak")] + Task SpeakTextAsync(string callControlId, [Body]SpeakTextRequest request, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Liquid/ConfigureLiquidEngine.cs b/src/activities/Elsa.Activities.Telnyx/Liquid/ConfigureLiquidEngine.cs index ab70d48f3..ec5ad1981 100644 --- a/src/activities/Elsa.Activities.Telnyx/Liquid/ConfigureLiquidEngine.cs +++ b/src/activities/Elsa.Activities.Telnyx/Liquid/ConfigureLiquidEngine.cs @@ -28,6 +28,10 @@ namespace Elsa.Activities.Telnyx.Liquid memberAccessStrategy.Register(); memberAccessStrategy.Register(); memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); memberAccessStrategy.Register(); memberAccessStrategy.Register(); diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallRecordingSaved.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallRecordingSaved.cs new file mode 100644 index 000000000..35321ea3d --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallRecordingSaved.cs @@ -0,0 +1,17 @@ +using Elsa.Activities.Telnyx.Webhooks.Attributes; +using NodaTime; + +namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call +{ + [Webhook(EventType, ActivityTypeName, "Call Recording Saved", "Triggered when a recording has been saved.")] + public sealed record CallRecordingSaved : CallPayload + { + public const string EventType = "call.recording.saved"; + public const string ActivityTypeName = "CallRecordingSaved"; + public string Channels { get; set; } + public CallRecordingUrls PublicRecordingUrls { get; set; } + public CallRecordingUrls RecordingUrls { get; set; } + public Instant RecordingEndedAt { get; set; } + public Instant RecordingStartedAt { get; set; } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallRecordingUrls.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallRecordingUrls.cs new file mode 100644 index 000000000..251befcd2 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallRecordingUrls.cs @@ -0,0 +1,4 @@ +namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call +{ + public record CallRecordingUrls(string? Wav, string? Mp3); +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallSpeakEnded.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallSpeakEnded.cs new file mode 100644 index 000000000..d140f0d25 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallSpeakEnded.cs @@ -0,0 +1,11 @@ +using Elsa.Activities.Telnyx.Webhooks.Attributes; + +namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call +{ + [Webhook(EventType, ActivityTypeName, "Call Speak Ended", "Triggered when speaking has ended.")] + public sealed record CallSpeakEnded : CallPayload + { + public const string EventType = "call.speak.ended"; + public const string ActivityTypeName = "CallSpeakEnded"; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallSpeakStarted.cs b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallSpeakStarted.cs new file mode 100644 index 000000000..870791ef0 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Webhooks/Payloads/Call/CallSpeakStarted.cs @@ -0,0 +1,11 @@ +using Elsa.Activities.Telnyx.Webhooks.Attributes; + +namespace Elsa.Activities.Telnyx.Webhooks.Payloads.Call +{ + [Webhook(EventType, ActivityTypeName, "Call Speak Started", "Triggered when speaking has started.")] + public sealed record CallSpeakStarted : CallPayload + { + public const string EventType = "call.speak.started"; + public const string ActivityTypeName = "CallSpeakStarted"; + } +} \ No newline at end of file