parent
c9ca4a121d
commit
166e11c416
24
src/bundles/Elsa.WorkflowServer.Web/HeartbeatWorkflow.cs
Normal file
24
src/bundles/Elsa.WorkflowServer.Web/HeartbeatWorkflow.cs
Normal 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}")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ services
|
|||
|
||||
elsa
|
||||
.AddActivitiesFrom<Program>()
|
||||
.AddWorkflowsFrom<Program>()
|
||||
.UseFluentStorageProvider()
|
||||
.AddTypeAlias<ApiResponse<User>>("ApiResponse[User]")
|
||||
.UseIdentity(identity =>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using Elsa.Common.Entities;
|
||||
|
||||
namespace Elsa.Dapper.Modules.Runtime.Records;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
36
src/modules/Elsa.Http/Activities/FlowSendHttpRequest.cs
Normal file
36
src/modules/Elsa.Http/Activities/FlowSendHttpRequest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
159
src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs
Normal file
159
src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs
Normal 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));
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
37
src/modules/Elsa.Http/Models/HttpStatusCodeCase.cs
Normal file
37
src/modules/Elsa.Http/Models/HttpStatusCodeCase.cs
Normal 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; }
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Elsa.Common.Contracts;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Scheduling.Commands;
|
||||
using Elsa.Scheduling.Contracts;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Elsa.Common.Contracts;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Scheduling.Commands;
|
||||
using Elsa.Scheduling.Contracts;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Elsa.Common.Contracts;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Scheduling.Commands;
|
||||
using Elsa.Scheduling.Contracts;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/modules/Elsa.Telnyx/Activities/AnswerCallBase.cs
Normal file
89
src/modules/Elsa.Telnyx/Activities/AnswerCallBase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
95
src/modules/Elsa.Telnyx/Activities/BridgeCallsBase.cs
Normal file
95
src/modules/Elsa.Telnyx/Activities/BridgeCallsBase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
23
src/modules/Elsa.Telnyx/Activities/FlowAnswerCall.cs
Normal file
23
src/modules/Elsa.Telnyx/Activities/FlowAnswerCall.cs
Normal 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"));
|
||||
}
|
||||
22
src/modules/Elsa.Telnyx/Activities/FlowBridgeCalls.cs
Normal file
22
src/modules/Elsa.Telnyx/Activities/FlowBridgeCalls.cs
Normal 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");
|
||||
}
|
||||
22
src/modules/Elsa.Telnyx/Activities/FlowHangupCall.cs
Normal file
22
src/modules/Elsa.Telnyx/Activities/FlowHangupCall.cs
Normal 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");
|
||||
}
|
||||
22
src/modules/Elsa.Telnyx/Activities/FlowPlayAudio.cs
Normal file
22
src/modules/Elsa.Telnyx/Activities/FlowPlayAudio.cs
Normal 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");
|
||||
}
|
||||
22
src/modules/Elsa.Telnyx/Activities/FlowSpeakText.cs
Normal file
22
src/modules/Elsa.Telnyx/Activities/FlowSpeakText.cs
Normal 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");
|
||||
}
|
||||
22
src/modules/Elsa.Telnyx/Activities/FlowStartRecording.cs
Normal file
22
src/modules/Elsa.Telnyx/Activities/FlowStartRecording.cs
Normal 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");
|
||||
}
|
||||
22
src/modules/Elsa.Telnyx/Activities/FlowStopAudioPlayback.cs
Normal file
22
src/modules/Elsa.Telnyx/Activities/FlowStopAudioPlayback.cs
Normal 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");
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
56
src/modules/Elsa.Telnyx/Activities/HangupCallBase.cs
Normal file
56
src/modules/Elsa.Telnyx/Activities/HangupCallBase.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
128
src/modules/Elsa.Telnyx/Activities/PlayAudioBase.cs
Normal file
128
src/modules/Elsa.Telnyx/Activities/PlayAudioBase.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
123
src/modules/Elsa.Telnyx/Activities/SpeakTextBase.cs
Normal file
123
src/modules/Elsa.Telnyx/Activities/SpeakTextBase.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
108
src/modules/Elsa.Telnyx/Activities/StartRecordingBase.cs
Normal file
108
src/modules/Elsa.Telnyx/Activities/StartRecordingBase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
70
src/modules/Elsa.Telnyx/Activities/StopAudioPlaybackBase.cs
Normal file
70
src/modules/Elsa.Telnyx/Activities/StopAudioPlaybackBase.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
11
src/modules/Elsa.Telnyx/Models/BridgedCallsOutput.cs
Normal file
11
src/modules/Elsa.Telnyx/Models/BridgedCallsOutput.cs
Normal 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);
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Elsa.Workflows.Api.Middleware;
|
||||
using Elsa.Workflows.Api.RealTime.Hubs;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
18
src/modules/Elsa.Workflows.Core/Activities/End.cs
Normal file
18
src/modules/Elsa.Workflows.Core/Activities/End.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using Elsa.Workflows.Core.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.Core.Contracts;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Elsa.Workflows.Core.Abstractions;
|
||||
using Elsa.Workflows.Core.Activities;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.State;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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}";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue