Fix loss of source file + line number (#4293)

Fixes #4287
This commit is contained in:
Sipke Schoorstra 2023-08-06 20:55:24 +02:00 committed by GitHub
parent c9ca4a121d
commit 166e11c416
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
153 changed files with 1350 additions and 1575 deletions

View file

@ -0,0 +1,24 @@
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Contracts;
using Timer = Elsa.Scheduling.Activities.Timer;
namespace Elsa.WorkflowServer.Web;
public class HeartbeatWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
builder.Root = new Sequence
{
Activities =
{
new Timer(TimeSpan.FromMinutes(5))
{
CanStartWorkflow = true
},
new WriteLine(context => $"Heartbeat workflow triggered at {DateTime.Now}")
}
};
}
}

View file

@ -32,6 +32,7 @@ services
elsa
.AddActivitiesFrom<Program>()
.AddWorkflowsFrom<Program>()
.UseFluentStorageProvider()
.AddTypeAlias<ApiResponse<User>>("ApiResponse[User]")
.UseIdentity(identity =>

View file

@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Runtime.CompilerServices;
using Elsa.AzureServiceBus.Models;
using Elsa.Common.Contracts;
using Elsa.Expressions.Models;
@ -18,8 +18,7 @@ public class MessageReceived : Trigger
internal const string InputKey = "TransportMessage";
/// <inheritdoc />
[JsonConstructor]
public MessageReceived()
public MessageReceived([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}

View file

@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using Azure.Messaging.ServiceBus;
using Elsa.Common.Contracts;
using Elsa.Common.Services;
@ -16,6 +17,11 @@ namespace Elsa.AzureServiceBus.Activities;
[PublicAPI]
public class SendMessage : CodeActivity
{
/// <inheritdoc />
public SendMessage([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The contents of the message to send.
/// </summary>

View file

@ -1,5 +1,3 @@
using Elsa.Common.Entities;
namespace Elsa.Dapper.Modules.Runtime.Records;
/// <summary>

View file

@ -2,7 +2,6 @@ using System.Collections;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Email.Contracts;
using Elsa.Email.Models;
using Elsa.Email.Options;
@ -27,12 +26,6 @@ public class SendEmail : Activity
{
}
/// <inheritdoc />
[JsonConstructor]
public SendEmail()
{
}
/// <summary>
/// The sender's email address.
/// </summary>

View file

@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View file

@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View file

@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View file

@ -2,7 +2,6 @@ using System.Text.Json;
using Elsa.EntityFrameworkCore.Common;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using Elsa.Workflows.Runtime.Models;

View file

@ -0,0 +1,36 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
namespace Elsa.Http;
/// <summary>
/// Send an HTTP request.
/// </summary>
[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request (flow)", Kind = ActivityKind.Task)]
public class FlowSendHttpRequest : SendHttpRequestBase
{
/// <inheritdoc />
public FlowSendHttpRequest([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// A list of expected status codes to handle.
/// </summary>
[Input(Description = "A list of expected status codes to handle.", UIHint = InputUIHints.MultiText)]
public Input<ICollection<int>> ExpectedStatusCodes { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response)
{
var expectedStatusCodes = ExpectedStatusCodes.GetOrDefault(context) ?? new List<int>(0);
var statusCode = (int)response.StatusCode;
var hasMatchingStatusCode = expectedStatusCodes.Contains(statusCode);
var outcome = expectedStatusCodes.Any() ? hasMatchingStatusCode ? statusCode.ToString() : "Unmatched status code" : "Done";
await context.CompleteActivityWithOutcomesAsync(outcome);
}
}

View file

@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Runtime.CompilerServices;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Http.Contracts;
@ -23,8 +23,7 @@ public class HttpEndpoint : Trigger<HttpRequest>
internal const string RequestPathInputKey = "RequestPath";
/// <inheritdoc />
[JsonConstructor]
public HttpEndpoint()
public HttpEndpoint([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}

View file

@ -1,41 +1,13 @@
using System.Net.Http.Headers;
using System.Text.Json.Serialization;
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Http.ContentWriters;
using Elsa.Http.Models;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
using HttpRequestHeaders = Elsa.Http.Models.HttpRequestHeaders;
namespace Elsa.Http;
/// <summary>
/// Send an HTTP request.
/// </summary>
[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request (flow)", Kind = ActivityKind.Task)]
[PublicAPI]
public class FlowSendHttpRequest : SendHttpRequestBase
{
/// <summary>
/// A list of expected status codes to handle.
/// </summary>
[Input(Description = "A list of expected status codes to handle.", UIHint = InputUIHints.MultiText)]
public Input<ICollection<int>> ExpectedStatusCodes { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response)
{
var expectedStatusCodes = ExpectedStatusCodes.GetOrDefault(context) ?? new List<int>(0);
var statusCode = (int)response.StatusCode;
var hasMatchingStatusCode = expectedStatusCodes.Contains(statusCode);
var outcome = expectedStatusCodes.Any() ? hasMatchingStatusCode ? statusCode.ToString() : "Unmatched status code" : "Done";
await context.CompleteActivityWithOutcomesAsync(outcome);
}
}
/// <summary>
/// Send an HTTP request.
/// </summary>
@ -43,6 +15,11 @@ public class FlowSendHttpRequest : SendHttpRequestBase
[PublicAPI]
public class SendHttpRequest : SendHttpRequestBase
{
/// <inheritdoc />
public SendHttpRequest([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// A list of expected status codes to handle and the corresponding activity to execute when the status code matches.
/// </summary>
@ -73,182 +50,4 @@ public class SendHttpRequest : SendHttpRequestBase
{
await context.CompleteActivityAsync();
}
}
/// <summary>
/// A binding between an HTTP status code and an activity.
/// </summary>
public class HttpStatusCodeCase
{
/// <summary>
/// Creates a new instance of the <see cref="HttpStatusCodeCase"/> class.
/// </summary>
[JsonConstructor]
public HttpStatusCodeCase()
{
}
/// <summary>
/// Creates a new instance of the <see cref="HttpStatusCodeCase"/> class.
/// </summary>
public HttpStatusCodeCase(int statusCode, IActivity activity)
{
StatusCode = statusCode;
Activity = activity;
}
/// <summary>
/// The HTTP status code to match.
/// </summary>
public int StatusCode { get; set; }
/// <summary>
/// The activity to execute when the HTTP status code matches.
/// </summary>
public IActivity? Activity { get; set; }
}
/// <summary>
/// Base class for activities that send HTTP requests.
/// </summary>
public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
{
/// <summary>
/// The URL to send the request to.
/// </summary>
[Input]
public Input<Uri?> Url { get; set; } = default!;
/// <summary>
/// The HTTP method to use when sending the request.
/// </summary>
[Input(
Description = "The HTTP method to use when sending the request.",
Options = new[] { "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD" },
DefaultValue = "GET",
UIHint = InputUIHints.Dropdown
)]
public Input<string> Method { get; set; } = new("GET");
/// <summary>
/// The content to send with the request. Can be a string, an object, a byte array or a stream.
/// </summary>
[Input(Description = "The content to send with the request. Can be a string, an object, a byte array or a stream.")]
public Input<object?> Content { get; set; } = default!;
/// <summary>
/// The content type to use when sending the request.
/// </summary>
[Input(
Description = "The content type to use when sending the request.",
Options = new[] { "", "text/plain", "text/html", "application/json", "application/xml", "application/x-www-form-urlencoded" },
UIHint = InputUIHints.Dropdown
)]
public Input<string?> ContentType { get; set; } = default!;
/// <summary>
/// The Authorization header value to send with the request.
/// </summary>
/// <example>Bearer {some-access-token}</example>
[Input(
Description = "The Authorization header value to send with the request. For example: Bearer {some-access-token}",
Category = "Security"
)]
public Input<string?> Authorization { get; set; } = default!;
/// <summary>
/// The headers to send along with the request.
/// </summary>
[Input(Description = "The headers to send along with the request.", Category = "Advanced")]
public Input<HttpRequestHeaders?> RequestHeaders { get; set; } = new(new HttpRequestHeaders());
/// <summary>
/// The parsed content, if any.
/// </summary>
[Output(Description = "The parsed content, if any.")]
public Output<object?> ParsedContent { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
await TrySendAsync(context);
}
/// <summary>
/// Handles the response.
/// </summary>
protected abstract ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response);
private async Task TrySendAsync(ActivityExecutionContext context)
{
var request = PrepareRequest(context);
var httpClientFactory = context.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase));
var cancellationToken = context.CancellationToken;
try
{
var response = await httpClient.SendAsync(request, cancellationToken);
var parsedContent = await ParseContentAsync(context, response.Content);
context.Set(Result, response);
context.Set(ParsedContent, parsedContent);
await HandleResponseAsync(context, response);
}
catch (TaskCanceledException e)
{
context.JournalData.Add("Cancelled", true);
}
}
private async Task<object?> ParseContentAsync(ActivityExecutionContext context, HttpContent httpContent)
{
if (!HasContent(httpContent))
return null;
var cancellationToken = context.CancellationToken;
var targetType = ParsedContent.GetTargetType(context);
var contentStream = await httpContent.ReadAsStreamAsync(cancellationToken);
var contentType = httpContent.Headers.ContentType?.MediaType!;
targetType ??= contentType switch
{
"application/json" => typeof(object),
_ => typeof(string)
};
return await context.ParseContentAsync(contentStream, contentType, targetType, cancellationToken);
}
private static bool HasContent(HttpContent httpContent) => httpContent.Headers.ContentLength > 0;
private HttpRequestMessage PrepareRequest(ActivityExecutionContext context)
{
var method = Method.GetOrDefault(context) ?? "GET";
var url = Url.Get(context);
var request = new HttpRequestMessage(new HttpMethod(method), url);
var headers = context.GetHeaders(RequestHeaders);
var authorization = Authorization.GetOrDefault(context);
if (!string.IsNullOrWhiteSpace(authorization))
request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization);
foreach (var header in headers)
request.Headers.Add(header.Key, header.Value.AsEnumerable());
var contentType = ContentType.GetOrDefault(context);
var content = Content.GetOrDefault(context);
if (contentType != null && content != null)
{
var contentWriters = context.GetServices<IHttpContentFactory>();
var contentWriter = SelectContentWriter(contentType, contentWriters);
request.Content = contentWriter.CreateHttpContent(content, contentType);
}
return request;
}
private IHttpContentFactory SelectContentWriter(string? contentType, IEnumerable<IHttpContentFactory> requestContentWriters) =>
string.IsNullOrWhiteSpace(contentType) ? new JsonContentFactory() : requestContentWriters.First(w => w.SupportsContentType(contentType));
}

View file

@ -0,0 +1,159 @@
using System.Net.Http.Headers;
using Elsa.Extensions;
using Elsa.Http.ContentWriters;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using HttpRequestHeaders = Elsa.Http.Models.HttpRequestHeaders;
namespace Elsa.Http;
/// <summary>
/// Base class for activities that send HTTP requests.
/// </summary>
public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
{
/// <inheritdoc />
protected SendHttpRequestBase(string? source = default, int? line = default) : base(source, line)
{
}
/// <summary>
/// The URL to send the request to.
/// </summary>
[Input]
public Input<Uri?> Url { get; set; } = default!;
/// <summary>
/// The HTTP method to use when sending the request.
/// </summary>
[Input(
Description = "The HTTP method to use when sending the request.",
Options = new[] { "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD" },
DefaultValue = "GET",
UIHint = InputUIHints.Dropdown
)]
public Input<string> Method { get; set; } = new("GET");
/// <summary>
/// The content to send with the request. Can be a string, an object, a byte array or a stream.
/// </summary>
[Input(Description = "The content to send with the request. Can be a string, an object, a byte array or a stream.")]
public Input<object?> Content { get; set; } = default!;
/// <summary>
/// The content type to use when sending the request.
/// </summary>
[Input(
Description = "The content type to use when sending the request.",
Options = new[] { "", "text/plain", "text/html", "application/json", "application/xml", "application/x-www-form-urlencoded" },
UIHint = InputUIHints.Dropdown
)]
public Input<string?> ContentType { get; set; } = default!;
/// <summary>
/// The Authorization header value to send with the request.
/// </summary>
/// <example>Bearer {some-access-token}</example>
[Input(
Description = "The Authorization header value to send with the request. For example: Bearer {some-access-token}",
Category = "Security"
)]
public Input<string?> Authorization { get; set; } = default!;
/// <summary>
/// The headers to send along with the request.
/// </summary>
[Input(Description = "The headers to send along with the request.", Category = "Advanced")]
public Input<HttpRequestHeaders?> RequestHeaders { get; set; } = new(new HttpRequestHeaders());
/// <summary>
/// The parsed content, if any.
/// </summary>
[Output(Description = "The parsed content, if any.")]
public Output<object?> ParsedContent { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
await TrySendAsync(context);
}
/// <summary>
/// Handles the response.
/// </summary>
protected abstract ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response);
private async Task TrySendAsync(ActivityExecutionContext context)
{
var request = PrepareRequest(context);
var httpClientFactory = context.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase));
var cancellationToken = context.CancellationToken;
try
{
var response = await httpClient.SendAsync(request, cancellationToken);
var parsedContent = await ParseContentAsync(context, response.Content);
context.Set(Result, response);
context.Set(ParsedContent, parsedContent);
await HandleResponseAsync(context, response);
}
catch (TaskCanceledException e)
{
context.JournalData.Add("Cancelled", true);
}
}
private async Task<object?> ParseContentAsync(ActivityExecutionContext context, HttpContent httpContent)
{
if (!HasContent(httpContent))
return null;
var cancellationToken = context.CancellationToken;
var targetType = ParsedContent.GetTargetType(context);
var contentStream = await httpContent.ReadAsStreamAsync(cancellationToken);
var contentType = httpContent.Headers.ContentType?.MediaType!;
targetType ??= contentType switch
{
"application/json" => typeof(object),
_ => typeof(string)
};
return await context.ParseContentAsync(contentStream, contentType, targetType, cancellationToken);
}
private static bool HasContent(HttpContent httpContent) => httpContent.Headers.ContentLength > 0;
private HttpRequestMessage PrepareRequest(ActivityExecutionContext context)
{
var method = Method.GetOrDefault(context) ?? "GET";
var url = Url.Get(context);
var request = new HttpRequestMessage(new HttpMethod(method), url);
var headers = context.GetHeaders(RequestHeaders);
var authorization = Authorization.GetOrDefault(context);
if (!string.IsNullOrWhiteSpace(authorization))
request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization);
foreach (var header in headers)
request.Headers.Add(header.Key, header.Value.AsEnumerable());
var contentType = ContentType.GetOrDefault(context);
var content = Content.GetOrDefault(context);
if (contentType != null && content != null)
{
var contentWriters = context.GetServices<IHttpContentFactory>();
var contentWriter = SelectContentWriter(contentType, contentWriters);
request.Content = contentWriter.CreateHttpContent(content, contentType);
}
return request;
}
private IHttpContentFactory SelectContentWriter(string? contentType, IEnumerable<IHttpContentFactory> requestContentWriters) =>
string.IsNullOrWhiteSpace(contentType) ? new JsonContentFactory() : requestContentWriters.First(w => w.SupportsContentType(contentType));
}

View file

@ -1,4 +1,5 @@
using System.Net;
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Http.ContentWriters;
using Elsa.Http.Models;
@ -16,6 +17,11 @@ namespace Elsa.Http;
[Activity("Elsa", "HTTP", "Write a response to the current HTTP response object.", DisplayName = "HTTP Response")]
public class WriteHttpResponse : Activity
{
/// <inheritdoc />
public WriteHttpResponse([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The status code to return.
/// </summary>

View file

@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using Elsa.Workflows.Core.Contracts;
namespace Elsa.Http.Models;
/// <summary>
/// A binding between an HTTP status code and an activity.
/// </summary>
public class HttpStatusCodeCase
{
/// <summary>
/// Creates a new instance of the <see cref="HttpStatusCodeCase"/> class.
/// </summary>
[JsonConstructor]
public HttpStatusCodeCase()
{
}
/// <summary>
/// Creates a new instance of the <see cref="HttpStatusCodeCase"/> class.
/// </summary>
public HttpStatusCodeCase(int statusCode, IActivity activity)
{
StatusCode = statusCode;
Activity = activity;
}
/// <summary>
/// The HTTP status code to match.
/// </summary>
public int StatusCode { get; set; }
/// <summary>
/// The activity to execute when the HTTP status code matches.
/// </summary>
public IActivity? Activity { get; set; }
}

View file

@ -1,5 +1,5 @@
using System.ComponentModel;
using System.Text.Json.Serialization;
using System.Runtime.CompilerServices;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.MassTransit.Implementations;
@ -16,8 +16,7 @@ public class MessageReceived : Trigger<object>
internal const string InputKey = "Message";
/// <inheritdoc />
[JsonConstructor]
public MessageReceived()
public MessageReceived([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}

View file

@ -1,5 +1,5 @@
using System.ComponentModel;
using System.Text.Json.Serialization;
using System.Runtime.CompilerServices;
using Elsa.Expressions.Helpers;
using Elsa.Extensions;
using Elsa.MassTransit.Implementations;
@ -17,8 +17,7 @@ namespace Elsa.MassTransit.Activities;
public class PublishMessage : CodeActivity
{
/// <inheritdoc />
[JsonConstructor]
public PublishMessage()
public PublishMessage([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}

View file

@ -8,7 +8,6 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.IdGenerators;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
using MongoDB.Driver.Core.Extensions.DiagnosticSources;

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Scheduling.Bookmarks;
using Elsa.Scheduling.Contracts;
@ -15,13 +14,6 @@ namespace Elsa.Scheduling.Activities;
[Activity("Elsa", "Scheduling", "Trigger workflow execution at a specific interval using a CRON expression.")]
public class Cron : EventGenerator
{
/// <inheritdoc />
[JsonConstructor]
public Cron()
{
}
/// <inheritdoc />
public Cron([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,8 +1,5 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Elsa.Common.Contracts;
using Elsa.Expressions.Models;
using Elsa.Extensions;
@ -21,12 +18,6 @@ namespace Elsa.Scheduling.Activities;
[Activity( "Elsa", "Scheduling", "Delay execution for the specified amount of time.")]
public class Delay : Activity, IActivityPropertyDefaultValueProvider
{
/// <inheritdoc />
[JsonConstructor]
internal Delay()
{
}
/// <inheritdoc />
public Delay([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Common.Contracts;
using Elsa.Expressions.Models;
using Elsa.Extensions;
@ -20,12 +19,6 @@ public class StartAt : Trigger
{
private const string InputKey = "ExecuteAt";
/// <inheritdoc />
[JsonConstructor]
public StartAt()
{
}
/// <inheritdoc />
public StartAt([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Common.Contracts;
using Elsa.Extensions;
using Elsa.Scheduling.Bookmarks;
@ -15,12 +14,6 @@ namespace Elsa.Scheduling.Activities;
[Activity("Elsa", "Scheduling", "Trigger workflow execution at a specific interval.")]
public class Timer : EventGenerator
{
/// <inheritdoc />
[JsonConstructor]
public Timer()
{
}
/// <inheritdoc />
public Timer([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using Elsa.Common.Contracts;
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Scheduling.Commands;
using Elsa.Scheduling.Contracts;

View file

@ -1,5 +1,4 @@
using Elsa.Common.Contracts;
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Scheduling.Commands;
using Elsa.Scheduling.Contracts;

View file

@ -1,5 +1,4 @@
using Elsa.Common.Contracts;
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Scheduling.Commands;
using Elsa.Scheduling.Contracts;

View file

@ -1,57 +1,13 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
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.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Activities.Flowchart.Models;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Runtime.Contracts;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Connected", "Disconnected")]
[PublicAPI]
public class FlowAnswerCall : AnswerCallBase
{
/// <inheritdoc />
[JsonConstructor]
public FlowAnswerCall()
{
}
/// <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"));
}
/// <inheritdoc />
[PublicAPI]
public class AnswerCall : AnswerCallBase
{
/// <inheritdoc />
[JsonConstructor]
public AnswerCall()
{
}
/// <inheritdoc />
public AnswerCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
@ -74,80 +30,4 @@ public class AnswerCall : AnswerCallBase
/// <inheritdoc />
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected);
}
/// <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)]
[WebhookDriven(WebhookEventTypes.CallAnswered)]
[PublicAPI]
public abstract class AnswerCallBase : Activity<CallAnsweredPayload>, IBookmarksPersistedHandler
{
/// <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; }
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
// Create a bookmark first, then after it's persisted, we call out to Telnyx.
// This ensures that the bookmark is available in case Telnyx responds with the webhook before the runtime got a chance to persist bookmarks.
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
context.CreateBookmark(new WebhookEventBookmarkPayload(WebhookEventTypes.CallAnswered, callControlId), ResumeAsync);
}
/// <summary>
/// Invokes Telnyx to answer the call.
/// </summary>
public async ValueTask BookmarksPersistedAsync(ActivityExecutionContext context) => await InvokeTelnyxAsync(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.GetInput<CallAnsweredPayload>();
context.Set(Result, payload);
await HandleConnectedAsync(context);
}
/// <summary>
/// Invokes Telnyx' API to answer the call.
/// </summary>
private async ValueTask InvokeTelnyxAsync(ActivityExecutionContext context)
{
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
var request = new AnswerCallRequest
{
ClientState = context.CreateCorrelatingClientState()
};
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.AnswerCallAsync(callControlId, request, context.CancellationToken);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await HandleDisconnectedAsync(context);
}
}
}

View file

@ -0,0 +1,89 @@
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.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Runtime.Contracts;
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)]
[WebhookDriven(WebhookEventTypes.CallAnswered)]
public abstract class AnswerCallBase : Activity<CallAnsweredPayload>, IBookmarksPersistedHandler
{
/// <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; }
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
// Create a bookmark first, then after it's persisted, we call out to Telnyx.
// This ensures that the bookmark is available in case Telnyx responds with the webhook before the runtime got a chance to persist bookmarks.
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
context.CreateBookmark(new WebhookEventBookmarkPayload(WebhookEventTypes.CallAnswered, callControlId), ResumeAsync);
}
/// <summary>
/// Invokes Telnyx to answer the call.
/// </summary>
public async ValueTask BookmarksPersistedAsync(ActivityExecutionContext context) => await InvokeTelnyxAsync(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.GetInput<CallAnsweredPayload>();
context.Set(Result, payload);
await HandleConnectedAsync(context);
}
/// <summary>
/// Invokes Telnyx' API to answer the call.
/// </summary>
private async ValueTask InvokeTelnyxAsync(ActivityExecutionContext context)
{
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
var request = new AnswerCallRequest
{
ClientState = context.CreateCorrelatingClientState()
};
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.AnswerCallAsync(callControlId, request, context.CancellationToken);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await HandleDisconnectedAsync(context);
}
}
}

View file

@ -1,56 +1,15 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
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.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Runtime.Contracts;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Bridged", "Disconnected")]
[PublicAPI]
public class FlowBridgeCalls : BridgeCallsBase
{
/// <inheritdoc />
[JsonConstructor]
public FlowBridgeCalls()
{
}
/// <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");
}
/// <inheritdoc />
[PublicAPI]
public class BridgeCalls : BridgeCallsBase
{
/// <inheritdoc />
[JsonConstructor]
public BridgeCalls()
{
}
/// <inheritdoc />
public BridgeCalls([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
@ -73,85 +32,4 @@ public class BridgeCalls : BridgeCallsBase
/// <inheritdoc />
protected override async ValueTask HandleBridgedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Bridged, OnCompleted);
}
/// <summary>
/// Bridge two calls.
/// </summary>
[Activity(Constants.Namespace, "Bridge two calls.", Kind = ActivityKind.Task)]
[WebhookDriven(WebhookEventTypes.CallBridged)]
[PublicAPI]
public abstract class BridgeCallsBase : Activity<BridgedCallsOutput>, IBookmarksPersistedHandler
{
/// <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; }
/// <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; }
/// <inheritdoc />
public async ValueTask BookmarksPersistedAsync(ActivityExecutionContext context)
{
var callControlIdA = context.GetPrimaryCallControlId(CallControlIdA) ?? throw new Exception("CallControlA is required");
var callControlIdB = context.GetSecondaryCallControlId(CallControlIdB) ?? throw new Exception("CallControlB is required");
var request = new BridgeCallsRequest(callControlIdB, ClientState: context.CreateCorrelatingClientState());
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.BridgeCallsAsync(callControlIdA, request, context.CancellationToken);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await HandleDisconnectedAsync(context);
}
}
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
var callControlIdA = context.GetPrimaryCallControlId(CallControlIdA) ?? throw new Exception("CallControlA is required");
var callControlIdB = context.GetSecondaryCallControlId(CallControlIdB) ?? throw new Exception("CallControlB is required");
var bookmarkA = new WebhookEventBookmarkPayload(WebhookEventTypes.CallBridged, callControlIdA);
var bookmarkB = new WebhookEventBookmarkPayload(WebhookEventTypes.CallBridged, callControlIdB);
context.CreateBookmarks(new[] { bookmarkA, bookmarkB }, ResumeAsync);
}
protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context);
protected abstract ValueTask HandleBridgedAsync(ActivityExecutionContext context);
protected async ValueTask OnCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync();
private async ValueTask ResumeAsync(ActivityExecutionContext context)
{
var payload = context.GetInput<CallBridgedPayload>()!;
var callControlIdA = context.GetPrimaryCallControlId(CallControlIdA);
var callControlIdB = context.GetSecondaryCallControlId(CallControlIdB);
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);
}
}
}
public record BridgedCallsOutput(CallBridgedPayload PayloadA, CallBridgedPayload PayloadB);
}

View file

@ -0,0 +1,95 @@
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.Models;
using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Runtime.Contracts;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <summary>
/// Bridge two calls.
/// </summary>
[Activity(Constants.Namespace, "Bridge two calls.", Kind = ActivityKind.Task)]
[WebhookDriven(WebhookEventTypes.CallBridged)]
[PublicAPI]
public abstract class BridgeCallsBase : Activity<BridgedCallsOutput>, IBookmarksPersistedHandler
{
/// <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; }
/// <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; }
/// <inheritdoc />
public async ValueTask BookmarksPersistedAsync(ActivityExecutionContext context)
{
var callControlIdA = context.GetPrimaryCallControlId(CallControlIdA) ?? throw new Exception("CallControlA is required");
var callControlIdB = context.GetSecondaryCallControlId(CallControlIdB) ?? throw new Exception("CallControlB is required");
var request = new BridgeCallsRequest(callControlIdB, ClientState: context.CreateCorrelatingClientState());
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.BridgeCallsAsync(callControlIdA, request, context.CancellationToken);
}
catch (ApiException e)
{
if (!await e.CallIsNoLongerActiveAsync()) throw;
await HandleDisconnectedAsync(context);
}
}
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
var callControlIdA = context.GetPrimaryCallControlId(CallControlIdA) ?? throw new Exception("CallControlA is required");
var callControlIdB = context.GetSecondaryCallControlId(CallControlIdB) ?? throw new Exception("CallControlB is required");
var bookmarkA = new WebhookEventBookmarkPayload(WebhookEventTypes.CallBridged, callControlIdA);
var bookmarkB = new WebhookEventBookmarkPayload(WebhookEventTypes.CallBridged, callControlIdB);
context.CreateBookmarks(new[] { bookmarkA, bookmarkB }, ResumeAsync);
}
protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context);
protected abstract ValueTask HandleBridgedAsync(ActivityExecutionContext context);
protected async ValueTask OnCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync();
private async ValueTask ResumeAsync(ActivityExecutionContext context)
{
var payload = context.GetInput<CallBridgedPayload>()!;
var callControlIdA = context.GetPrimaryCallControlId(CallControlIdA);
var callControlIdB = context.GetSecondaryCallControlId(CallControlIdB);
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);
}
}
}

View file

@ -1,4 +1,3 @@
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
@ -8,7 +7,6 @@ using Elsa.Telnyx.Options;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
namespace Elsa.Telnyx.Activities;
@ -17,15 +15,8 @@ namespace Elsa.Telnyx.Activities;
/// Dial a number or SIP URI.
/// </summary>
[Activity(Constants.Namespace, "Dial a number or SIP URI.", Kind = ActivityKind.Task)]
[PublicAPI]
public class Dial : CodeActivity<DialResponse>
{
/// <inheritdoc />
[JsonConstructor]
public Dial()
{
}
/// <inheritdoc />
public Dial(string? source = default, int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Extensions;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Client.Models;
@ -12,7 +11,6 @@ using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
namespace Elsa.Telnyx.Activities;
@ -23,15 +21,8 @@ namespace Elsa.Telnyx.Activities;
[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)]
[PublicAPI]
public class DialAndWait : Activity<CallPayload>
{
/// <inheritdoc />
[JsonConstructor]
public DialAndWait()
{
}
/// <inheritdoc />
public DialAndWait(string? source = default, int? line = default) : base(source, line)
{

View file

@ -0,0 +1,23 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.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"));
}

View file

@ -0,0 +1,22 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.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");
}

View file

@ -0,0 +1,22 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.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");
}

View file

@ -0,0 +1,22 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.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");
}

View file

@ -0,0 +1,22 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.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 HandleDone(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Finished speaking", "Done");
}

View file

@ -0,0 +1,22 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.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");
}

View file

@ -0,0 +1,22 @@
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.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");
}

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
@ -12,7 +11,6 @@ using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Runtime.Contracts;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
@ -23,15 +21,8 @@ namespace Elsa.Telnyx.Activities;
[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)]
[PublicAPI]
public class GatherUsingAudio : Activity<CallGatherEndedPayload>, IBookmarksPersistedHandler
{
/// <inheritdoc />
[JsonConstructor]
public GatherUsingAudio()
{
}
/// <inheritdoc />
public GatherUsingAudio([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Telnyx.Attributes;
using Elsa.Telnyx.Bookmarks;
@ -12,7 +11,6 @@ using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Runtime.Contracts;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
@ -23,15 +21,8 @@ namespace Elsa.Telnyx.Activities;
[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)]
[PublicAPI]
public class GatherUsingSpeak : Activity<CallGatherEndedPayload>, IBookmarksPersistedHandler
{
/// <inheritdoc />
[JsonConstructor]
public GatherUsingSpeak()
{
}
/// <inheritdoc />
public GatherUsingSpeak([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,27 +1,18 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Services;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Alive", "Dead", "Done")]
[Activity(Constants.Namespace, "Get the status of a call.", Kind = ActivityKind.Task)]
[PublicAPI]
public class GetCallStatus : Activity<bool>
{
/// <inheritdoc />
[JsonConstructor]
public GetCallStatus()
{
}
/// <inheritdoc />
public GetCallStatus([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,52 +1,16 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Done", "Disconnected")]
[PublicAPI]
public class FlowHangupCall : HangupCallBase
{
/// <inheritdoc />
[JsonConstructor]
public FlowHangupCall()
{
}
/// <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");
}
/// <inheritdoc />
[PublicAPI]
public class HangupCall : HangupCallBase
{
/// <inheritdoc />
[JsonConstructor]
public HangupCall()
{
}
/// <inheritdoc />
public HangupCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
@ -67,52 +31,4 @@ public class HangupCall : HangupCallBase
/// Executed when any child activity completed.
/// </summary>
private async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync();
}
/// <summary>
/// Hang up the call.
/// </summary>
[Activity(Constants.Namespace, "Hang up the call.", Kind = ActivityKind.Task)]
[PublicAPI]
public abstract class HangupCallBase : Activity
{
/// <inheritdoc />
protected HangupCallBase([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 callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
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);
}

View file

@ -0,0 +1,56 @@
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.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 = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
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);
}

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Telnyx.Bookmarks;
@ -10,7 +9,6 @@ using Elsa.Telnyx.Payloads.Call;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Activities;
@ -22,15 +20,8 @@ namespace Elsa.Telnyx.Activities;
"Telnyx",
"Triggered when an inbound phone call is received for any of the specified source or destination phone numbers.",
Kind = ActivityKind.Trigger)]
[PublicAPI]
public class IncomingCall : Trigger<CallInitiatedPayload>
{
/// <inheritdoc />
[JsonConstructor]
public IncomingCall()
{
}
/// <inheritdoc />
public IncomingCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,12 +1,10 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
namespace Elsa.Telnyx.Activities;
@ -14,15 +12,8 @@ namespace Elsa.Telnyx.Activities;
/// Returns information about the provided phone number.
/// </summary>
[Activity(Constants.Namespace, "Returns information about the provided phone number.", Kind = ActivityKind.Task)]
[PublicAPI]
public class LookupNumber : CodeActivity<NumberLookupResponse>
{
/// <inheritdoc />
[JsonConstructor]
public LookupNumber()
{
}
/// <inheritdoc />
public LookupNumber([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,55 +1,16 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
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.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Runtime.Contracts;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Playback started", "Disconnected")]
[PublicAPI]
public class FlowPlayAudio : PlayAudioBase
{
/// <inheritdoc />
[JsonConstructor]
public FlowPlayAudio()
{
}
/// <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");
}
/// <inheritdoc />
[PublicAPI]
public class PlayAudio : PlayAudioBase
{
/// <inheritdoc />
[JsonConstructor]
public PlayAudio()
{
}
/// <inheritdoc />
public PlayAudio([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
@ -68,118 +29,4 @@ public class PlayAudio : PlayAudioBase
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync);
private async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync();
}
/// <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)]
[PublicAPI]
public abstract class PlayAudioBase : Activity, IBookmarksPersistedHandler
{
/// <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; }
/// <summary>
/// Calls out to Telnyx to start playing an audio file.
/// </summary>
public async ValueTask BookmarksPersistedAsync(ActivityExecutionContext context)
{
var loop = Loop.Get(context);
var request = new PlayAudioRequest(
AudioUrl.Get(context) ?? throw new Exception("AudioUrl is required."),
Overlay.Get(context),
string.IsNullOrWhiteSpace(loop) ? null : loop == "infinity" ? "infinity" : int.Parse(loop),
TargetLegs.Get(context).EmptyToNull(),
ClientState: context.CreateCorrelatingClientState()
);
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
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);
}
}
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
context.CreateBookmark(new WebhookEventBookmarkPayload(WebhookEventTypes.CallPlaybackStarted, callControlId), ResumeAsync);
}
/// <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);
}

View file

@ -0,0 +1,128 @@
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.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Runtime.Contracts;
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, IBookmarksPersistedHandler
{
/// <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; }
/// <summary>
/// Calls out to Telnyx to start playing an audio file.
/// </summary>
public async ValueTask BookmarksPersistedAsync(ActivityExecutionContext context)
{
var loop = Loop.Get(context);
var request = new PlayAudioRequest(
AudioUrl.Get(context) ?? throw new Exception("AudioUrl is required."),
Overlay.Get(context),
string.IsNullOrWhiteSpace(loop) ? null : loop == "infinity" ? "infinity" : int.Parse(loop),
TargetLegs.Get(context).EmptyToNull(),
ClientState: context.CreateCorrelatingClientState()
);
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
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);
}
}
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
context.CreateBookmark(new WebhookEventBookmarkPayload(WebhookEventTypes.CallPlaybackStarted, callControlId), ResumeAsync);
}
/// <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);
}

View file

@ -1,166 +1,15 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
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)]
[PublicAPI]
public abstract class SpeakTextBase : Activity
{
/// <inheritdoc />
protected SpeakTextBase([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 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.Get(context),
Voice.Get(context),
Payload.Get(context),
PayloadType.GetOrDefault(context).EmptyToNull(),
ServiceLevel.GetOrDefault(context).EmptyToNull(),
ClientState: context.CreateCorrelatingClientState()
);
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.SpeakTextAsync(callControlId, request, context.CancellationToken);
await HandleDone(context);
}
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 HandleDone(ActivityExecutionContext context);
private async ValueTask ResumeAsync(ActivityExecutionContext context) => await HandleDone(context);
}
/// <inheritdoc />
[FlowNode("Done", "Finished speaking", "Disconnected")]
[PublicAPI]
public class FlowSpeakText : SpeakTextBase
{
/// <inheritdoc />
[JsonConstructor]
public FlowSpeakText()
{
}
/// <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 HandleDone(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Finished speaking", "Done");
}
/// <inheritdoc />
[PublicAPI]
public class SpeakText : SpeakTextBase
{
/// <inheritdoc />
[JsonConstructor]
public SpeakText()
{
}
/// <inheritdoc />
public SpeakText([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
@ -171,7 +20,7 @@ public class SpeakText : SpeakTextBase
/// </summary>
[Port]
public IActivity? Disconnected { get; set; }
/// <summary>
/// The <see cref="IActivity"/> to execute when speaking has finished.
/// </summary>

View file

@ -0,0 +1,123 @@
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.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)]
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.Get(context),
Voice.Get(context),
Payload.Get(context),
PayloadType.GetOrDefault(context).EmptyToNull(),
ServiceLevel.GetOrDefault(context).EmptyToNull(),
ClientState: context.CreateCorrelatingClientState()
);
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.SpeakTextAsync(callControlId, request, context.CancellationToken);
await HandleDone(context);
}
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 HandleDone(ActivityExecutionContext context);
private async ValueTask ResumeAsync(ActivityExecutionContext context) => await HandleDone(context);
}

View file

@ -1,55 +1,16 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
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.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Recording finished", "Disconnected")]
[PublicAPI]
public class FlowStartRecording : StartRecordingBase
{
/// <inheritdoc />
[JsonConstructor]
public FlowStartRecording()
{
}
/// <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");
}
/// <inheritdoc />
[PublicAPI]
public class StartRecording : StartRecordingBase
{
/// <inheritdoc />
[JsonConstructor]
public StartRecording()
{
}
/// <inheritdoc />
public StartRecording([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
@ -72,100 +33,4 @@ public class StartRecording : StartRecordingBase
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync);
private async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync();
}
/// <summary>
/// Start recording the call.
/// </summary>
[Activity(Constants.Namespace, "Start recording the call.", Kind = ActivityKind.Task)]
[WebhookDriven(WebhookEventTypes.CallRecordingSaved)]
[PublicAPI]
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 = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.StartRecordingAsync(callControlId, request, context.CancellationToken);
context.CreateBookmark(new WebhookEventBookmarkPayload(WebhookEventTypes.CallRecordingSaved, callControlId), ResumeAsync);
}
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.GetInput<CallRecordingSavedPayload>();
context.Set(Result, payload);
await HandleCallRecordingSavedAsync(context);
}
}

View file

@ -0,0 +1,108 @@
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.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.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 = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
try
{
await telnyxClient.Calls.StartRecordingAsync(callControlId, request, context.CancellationToken);
context.CreateBookmark(new WebhookEventBookmarkPayload(WebhookEventTypes.CallRecordingSaved, callControlId), ResumeAsync);
}
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.GetInput<CallRecordingSavedPayload>();
context.Set(Result, payload);
await HandleCallRecordingSavedAsync(context);
}
}

View file

@ -1,52 +1,14 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
/// <inheritdoc />
[FlowNode("Done", "Disconnected")]
[PublicAPI]
public class FlowStopAudioPlayback : StopAudioPlaybackBase
{
/// <inheritdoc />
[JsonConstructor]
public FlowStopAudioPlayback()
{
}
/// <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");
}
/// <inheritdoc />
[PublicAPI]
public class StopAudioPlayback : StopAudioPlaybackBase
{
/// <inheritdoc />
[JsonConstructor]
public StopAudioPlayback()
{
}
/// <inheritdoc />
public StopAudioPlayback([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
@ -64,65 +26,4 @@ public class StopAudioPlayback : StopAudioPlaybackBase
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync);
private async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync();
}
/// <summary>
/// Stop audio playback.
/// </summary>
[Activity(Constants.Namespace, Description = "Stop audio playback.", Kind = ActivityKind.Task)]
[PublicAPI]
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());
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
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);
}

View file

@ -0,0 +1,70 @@
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.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());
var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required.");
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);
}

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
@ -8,7 +7,6 @@ using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
@ -18,15 +16,8 @@ namespace Elsa.Telnyx.Activities;
/// </summary>
[Activity(Constants.Namespace, "Stop recording the call.", Kind = ActivityKind.Task)]
[FlowNode("Recording stopped", "Disconnected")]
[PublicAPI]
public class StopRecording : Activity
{
/// <inheritdoc />
[JsonConstructor]
public StopRecording()
{
}
/// <inheritdoc />
public StopRecording([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
@ -9,7 +8,6 @@ using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
using Refit;
namespace Elsa.Telnyx.Activities;
@ -19,15 +17,8 @@ namespace Elsa.Telnyx.Activities;
/// </summary>
[Activity(Constants.Namespace, "Transfer a call to a new destination.", Kind = ActivityKind.Task)]
[FlowNode("Transferred", "Hangup", "Disconnected")]
[PublicAPI]
public class TransferCall : Activity
{
/// <inheritdoc />
[JsonConstructor]
public TransferCall()
{
}
/// <inheritdoc />
public TransferCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,6 +1,5 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Helpers;
@ -20,12 +19,6 @@ namespace Elsa.Telnyx.Activities;
[Browsable(false)]
public class WebhookEvent : Activity<Payload>
{
/// <inheritdoc />
[JsonConstructor]
public WebhookEvent()
{
}
/// <inheritdoc />
public WebhookEvent([CallerFilePath]string? source = default, [CallerLineNumber]int? line = default) : base(source, line)
{

View file

@ -0,0 +1,11 @@
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);

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.WorkflowContexts.Contracts;
@ -17,12 +16,6 @@ namespace Elsa.WorkflowContexts.Activities;
[PublicAPI]
public class SetWorkflowContextParameter : CodeActivity
{
/// <inheritdoc />
[JsonConstructor]
public SetWorkflowContextParameter()
{
}
/// <inheritdoc />
public SetWorkflowContextParameter([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,4 +1,3 @@
using Elsa.Workflows.Api.Middleware;
using Elsa.Workflows.Api.RealTime.Hubs;
using Microsoft.AspNetCore.Builder;

View file

@ -1,5 +1,4 @@
using Elsa.Workflows.Api.RealTime.Contracts;
using Elsa.Workflows.Api.RealTime.Messages;
using Elsa.Workflows.Runtime.Contracts;
using JetBrains.Annotations;
using Microsoft.AspNetCore.SignalR;

View file

@ -5,6 +5,7 @@ using Elsa.Workflows.Core.Behaviors;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Core;
@ -12,6 +13,7 @@ namespace Elsa.Workflows.Core;
/// Base class for custom activities.
/// </summary>
[DebuggerDisplay("{Type} - {Id}")]
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
public abstract class Activity : IActivity, ISignalHandler
{
private readonly ICollection<SignalHandlerRegistration> _signalHandlers = new List<SignalHandlerRegistration>();

View file

@ -1,11 +1,13 @@
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Memory;
using JetBrains.Annotations;
namespace Elsa.Workflows.Core.Abstractions;
namespace Elsa.Workflows.Core;
/// <summary>
/// A base class for implementing workflow definitions using the pipelineBuilder API.
/// </summary>
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
public abstract class WorkflowBase : IWorkflow
{
/// <summary>

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Signals;
@ -14,17 +13,12 @@ namespace Elsa.Workflows.Core.Activities;
[PublicAPI]
public class Break : CodeActivity
{
/// <inheritdoc />
[JsonConstructor]
public Break()
{
}
/// <inheritdoc />
public Break([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
await context.SendSignalAsync(new BreakSignal());

View file

@ -1,6 +1,5 @@
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows.Core.Activities.Flowchart.Models;
@ -18,12 +17,6 @@ namespace Elsa.Workflows.Core.Activities;
[PublicAPI]
public class Complete : Activity
{
/// <inheritdoc />
[JsonConstructor]
public Complete()
{
}
/// <inheritdoc />
public Complete([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,5 @@
using System.ComponentModel;
using System.Text.Json.Serialization;
using System.Runtime.CompilerServices;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
@ -14,8 +14,7 @@ namespace Elsa.Workflows.Core.Activities;
public class Correlate : CodeActivity
{
/// <inheritdoc />
[JsonConstructor]
public Correlate()
public Correlate([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
@ -24,8 +23,9 @@ public class Correlate : CodeActivity
/// </summary>
[Description("An expression that evaluates to the value to store as the correlation id")]
public Input<string> CorrelationId { get; set; } = default!;
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
var correlationId = context.Get(CorrelationId);
context.WorkflowExecutionContext.CorrelationId = correlationId;

View file

@ -0,0 +1,18 @@
using System.Runtime.CompilerServices;
using Elsa.Workflows.Core.Attributes;
using JetBrains.Annotations;
namespace Elsa.Workflows.Core.Activities;
/// <summary>
/// Marks the end of a flowchart, causing the flowchart to complete.
/// </summary>
[Activity("Elsa", "Flow", "A milestone activity that marks the start of a flowchart.", Kind = ActivityKind.Action)]
[PublicAPI]
public class End : CodeActivity
{
/// <inheritdoc />
public End([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
}

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
@ -14,12 +13,6 @@ namespace Elsa.Workflows.Core.Activities;
[PublicAPI]
public class Fault : Activity
{
/// <inheritdoc />
[JsonConstructor]
public Fault()
{
}
/// <inheritdoc />
public Fault([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Workflows.Core.Attributes;
using JetBrains.Annotations;
@ -12,12 +11,6 @@ namespace Elsa.Workflows.Core.Activities;
[PublicAPI]
public class Finish : Activity
{
/// <inheritdoc />
[JsonConstructor]
public Finish()
{
}
/// <inheritdoc />
public Finish([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
@ -18,12 +17,6 @@ namespace Elsa.Workflows.Core.Activities.Flowchart.Activities;
[PublicAPI]
public class FlowDecision : Activity
{
/// <inheritdoc />
[JsonConstructor]
public FlowDecision()
{
}
/// <inheritdoc />
public FlowDecision([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -6,7 +6,6 @@ using Elsa.Workflows.Core.Activities.Flowchart.Models;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
using Newtonsoft.Json;
namespace Elsa.Workflows.Core.Activities.Flowchart.Activities;
@ -17,12 +16,6 @@ namespace Elsa.Workflows.Core.Activities.Flowchart.Activities;
[PublicAPI]
public class FlowJoin : Activity, IJoinNode
{
/// <inheritdoc />
[JsonConstructor]
public FlowJoin()
{
}
/// <inheritdoc />
public FlowJoin([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
@ -14,11 +13,6 @@ namespace Elsa.Workflows.Core.Activities.Flowchart.Activities;
[PublicAPI]
public class FlowNode : Activity
{
/// <inheritdoc />
[JsonConstructor]
public FlowNode()
{
}
/// <inheritdoc />
public FlowNode([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)

View file

@ -20,12 +20,6 @@ namespace Elsa.Workflows.Core.Activities.Flowchart.Activities;
[PublicAPI]
public class FlowSwitch : Activity
{
/// <inheritdoc />
[JsonConstructor]
public FlowSwitch()
{
}
/// <inheritdoc />
public FlowSwitch([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,6 +1,5 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Workflows.Core.Activities.Flowchart.Contracts;
using Elsa.Workflows.Core.Activities.Flowchart.Extensions;
@ -22,12 +21,6 @@ public class Flowchart : Container
{
internal const string ScopeProperty = "Scope";
/// <inheritdoc />
[JsonConstructor]
public Flowchart() : this(default, default)
{
}
/// <inheritdoc />
public Flowchart([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
@ -92,47 +85,55 @@ public class Flowchart : Container
scope.RegisterActivityExecution(completedActivity);
if (children.Any())
// If the completed activity is an End activity, complete the flowchart.
if (completedActivity is End)
{
scope.AddActivities(children);
// Schedule each child, but only if all of its left inbound activities have already executed.
foreach (var activity in children)
await flowchartActivityExecutionContext.CompleteActivityAsync();
}
else
{
if (children.Any())
{
var inboundActivities = Connections.LeftInboundActivities(activity).ToList();
scope.AddActivities(children);
// If the completed activity is not part of the left inbound path, always allow its children to be scheduled.
if (!inboundActivities.Contains(completedActivity))
// Schedule each child, but only if all of its left inbound activities have already executed.
foreach (var activity in children)
{
await flowchartActivityExecutionContext.ScheduleActivityAsync(activity);
continue;
}
var inboundActivities = Connections.LeftInboundActivities(activity).ToList();
// If the activity is anything but a join activity, only schedule it if all of its left-inbound activities have executed, effectively implementing a "wait all" join.
if (activity is not IJoinNode)
{
var executionCount = scope.GetExecutionCount(activity);
var haveInboundActivitiesExecuted = inboundActivities.All(x => scope.GetExecutionCount(x) > executionCount);
if (haveInboundActivitiesExecuted)
// If the completed activity is not part of the left inbound path, always allow its children to be scheduled.
if (!inboundActivities.Contains(completedActivity))
{
await flowchartActivityExecutionContext.ScheduleActivityAsync(activity);
}
else
{
await flowchartActivityExecutionContext.ScheduleActivityAsync(activity);
continue;
}
// If the activity is anything but a join activity, only schedule it if all of its left-inbound activities have executed, effectively implementing a "wait all" join.
if (activity is not IJoinNode)
{
var executionCount = scope.GetExecutionCount(activity);
var haveInboundActivitiesExecuted = inboundActivities.All(x => scope.GetExecutionCount(x) > executionCount);
if (haveInboundActivitiesExecuted)
await flowchartActivityExecutionContext.ScheduleActivityAsync(activity);
}
else
{
await flowchartActivityExecutionContext.ScheduleActivityAsync(activity);
}
}
}
}
if (!children.Any())
{
var workflowExecutionContext = context.ReceiverActivityExecutionContext.WorkflowExecutionContext;
if (!children.Any())
{
var workflowExecutionContext = context.ReceiverActivityExecutionContext.WorkflowExecutionContext;
// If there is no pending work, complete the flowchart activity.
var hasPendingWork = HasPendingWork(workflowExecutionContext);
// If there is no pending work, complete the flowchart activity.
var hasPendingWork = HasPendingWork(workflowExecutionContext);
if (!hasPendingWork)
await flowchartActivityExecutionContext.CompleteActivityAsync();
if (!hasPendingWork)
await flowchartActivityExecutionContext.CompleteActivityAsync();
}
}
flowchartActivityExecutionContext.SetProperty(ScopeProperty, scope);
@ -149,7 +150,7 @@ public class Flowchart : Container
var activityExecutionContexts = workflowExecutionContext.ActivityExecutionContexts.Where(x => activityIds.Contains(x.Activity.Id)).ToList();
var hasPendingWork = workflowExecutionContext.Scheduler.List().Any(x => activityNodeIds.Contains(x.ActivityId));
var hasRunningActivityInstances = activityExecutionContexts.Any(x => x.Status == ActivityStatus.Running);
return hasRunningActivityInstances || hasPendingWork;
}
@ -158,18 +159,18 @@ public class Flowchart : Container
// If there's a trigger that triggered this workflow, use that.
var triggerActivityId = context.WorkflowExecutionContext.TriggerActivityId;
var triggerActivity = triggerActivityId != null ? Activities.FirstOrDefault(x => x.Id == triggerActivityId) : default;
if(triggerActivity != null)
if (triggerActivity != null)
return triggerActivity;
// If an explicit Start activity was provided, use that.
if(Start != null)
if (Start != null)
return Start;
// If there is a Start activity on the flowchart, use that.
var startActivity = Activities.FirstOrDefault(x => x is Start);
if(startActivity != null)
if (startActivity != null)
return startActivity;
// If there is a single activity that has no inbound connections, use that.
@ -177,7 +178,7 @@ public class Flowchart : Container
if (root != null)
return root;
// If no start activity found, return the first activity.
return Activities.FirstOrDefault();
}
@ -190,7 +191,7 @@ public class Flowchart : Container
let inboundConnections = Connections.Any(x => x.Target.Activity == activity)
where !inboundConnections
select activity;
var rootActivity = query.FirstOrDefault();
return rootActivity;
}

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Behaviors;
@ -18,12 +17,6 @@ public class For : Activity
{
private const string CurrentStepProperty = "CurrentStep";
/// <inheritdoc />
[JsonConstructor]
public For() : this(default, default)
{
}
/// <inheritdoc />
public For([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows.Core.Attributes;
@ -19,12 +18,6 @@ public class ForEach : Activity
{
private const string CurrentIndexProperty = "CurrentIndex";
/// <inheritdoc />
[JsonConstructor]
public ForEach() : this(default, default)
{
}
/// <inheritdoc />
public ForEach([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default)
{
@ -98,8 +91,7 @@ public class ForEach : Activity
public class ForEach<T> : ForEach
{
/// <inheritdoc />
[JsonConstructor]
public ForEach()
public ForEach([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}

View file

@ -1,7 +1,6 @@
using System.Collections.Immutable;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
@ -17,12 +16,6 @@ namespace Elsa.Workflows.Core.Activities;
[Browsable(false)]
public class Fork : Activity
{
/// <inheritdoc />
[JsonConstructor]
public Fork()
{
}
/// <inheritdoc />
public Fork([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows.Core.Attributes;
@ -16,12 +15,6 @@ namespace Elsa.Workflows.Core.Activities;
[PublicAPI]
public class If : Activity<bool>
{
/// <inheritdoc />
[JsonConstructor]
public If()
{
}
/// <inheritdoc />
public If([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,6 +1,5 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Workflows.Core.Attributes;
using JetBrains.Annotations;
@ -17,12 +16,6 @@ public class Inline : CodeActivity
{
private readonly Func<ActivityExecutionContext, ValueTask> _activity = default!;
/// <inheritdoc />
[JsonConstructor]
public Inline()
{
}
/// <inheritdoc />
public Inline([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Elsa.Workflows.Core.Attributes;
using JetBrains.Annotations;
@ -13,12 +14,12 @@ namespace Elsa.Workflows.Core.Activities;
public class NotFoundActivity : CodeActivity
{
/// <inheritdoc />
public NotFoundActivity()
public NotFoundActivity([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
public NotFoundActivity(string missingTypeName)
public NotFoundActivity(string missingTypeName, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line)
{
MissingTypeName = missingTypeName;
}

View file

@ -1,6 +1,5 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
@ -18,12 +17,6 @@ public class Parallel : Container
{
private const string ScheduledChildrenProperty = "ScheduledChildren";
/// <inheritdoc />
[JsonConstructor]
public Parallel()
{
}
/// <inheritdoc />
public Parallel([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
@ -19,12 +18,6 @@ public class ParallelForEach<T> : CodeActivity
{
private const string CollectedCountProperty = nameof(CollectedCountProperty);
/// <inheritdoc />
[JsonConstructor]
public ParallelForEach()
{
}
/// <inheritdoc />
public ParallelForEach([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
@ -16,12 +15,6 @@ namespace Elsa.Workflows.Core.Activities;
[PublicAPI]
public class ReadLine : CodeActivity<string>
{
/// <inheritdoc />
[JsonConstructor]
public ReadLine()
{
}
/// <inheritdoc />
public ReadLine([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,6 +1,5 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
@ -19,13 +18,7 @@ namespace Elsa.Workflows.Core.Activities;
public class Sequence : Container
{
private const string CurrentIndexProperty = "CurrentIndex";
/// <inheritdoc />
[JsonConstructor]
public Sequence() : this(default(string?), default)
{
}
/// <inheritdoc />
public Sequence(params IActivity[] activities) : this(default(string?), default)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
@ -18,12 +17,6 @@ public class SetName : CodeActivity
/// </summary>
public const string WorkflowInstanceNameKey = "WorkflowInstanceName";
/// <inheritdoc />
[JsonConstructor]
public SetName()
{
}
/// <inheritdoc />
public SetName([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,6 +1,5 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows.Core.Attributes;
@ -18,12 +17,6 @@ namespace Elsa.Workflows.Core.Activities;
[PublicAPI]
public class SetVariable<T> : CodeActivity
{
/// <inheritdoc />
[JsonConstructor]
public SetVariable()
{
}
/// <inheritdoc />
public SetVariable([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
@ -87,12 +80,6 @@ public class SetVariable<T> : CodeActivity
[PublicAPI]
public class SetVariable : CodeActivity
{
/// <inheritdoc />
[JsonConstructor]
public SetVariable()
{
}
/// <inheritdoc />
public SetVariable([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Workflows.Core.Attributes;
using JetBrains.Annotations;
@ -8,16 +7,10 @@ namespace Elsa.Workflows.Core.Activities;
/// <summary>
/// Marks the start of a flowchart.
/// </summary>
[Activity("Elsa", "Flow", "A milestone activity with no behavior other than marking the start of a flowchart.", Kind = ActivityKind.Action)]
[Activity("Elsa", "Flow", "A milestone activity that marks the start of a flowchart.", Kind = ActivityKind.Action)]
[PublicAPI]
public class Start : CodeActivity
{
/// <inheritdoc />
[JsonConstructor]
public Start()
{
}
/// <inheritdoc />
public Start([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -19,12 +19,6 @@ namespace Elsa.Workflows.Core.Activities;
[PublicAPI]
public class Switch : Activity
{
/// <inheritdoc />
[JsonConstructor]
public Switch()
{
}
/// <inheritdoc />
public Switch([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{

View file

@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows.Core.Attributes;
@ -26,17 +25,15 @@ public class While : Activity
};
/// <inheritdoc />
[JsonConstructor]
public While() : this(default, default, default)
public While([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
Behaviors.Add<BreakBehavior>(this);
}
/// <inheritdoc />
public While(IActivity? body = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
public While(IActivity? body = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line)
{
Body = body;
Behaviors.Add<BreakBehavior>(this);
Behaviors.Remove<AutoCompleteBehavior>();
}
/// <inheritdoc />

View file

@ -17,7 +17,7 @@ namespace Elsa.Workflows.Core.Activities;
public class Workflow : Composite<object>, ICloneable
{
/// <summary>
/// Constructor.
/// Initializes a new instance of the <see cref="Workflow"/> class.
/// </summary>
public Workflow(
WorkflowIdentity identity,
@ -47,7 +47,7 @@ public class Workflow : Composite<object>, ICloneable
}
/// <summary>
/// Constructor.
/// Initializes a new instance of the <see cref="Workflow"/> class.
/// </summary>
public Workflow(IActivity root) : this()
{

View file

@ -7,6 +7,7 @@ using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Memory;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Services;
using JetBrains.Annotations;
namespace Elsa.Workflows.Core.Activities;
@ -14,16 +15,12 @@ namespace Elsa.Workflows.Core.Activities;
/// Write a line of text to the console.
/// </summary>
[Activity("Elsa", "Console", "Write a line of text to the console.")]
[PublicAPI]
public class WriteLine : CodeActivity
{
/// <inheritdoc />
internal WriteLine([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
[JsonConstructor]
public WriteLine() : this(default, default)
private WriteLine(string? source = default, int? line = default) : base(source, line)
{
}

View file

@ -1,5 +1,3 @@
using Elsa.Workflows.Core.Abstractions;
namespace Elsa.Workflows.Core.Contracts;
/// <summary>

View file

@ -1,4 +1,3 @@
using Elsa.Workflows.Core.Abstractions;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.State;

View file

@ -1,4 +1,6 @@
namespace Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Activities;
namespace Elsa.Workflows.Core;
/// <summary>
/// Controls when a <see cref="Fork"/> completes.

View file

@ -53,7 +53,7 @@ public static class ActivityPropertyExtensions
/// </summary>
public static void SetSource(this IActivity activity, string? sourceFile, int? lineNumber)
{
if (sourceFile == null || lineNumber != null)
if (sourceFile == null || lineNumber == null)
return;
var source = $"{Path.GetFileName(sourceFile)}:{lineNumber}";

View file

@ -0,0 +1,66 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace Elsa.Workflows.Core.Serialization;
/// <summary>
/// A custom JSON type info resolver that allows private constructors to be used when deserializing JSON.
/// </summary>
public class ActivityConstructorContractResolver : DefaultJsonTypeInfoResolver
{
/// <inheritdoc />
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
var jsonTypeInfo = base.GetTypeInfo(type, options);
if (jsonTypeInfo is not { Kind: JsonTypeInfoKind.Object, CreateObject: null })
return jsonTypeInfo;
var defaultConstructor = GetDefaultConstructor(jsonTypeInfo.Type);
if (defaultConstructor != null)
{
jsonTypeInfo.CreateObject = defaultConstructor;
}
return jsonTypeInfo;
}
private static Func<object>? GetDefaultConstructor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
{
foreach (var constructor in type.GetConstructors(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
{
// If we have a default constructor, use that one.
if (!constructor.GetParameters().Any())
return () => constructor.Invoke(default, Array.Empty<object>())!;
// Else, find a constructor with the following signature: (string?, int?).
// Check for a constructor with the following signature:
// ctor(string, int) where string is decorated with [CallerFilePath] and int is decorated with [CallerLineNumber]
var parameters = constructor.GetParameters();
// Check parameter count
if (parameters.Length != 2) continue;
// Does the constructor have a [JsonConstructor] attribute?
var isJsonConstructor = constructor.GetCustomAttribute<JsonConstructorAttribute>() != null;
// Check first parameter type and attribute
if (parameters[0].ParameterType != typeof(string) ||
parameters[0].DefaultValue != null ||
(parameters[0].GetCustomAttribute<CallerFilePathAttribute>() == null && !isJsonConstructor)) continue;
// Check second parameter type and attribute
if (parameters[1].ParameterType != typeof(int?) ||
parameters[1].DefaultValue != null ||
(parameters[1].GetCustomAttribute<CallerLineNumberAttribute>() == null && !isJsonConstructor)) continue;
return () => constructor.Invoke(new object[] { null!, 0 });
}
return null;
}
}

View file

@ -1,33 +0,0 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace Elsa.Workflows.Core.Serialization;
/// <summary>
/// A custom JSON type info resolver that allows private constructors to be used when deserializing JSON.
/// </summary>
public class PrivateConstructorContractResolver : DefaultJsonTypeInfoResolver
{
/// <inheritdoc />
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
var jsonTypeInfo = base.GetTypeInfo(type, options);
if (jsonTypeInfo is not { Kind: JsonTypeInfoKind.Object, CreateObject: null })
return jsonTypeInfo;
if (HasPrivateJsonConstructor(jsonTypeInfo.Type))
{
// The type doesn't have public constructors
jsonTypeInfo.CreateObject = () => Activator.CreateInstance(jsonTypeInfo.Type, true)!;
}
return jsonTypeInfo;
}
private static bool HasPrivateJsonConstructor(Type type) => type
.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)
.Any(x => (x.IsPrivate || x.IsAssembly || x.IsFamily) && !x.GetParameters().Any() && x.GetCustomAttribute<JsonConstructorAttribute>() != null);
}

View file

@ -49,7 +49,7 @@ public class ApiSerializer : IApiSerializer
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.PropertyNameCaseInsensitive = true;
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.TypeInfoResolver = new PrivateConstructorContractResolver();
options.TypeInfoResolver = new ActivityConstructorContractResolver();
options.Converters.Add(Create<JsonStringEnumConverter>());
options.Converters.Add(Create<TypeJsonConverter>());
options.Converters.Add(JsonMetadataServices.TimeSpanConverter);

View file

@ -54,7 +54,7 @@ public class JsonActivitySerializer : IActivitySerializer
{
var options = new JsonSerializerOptions
{
TypeInfoResolver = new PrivateConstructorContractResolver(),
TypeInfoResolver = new ActivityConstructorContractResolver(),
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull

View file

@ -1,7 +1,6 @@
using Elsa.Common.Contracts;
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Core.Abstractions;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;

Some files were not shown because too many files have changed in this diff Show more