diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/SpeakAndWait.cs b/src/activities/Elsa.Activities.Telnyx/Activities/SpeakAndWait.cs new file mode 100644 index 000000000..5ebdadec6 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Activities/SpeakAndWait.cs @@ -0,0 +1,135 @@ +using Elsa.Activities.ControlFlow; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; +using Elsa.Attributes; +using Elsa.Builders; +using Elsa.Design; +using Elsa.Expressions; +using Elsa.Services; + +namespace Elsa.Activities.Telnyx.Activities +{ + [Action( + Category = Constants.Category, + Description = "Convert text to speech and play it back on the call. When done speaking, control is returned to the workflow.", + Outcomes = new[] {OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive}, + DisplayName = "Speak Text And Wait" + )] + public class SpeakAndWait : CompositeActivity + { + [ActivityInput( + Label = "Call Control ID", + Hint = "Unique identifier and token for controlling the call", + Category = PropertyCategories.Advanced, + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} + )] + public string? CallControlId + { + get => GetState(); + set => SetState(value); + } + + [ActivityInput( + Hint = "The language you want spoken.", + UIHint = ActivityInputUIHints.Dropdown, + Options = new[] {"en-US", "en-AU", "nl-NL", "es-ES", "ru-RU"}, + DefaultValue = "en-US", + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid} + )] + public string Language + { + get => GetState(() => "en-US"); + set => SetState(value); + } + + [ActivityInput( + Hint = "The gender of the voice used to speak back the text.", + UIHint = ActivityInputUIHints.Dropdown, + Options = new[] {"female", "male"}, + DefaultValue = "female", + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid} + )] + public string Voice + { + get => GetState(() => "female"); + set => SetState(value); + } + + [ActivityInput(Hint = "The text or SSML to be converted into speech. There is a 5,000 character limit.", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] + public string Payload + { + get => GetState()!; + set => SetState(value); + } + + [ActivityInput( + Hint = "The type of the provided payload. The payload can either be plain text, or Speech Synthesis Markup Language (SSML).", + UIHint = ActivityInputUIHints.Dropdown, + Options = new[] {"", "text", "ssml"}, + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid} + )] + public string? PayloadType + { + get => GetState(); + set => SetState(value); + } + + [ActivityInput( + 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 = ActivityInputUIHints.Dropdown, + Options = new[] {"", "basic", "premium"}, + Category = PropertyCategories.Advanced, + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid} + )] + public string? ServiceLevel + { + get => GetState(); + set => SetState(value); + } + + [ActivityInput( + Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", + Category = PropertyCategories.Advanced, + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} + )] + public string? ClientState + { + get => GetState(); + set => SetState(value); + } + + [ActivityInput( + Label = "Command ID", + Hint = "Use this field to avoid duplicate commands. Telnyx will ignore commands with the same Command ID.", + Category = PropertyCategories.Advanced, + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} + )] + public string? CommandId + { + get => GetState(); + set => SetState(value); + } + + public override void Build(ICompositeActivityBuilder builder) + { + builder + .StartWith(speakText => speakText + .WithLanguage(() => Language) + .WithPayload(() => Payload) + .WithPayloadType(() => PayloadType) + .WithVoice(() => Voice) + .WithClientState(() => ClientState) + .WithCommandId(() => CommandId) + .WithServiceLevel(() => ServiceLevel) + .WithCallControlId(() => CallControlId) + , speakText => + { + speakText + .When(OutcomeNames.Done) + .ThenTypeNamed(nameof(CallSpeakEnded)) + .Finish(OutcomeNames.Done); + + speakText.When(TelnyxOutcomeNames.CallIsNoLongerActive).Finish(TelnyxOutcomeNames.CallIsNoLongerActive); + }); + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs b/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs index 4bea2a094..0d6be61f8 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs @@ -1,9 +1,11 @@ -using System.Threading.Tasks; +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.Builders; using Elsa.Design; using Elsa.Exceptions; using Elsa.Expressions; @@ -36,17 +38,19 @@ namespace Elsa.Activities.Telnyx.Activities Hint = "The language you want spoken.", UIHint = ActivityInputUIHints.Dropdown, Options = new[] { "en-US", "en-AU", "nl-NL", "es-ES", "ru-RU" }, + DefaultValue = "en-US", SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid } )] - public string Language { get; set; } = default!; + public string Language { get; set; } = "en-US"; [ActivityInput( Hint = "The gender of the voice used to speak back the text.", UIHint = ActivityInputUIHints.Dropdown, Options = new[] { "female", "male" }, + DefaultValue = "female", SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid } )] - public string Voice { get; set; } = default!; + public string Voice { get; set; } = "female"; [ActivityInput(Hint = "The text or SSML to be converted into speech. There is a 5,000 character limit.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] public string Payload { get; set; } = default!; @@ -114,4 +118,55 @@ namespace Elsa.Activities.Telnyx.Activities private static string? EmptyToNull(string? value) => value is "" ? null : value; } + + public static class SpeakTextExtensions + { + public static ISetupActivity WithCallControlId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlId(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlId(this ISetupActivity setup, Func value) => setup.Set(x => x.CallControlId, value); + public static ISetupActivity WithCallControlId(this ISetupActivity setup, string? value) => setup.Set(x => x.CallControlId, value); + + public static ISetupActivity WithLanguage(this ISetupActivity setup, Func> value) => setup.Set(x => x.Language, value!); + public static ISetupActivity WithLanguage(this ISetupActivity setup, Func value) => setup.Set(x => x.Language, value); + public static ISetupActivity WithLanguage(this ISetupActivity setup, Func> value) => setup.Set(x => x.Language, value!); + public static ISetupActivity WithLanguage(this ISetupActivity setup, Func value) => setup.Set(x => x.Language, value); + public static ISetupActivity WithLanguage(this ISetupActivity setup, string value) => setup.Set(x => x.Language, value); + + public static ISetupActivity WithVoice(this ISetupActivity setup, Func> value) => setup.Set(x => x.Voice, value!); + public static ISetupActivity WithVoice(this ISetupActivity setup, Func value) => setup.Set(x => x.Voice, value); + public static ISetupActivity WithVoice(this ISetupActivity setup, Func> value) => setup.Set(x => x.Voice, value!); + public static ISetupActivity WithVoice(this ISetupActivity setup, Func value) => setup.Set(x => x.Voice, value); + public static ISetupActivity WithVoice(this ISetupActivity setup, string value) => setup.Set(x => x.Voice, value); + + public static ISetupActivity WithPayload(this ISetupActivity setup, Func> value) => setup.Set(x => x.Payload, value!); + public static ISetupActivity WithPayload(this ISetupActivity setup, Func value) => setup.Set(x => x.Payload, value); + public static ISetupActivity WithPayload(this ISetupActivity setup, Func> value) => setup.Set(x => x.Payload, value!); + public static ISetupActivity WithPayload(this ISetupActivity setup, Func value) => setup.Set(x => x.Payload, value); + public static ISetupActivity WithPayload(this ISetupActivity setup, string value) => setup.Set(x => x.Payload, value); + + public static ISetupActivity WithPayloadType(this ISetupActivity setup, Func> value) => setup.Set(x => x.PayloadType, value); + public static ISetupActivity WithPayloadType(this ISetupActivity setup, Func value) => setup.Set(x => x.PayloadType, value); + public static ISetupActivity WithPayloadType(this ISetupActivity setup, Func> value) => setup.Set(x => x.PayloadType, value); + public static ISetupActivity WithPayloadType(this ISetupActivity setup, Func value) => setup.Set(x => x.PayloadType, value); + public static ISetupActivity WithPayloadType(this ISetupActivity setup, string? value) => setup.Set(x => x.PayloadType, value); + + public static ISetupActivity WithServiceLevel(this ISetupActivity setup, Func> value) => setup.Set(x => x.ServiceLevel, value); + public static ISetupActivity WithServiceLevel(this ISetupActivity setup, Func value) => setup.Set(x => x.ServiceLevel, value); + public static ISetupActivity WithServiceLevel(this ISetupActivity setup, Func> value) => setup.Set(x => x.ServiceLevel, value); + public static ISetupActivity WithServiceLevel(this ISetupActivity setup, Func value) => setup.Set(x => x.ServiceLevel, value); + public static ISetupActivity WithServiceLevel(this ISetupActivity setup, string? value) => setup.Set(x => x.ServiceLevel, value); + + public static ISetupActivity WithClientState(this ISetupActivity setup, Func> value) => setup.Set(x => x.ClientState, value); + public static ISetupActivity WithClientState(this ISetupActivity setup, Func value) => setup.Set(x => x.ClientState, value); + public static ISetupActivity WithClientState(this ISetupActivity setup, Func> value) => setup.Set(x => x.ClientState, value); + public static ISetupActivity WithClientState(this ISetupActivity setup, Func value) => setup.Set(x => x.ClientState, value); + public static ISetupActivity WithClientState(this ISetupActivity setup, string? value) => setup.Set(x => x.ClientState, value); + + public static ISetupActivity WithCommandId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CommandId, value); + public static ISetupActivity WithCommandId(this ISetupActivity setup, Func value) => setup.Set(x => x.CommandId, value); + public static ISetupActivity WithCommandId(this ISetupActivity setup, Func> value) => setup.Set(x => x.CommandId, value); + public static ISetupActivity WithCommandId(this ISetupActivity setup, Func value) => setup.Set(x => x.CommandId, value); + public static ISetupActivity WithCommandId(this ISetupActivity setup, string? value) => setup.Set(x => x.CommandId, value); + } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/StopRecording.cs b/src/activities/Elsa.Activities.Telnyx/Activities/StopRecording.cs new file mode 100644 index 000000000..67b1d0bd6 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Activities/StopRecording.cs @@ -0,0 +1,75 @@ +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.Expressions; +using Elsa.Services; +using Elsa.Services.Models; +using Refit; + +namespace Elsa.Activities.Telnyx.Activities +{ + [Action( + Category = Constants.Category, + Description = "Stop recording the call.", + Outcomes = new[] { OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive }, + DisplayName = "Stop Recording" + )] + public class StopRecording : Activity + { + private readonly ITelnyxClient _telnyxClient; + public StopRecording(ITelnyxClient telnyxClient) => _telnyxClient = telnyxClient; + + [ActivityInput( + Label = "Call Control ID", + Hint = "Unique identifier and token for controlling the call", + Category = PropertyCategories.Advanced, + SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + )] + public string? CallControlId { get; set; } = default!; + + [ActivityInput( + Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", + Category = PropertyCategories.Advanced, + SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + )] + public string? ClientState { get; set; } + + [ActivityInput( + Label = "Command ID", + Hint = "Use this field to avoid duplicate commands. Telnyx will ignore commands with the same Command ID.", + Category = PropertyCategories.Advanced, + SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + )] + public string? CommandId { get; set; } + + protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) + { + var request = new StopRecordingRequest( + EmptyToNull(ClientState), + EmptyToNull(CommandId) + ); + + var callControlId = context.GetCallControlId(CallControlId); + + try + { + await _telnyxClient.Calls.StopRecordingAsync(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/Webhook.cs b/src/activities/Elsa.Activities.Telnyx/Activities/Webhook.cs index 98251d009..a041cba00 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/Webhook.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/Webhook.cs @@ -38,7 +38,7 @@ namespace Elsa.Activities.Telnyx.Activities Model = webhookModel; Payload = webhookModel.Data.Payload; - return Done(); + return Done(webhookModel); } } } \ 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 c84dbb2ec..bb67020da 100644 --- a/src/activities/Elsa.Activities.Telnyx/Client/Models/Requests.cs +++ b/src/activities/Elsa.Activities.Telnyx/Client/Models/Requests.cs @@ -81,7 +81,12 @@ namespace Elsa.Activities.Telnyx.Client.Models string? CommandId, bool? PlayBeep ); - + + public record StopRecordingRequest( + string? ClientState, + string? CommandId + ); + public record SpeakTextRequest( string Language, string Voice, diff --git a/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs b/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs index 9d21ffdea..964dbfcb6 100644 --- a/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs +++ b/src/activities/Elsa.Activities.Telnyx/Client/Services/ICallsApi.cs @@ -31,6 +31,9 @@ namespace Elsa.Activities.Telnyx.Client.Services [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); }