Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<PackageVersion Include="FluentAssertions" Version="[7.1.0]" />
<PackageVersion Include="Fody" Version="6.9.1" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="Humanizer.Core" Version="2.14.1"/>
<PackageVersion Include="JetBrains.Annotations" Version="2024.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.10.0" />
Expand All @@ -30,6 +31,8 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Refit" Version="8.0.0"/>
<PackageVersion Include="Refit.HttpClientFactory" Version="8.0.0"/>
<PackageVersion Include="SlackNet" Version="0.15.5" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
Expand Down
7 changes: 7 additions & 0 deletions Elsa.Integrations.sln
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A99FA26E-2
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Slack.Tests", "test\Elsa.Integrations.Slack.Tests\Elsa.Integrations.Slack.Tests.csproj", "{861E1230-F9CB-450C-845E-04DFDA259E26}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.Telnyx", "src\Elsa.Integrations.Telnyx\Elsa.Integrations.Telnyx.csproj", "{128B2FC3-81A7-4327-9665-9155B05F21DA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -46,6 +48,10 @@ Global
{861E1230-F9CB-450C-845E-04DFDA259E26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{861E1230-F9CB-450C-845E-04DFDA259E26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{861E1230-F9CB-450C-845E-04DFDA259E26}.Release|Any CPU.Build.0 = Release|Any CPU
{128B2FC3-81A7-4327-9665-9155B05F21DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{128B2FC3-81A7-4327-9665-9155B05F21DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{128B2FC3-81A7-4327-9665-9155B05F21DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{128B2FC3-81A7-4327-9665-9155B05F21DA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -54,5 +60,6 @@ Global
{A8666FC4-66E1-4766-A22B-C410D42D03DD} = {527248D6-B851-4C8D-8667-E2FB0A91DABF}
{9732E404-11B5-48DB-B5D9-97997F018830} = {527248D6-B851-4C8D-8667-E2FB0A91DABF}
{861E1230-F9CB-450C-845E-04DFDA259E26} = {A99FA26E-2098-403A-BD04-6BBCFBE3AC7D}
{128B2FC3-81A7-4327-9665-9155B05F21DA} = {527248D6-B851-4C8D-8667-E2FB0A91DABF}
EndGlobalSection
EndGlobal
2 changes: 2 additions & 0 deletions Elsa.Integrations.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=telnyx/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
30 changes: 30 additions & 0 deletions src/Elsa.Integrations.Telnyx/Activities/AnswerCall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;

namespace Elsa.Integrations.Telnyx.Activities;

/// <inheritdoc />
public class AnswerCall : AnswerCallBase
{
/// <inheritdoc />
public AnswerCall([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : 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);
}
71 changes: 71 additions & 0 deletions src/Elsa.Integrations.Telnyx/Activities/AnswerCallBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using Elsa.Extensions;
using Elsa.Integrations.Telnyx.Bookmarks;
using Elsa.Integrations.Telnyx.Client.Models;
using Elsa.Integrations.Telnyx.Client.Services;
using Elsa.Integrations.Telnyx.Extensions;
using Elsa.Integrations.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Refit;

namespace Elsa.Integrations.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 = null, int? line = null) : 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; } = null!;

/// <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);
}
}
32 changes: 32 additions & 0 deletions src/Elsa.Integrations.Telnyx/Activities/BridgeCalls.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using JetBrains.Annotations;

namespace Elsa.Integrations.Telnyx.Activities;

/// <inheritdoc />
[PublicAPI]
public class BridgeCalls : BridgeCallsBase
{
/// <inheritdoc />
public BridgeCalls([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : 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);
}
99 changes: 99 additions & 0 deletions src/Elsa.Integrations.Telnyx/Activities/BridgeCallsBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using Elsa.Extensions;
using Elsa.Integrations.Telnyx.Bookmarks;
using Elsa.Integrations.Telnyx.Client.Models;
using Elsa.Integrations.Telnyx.Client.Services;
using Elsa.Integrations.Telnyx.Extensions;
using Elsa.Integrations.Telnyx.Models;
using Elsa.Integrations.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using JetBrains.Annotations;
using Refit;

namespace Elsa.Integrations.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 = null, int? line = null) : 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; } = null!;

/// <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; } = null!;

/// <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([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(callBridgedPayloadA, callBridgedPayloadB));
await HandleBridgedAsync(context);
}
}
}
54 changes: 54 additions & 0 deletions src/Elsa.Integrations.Telnyx/Activities/CallAnswered.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Integrations.Telnyx.Bookmarks;
using Elsa.Integrations.Telnyx.Helpers;
using Elsa.Integrations.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Elsa.Workflows.UIHints;

namespace Elsa.Integrations.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 = null, [CallerLineNumber] int? line = null) : 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; } = null!;

/// <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()
{
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();
}
}
54 changes: 54 additions & 0 deletions src/Elsa.Integrations.Telnyx/Activities/CallHangup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Integrations.Telnyx.Bookmarks;
using Elsa.Integrations.Telnyx.Helpers;
using Elsa.Integrations.Telnyx.Payloads.Call;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Elsa.Workflows.UIHints;

namespace Elsa.Integrations.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 = null, [CallerLineNumber] int? line = null) : 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; } = null!;

/// <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()
{
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();
}
}
Loading