Add Music On Hold feature to Call Ring Group activity

This commit is contained in:
Sipke Schoorstra 2021-06-18 21:19:11 +02:00
parent d1c73d5acd
commit 3d420f2bd8
9 changed files with 245 additions and 17 deletions

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@ -109,6 +110,16 @@ namespace Elsa.Activities.Telnyx.Activities
set => SetState(value);
}
[ActivityInput(
Hint = "The audio file to play while dialing.",
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public Uri? MusicOnHold
{
get => GetState<Uri>();
set => SetState(value);
}
[ActivityInput(
Hint = "Enables Answering Machine Detection.",
UIHint = ActivityInputUIHints.Dropdown,
@ -130,9 +141,22 @@ namespace Elsa.Activities.Telnyx.Activities
set => SetState(value);
}
public override void Build(ICompositeActivityBuilder builder) =>
public override void Build(ICompositeActivityBuilder builder)
{
// Root.
builder
.StartWith<Fork>(fork => fork.WithBranches("Ring", "Queue Timeout"), fork =>
.If(() => MusicOnHold != null, @if =>
{
@if
.When(OutcomeNames.True)
.Then<PlayAudio>(playAudio => playAudio.WithAudioUrl(() => MusicOnHold).WithLoop("infinity"))
.ThenNamed("MainFork");
@if
.When(OutcomeNames.False)
.ThenNamed("MainFork");
})
.Add<Fork>(fork => fork.WithBranches("Ring", "Queue Timeout"), fork =>
{
fork.When("Ring")
.While(true, iterate => iterate
@ -140,16 +164,21 @@ namespace Elsa.Activities.Telnyx.Activities
{
cases.Add(RingGroupStrategy.PrioritizedHunt.ToString(), () => Strategy == RingGroupStrategy.PrioritizedHunt, BuildPrioritizedHuntFlow);
cases.Add(RingGroupStrategy.RingAll.ToString(), () => Strategy == RingGroupStrategy.RingAll, BuildRingAllFlow);
}));
}))
.ThenNamed("ExitWithNoResponse");
fork.When("Queue Timeout")
.IfFalse(() => IsNullOrZero(MaxQueueWaitTime), whenTrue =>
{
whenTrue
.Timer(() => MaxQueueWaitTime!.Value)
.Finish(TelnyxOutcomeNames.NoResponse);
});
});
.IfFalse(() => IsNullOrZero(MaxQueueWaitTime), ifFalse => ifFalse.StartIn(() => MaxQueueWaitTime!.Value).ThenNamed("ExitWithNoResponse"));
}).WithName("MainFork");
// No Response exit node.
builder.Add<If>(@if => @if.WithCondition(() => MusicOnHold != null), @if =>
{
@if.When(OutcomeNames.True).Then<StopAudioPlayback>(stopAudioPlayback => stopAudioPlayback.When(TelnyxOutcomeNames.CallPlaybackEnded).Finish(TelnyxOutcomeNames.NoResponse));
@if.When(OutcomeNames.False).Finish(TelnyxOutcomeNames.NoResponse);
})
.WithName("ExitWithNoResponse");
}
protected override async ValueTask OnExitAsync(ActivityExecutionContext context, object? output)
{
@ -185,13 +214,24 @@ namespace Elsa.Activities.Telnyx.Activities
{
dial
.When(TelnyxOutcomeNames.Answered)
.Then<BridgeCalls>(bridgeCalls =>
.If(() => MusicOnHold != null, @if =>
{
@if.When(OutcomeNames.True)
.Then<StopAudioPlayback>(stopAudioPlayback => stopAudioPlayback
.When(TelnyxOutcomeNames.CallPlaybackEnded)
.ThenNamed("BridgeCalls1"));
@if.When(OutcomeNames.False)
.ThenNamed("BridgeCalls1");
})
.Add<BridgeCalls>(branch: bridgeCalls =>
bridgeCalls.When(TelnyxOutcomeNames.Bridged)
.Finish(activity => activity.WithOutcome(TelnyxOutcomeNames.Connected).WithOutput(context => context.GetInput<BridgedCallsOutput>())));
.Finish(activity => activity.WithOutcome(TelnyxOutcomeNames.Connected).WithOutput(context => context.GetInput<BridgedCallsOutput>()))).WithName("BridgeCalls1");
}
)
)
.IfTrue(() => IsNullOrZero(MaxQueueWaitTime), whenTrue => whenTrue.Finish(TelnyxOutcomeNames.NoResponse));
// If we don't have a queue wait time, then break out of outer loop.
.IfTrue(() => IsNullOrZero(MaxQueueWaitTime), @if => @if.Break());
private void BuildRingAllFlow(IOutcomeBuilder builder) =>
builder
@ -210,7 +250,12 @@ namespace Elsa.Activities.Telnyx.Activities
fork
.When("Timeout")
.StartIn(() => RingTime)
.IfTrue(() => IsNullOrZero(MaxQueueWaitTime), whenTrue => whenTrue.Finish(TelnyxOutcomeNames.NoResponse));
// If we don't have a queue wait time, then break out of outer loop.
.If(@if => @if.WithCondition( () => IsNullOrZero(MaxQueueWaitTime)), @if =>
{
@if.When(OutcomeNames.True).Break();
});
fork
.When("Dial Everyone")

View file

@ -5,6 +5,7 @@ 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;
@ -109,4 +110,37 @@ namespace Elsa.Activities.Telnyx.Activities
private static string? EmptyToNull(string? value) => value is "" ? null : value;
}
public static class PlayAudioExtensions
{
public static ISetupActivity<PlayAudio> WithCallControlId(this ISetupActivity<PlayAudio> setup, Func<ActivityExecutionContext, ValueTask<string?>> value) => setup.Set(x => x.CallControlId, value);
public static ISetupActivity<PlayAudio> WithCallControlId(this ISetupActivity<PlayAudio> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.CallControlId, value);
public static ISetupActivity<PlayAudio> WithCallControlId(this ISetupActivity<PlayAudio> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.CallControlId, value);
public static ISetupActivity<PlayAudio> WithCallControlId(this ISetupActivity<PlayAudio> setup, Func<string?> value) => setup.Set(x => x.CallControlId, value);
public static ISetupActivity<PlayAudio> WithCallControlId(this ISetupActivity<PlayAudio> setup, string? value) => setup.Set(x => x.CallControlId, value);
public static ISetupActivity<PlayAudio> WithAudioUrl(this ISetupActivity<PlayAudio> setup, Func<ActivityExecutionContext, ValueTask<Uri?>> value) => setup.Set(x => x.AudioUrl, value);
public static ISetupActivity<PlayAudio> WithAudioUrl(this ISetupActivity<PlayAudio> setup, Func<ActivityExecutionContext, Uri?> value) => setup.Set(x => x.AudioUrl, value);
public static ISetupActivity<PlayAudio> WithAudioUrl(this ISetupActivity<PlayAudio> setup, Func<ValueTask<Uri?>> value) => setup.Set(x => x.AudioUrl, value);
public static ISetupActivity<PlayAudio> WithAudioUrl(this ISetupActivity<PlayAudio> setup, Func<Uri?> value) => setup.Set(x => x.AudioUrl, value);
public static ISetupActivity<PlayAudio> WithAudioUrl(this ISetupActivity<PlayAudio> setup, Uri? value) => setup.Set(x => x.AudioUrl, value);
public static ISetupActivity<PlayAudio> WithLoop(this ISetupActivity<PlayAudio> setup, Func<ActivityExecutionContext, ValueTask<string?>> value) => setup.Set(x => x.Loop, value);
public static ISetupActivity<PlayAudio> WithLoop(this ISetupActivity<PlayAudio> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.Loop, value);
public static ISetupActivity<PlayAudio> WithLoop(this ISetupActivity<PlayAudio> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.Loop, value);
public static ISetupActivity<PlayAudio> WithLoop(this ISetupActivity<PlayAudio> setup, Func<string?> value) => setup.Set(x => x.Loop, value);
public static ISetupActivity<PlayAudio> WithLoop(this ISetupActivity<PlayAudio> setup, string? value) => setup.Set(x => x.Loop, value);
public static ISetupActivity<PlayAudio> WithOverlay(this ISetupActivity<PlayAudio> setup, Func<ActivityExecutionContext, ValueTask<bool?>> value) => setup.Set(x => x.Overlay, value);
public static ISetupActivity<PlayAudio> WithOverlay(this ISetupActivity<PlayAudio> setup, Func<ActivityExecutionContext, bool?> value) => setup.Set(x => x.Overlay, value);
public static ISetupActivity<PlayAudio> WithOverlay(this ISetupActivity<PlayAudio> setup, Func<ValueTask<bool?>> value) => setup.Set(x => x.Overlay, value);
public static ISetupActivity<PlayAudio> WithOverlay(this ISetupActivity<PlayAudio> setup, Func<bool?> value) => setup.Set(x => x.Overlay, value);
public static ISetupActivity<PlayAudio> WithOverlay(this ISetupActivity<PlayAudio> setup, bool? value) => setup.Set(x => x.Overlay, value);
public static ISetupActivity<PlayAudio> WithTargetLegs(this ISetupActivity<PlayAudio> setup, Func<ActivityExecutionContext, ValueTask<string?>> value) => setup.Set(x => x.TargetLegs, value);
public static ISetupActivity<PlayAudio> WithTargetLegs(this ISetupActivity<PlayAudio> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.TargetLegs, value);
public static ISetupActivity<PlayAudio> WithTargetLegs(this ISetupActivity<PlayAudio> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.TargetLegs, value);
public static ISetupActivity<PlayAudio> WithTargetLegs(this ISetupActivity<PlayAudio> setup, Func<string?> value) => setup.Set(x => x.TargetLegs, value);
public static ISetupActivity<PlayAudio> WithTargetLegs(this ISetupActivity<PlayAudio> setup, string? value) => setup.Set(x => x.TargetLegs, value);
}
}

View file

@ -0,0 +1,112 @@
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.Activities.Telnyx.Webhooks.Payloads.Call;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Builders;
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 = "Play an audio file on the call.",
Outcomes = new[] { TelnyxOutcomeNames.CallPlaybackEnding, TelnyxOutcomeNames.CallIsNoLongerActive, TelnyxOutcomeNames.CallPlaybackEnded },
DisplayName = "Play Audio"
)]
public class StopAudioPlayback : Activity
{
private readonly ITelnyxClient _telnyxClient;
public StopAudioPlayback(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; }
[ActivityInput(
Hint = "Use 'current' to stop only the current audio or 'all' to stop all audios in the queue.",
DefaultValue = "all",
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string? Stop { get; set; } = "all";
[ActivityOutput(Hint = "The received payload when speaking ended.")]
public CallPlaybackEndedPayload? PlaybackEndedPayload { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
var request = new StopAudioPlaybackRequest(
EmptyToNull(ClientState),
EmptyToNull(CommandId),
Stop
);
var callControlId = context.GetCallControlId(CallControlId);
try
{
await _telnyxClient.Calls.StopAudioPlaybackAsync(callControlId, request, context.CancellationToken);
return Combine(Outcome(TelnyxOutcomeNames.CallPlaybackEnding), Suspend());
}
catch (ApiException e)
{
if (await e.CallIsNoLongerActiveAsync())
return Outcome(TelnyxOutcomeNames.CallIsNoLongerActive);
throw new WorkflowException(e.Content ?? e.Message, e);
}
}
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
PlaybackEndedPayload = context.GetInput<CallPlaybackEndedPayload>();
return Outcome(TelnyxOutcomeNames.CallPlaybackEnded);
}
private static string? EmptyToNull(string? value) => value is "" ? null : value;
}
public static class StopAudioPlaybackExtensions
{
public static ISetupActivity<StopAudioPlayback> WithCallControlId(this ISetupActivity<StopAudioPlayback> setup, Func<ActivityExecutionContext, ValueTask<string?>> value) => setup.Set(x => x.CallControlId, value);
public static ISetupActivity<StopAudioPlayback> WithCallControlId(this ISetupActivity<StopAudioPlayback> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.CallControlId, value);
public static ISetupActivity<StopAudioPlayback> WithCallControlId(this ISetupActivity<StopAudioPlayback> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.CallControlId, value);
public static ISetupActivity<StopAudioPlayback> WithCallControlId(this ISetupActivity<StopAudioPlayback> setup, Func<string?> value) => setup.Set(x => x.CallControlId, value);
public static ISetupActivity<StopAudioPlayback> WithCallControlId(this ISetupActivity<StopAudioPlayback> setup, string? value) => setup.Set(x => x.CallControlId, value);
public static ISetupActivity<StopAudioPlayback> WithStop(this ISetupActivity<StopAudioPlayback> setup, Func<ActivityExecutionContext, ValueTask<string?>> value) => setup.Set(x => x.Stop, value);
public static ISetupActivity<StopAudioPlayback> WithStop(this ISetupActivity<StopAudioPlayback> setup, Func<ActivityExecutionContext, string?> value) => setup.Set(x => x.Stop, value);
public static ISetupActivity<StopAudioPlayback> WithStop(this ISetupActivity<StopAudioPlayback> setup, Func<ValueTask<string?>> value) => setup.Set(x => x.Stop, value);
public static ISetupActivity<StopAudioPlayback> WithStop(this ISetupActivity<StopAudioPlayback> setup, Func<string?> value) => setup.Set(x => x.Stop, value);
public static ISetupActivity<StopAudioPlayback> WithStop(this ISetupActivity<StopAudioPlayback> setup, string? value) => setup.Set(x => x.Stop, value);
}
}

View file

@ -90,6 +90,12 @@ namespace Elsa.Activities.Telnyx.Client.Models
bool Overlay,
string? TargetLegs
);
public record StopAudioPlaybackRequest(
string? ClientState,
string? CommandId,
string? Stop
);
public record StartRecordingRequest(
string Channels,

View file

@ -31,6 +31,9 @@ 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/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);

View file

@ -0,0 +1,13 @@
using Elsa.Activities.Telnyx.Activities;
using Elsa.Activities.Telnyx.Webhooks.Payloads.Call;
using Elsa.Services;
namespace Elsa.Activities.Telnyx.Handlers
{
public class ResumeStopAudioPlayback : ResumeWebhookDrivenActivity<StopAudioPlayback, CallPlaybackEndedPayload>
{
public ResumeStopAudioPlayback(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad)
{
}
}
}

View file

@ -13,7 +13,7 @@ using MediatR;
namespace Elsa.Activities.Telnyx.Handlers
{
public abstract class ResumeWebhookDrivenActivity<TActivity, TPayload> : ResumeWebhookDrivenActivity<TActivity> where TPayload : CallPayload
public abstract class ResumeWebhookDrivenActivity<TActivity, TPayload> : ResumeWebhookDrivenActivity<TActivity> where TPayload : CallPayload where TActivity : IActivity
{
protected ResumeWebhookDrivenActivity(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad)
{
@ -22,7 +22,7 @@ namespace Elsa.Activities.Telnyx.Handlers
protected override IEnumerable<Type> GetSupportedPayloadTypes() => new[] {typeof(TPayload)};
}
public abstract class ResumeWebhookDrivenActivity<TActivity> : INotificationHandler<TelnyxWebhookReceived>
public abstract class ResumeWebhookDrivenActivity<TActivity> : INotificationHandler<TelnyxWebhookReceived> where TActivity : IActivity
{
private readonly IWorkflowLaunchpad _workflowLaunchpad;
protected ResumeWebhookDrivenActivity(IWorkflowLaunchpad workflowLaunchpad) => _workflowLaunchpad = workflowLaunchpad;

View file

@ -23,5 +23,7 @@
public const string Speaking = "Speaking";
public const string Recording = "Recording";
public const string FinishedRecording = "Finished Recording";
public const string CallPlaybackEnding = "Playback Ending";
public const string CallPlaybackEnded = "Playback Ended";
}
}

View file

@ -0,0 +1,13 @@
using Elsa.Activities.Telnyx.Activities;
using Elsa.Services.Bookmarks;
namespace Elsa.Activities.Telnyx.Providers.Bookmarks
{
public class StopAudioPlaybackBookmark : IBookmark
{
}
public class StopAudioPlaybackBookmarkProvider : DefaultBookmarkProvider<StopAudioPlaybackBookmark, StopAudioPlayback>
{
}
}