mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
685 changed files with 20523 additions and 13443 deletions
@ -1,150 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text.Json.Serialization; |
|||
using Algolia.Search.Clients; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
#pragma warning disable IDE0059 // Value assigned to symbol is never used
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Algolia; |
|||
|
|||
public sealed class AlgoliaActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer) : RuleActionHandler<AlgoliaAction, AlgoliaJob>(formatter) |
|||
{ |
|||
private readonly ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex> clients = new ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex>(key => |
|||
{ |
|||
var client = new SearchClient(key.AppId, key.ApiKey); |
|||
|
|||
return client.InitIndex(key.IndexName); |
|||
}); |
|||
|
|||
protected override async Task<(string Description, AlgoliaJob Data)> CreateJobAsync(EnrichedEvent @event, AlgoliaAction action) |
|||
{ |
|||
if (@event is not IEnrichedEntityEvent entityEvent) |
|||
{ |
|||
return ("Ignore", new AlgoliaJob()); |
|||
} |
|||
|
|||
var delete = @event.ShouldDelete(scriptEngine, action.Delete); |
|||
|
|||
var ruleDescription = string.Empty; |
|||
var contentId = entityEvent.Id.ToString(); |
|||
var content = (AlgoliaContent?)null; |
|||
var indexName = (await FormatAsync(action.IndexName, @event))!; |
|||
|
|||
if (delete) |
|||
{ |
|||
ruleDescription = $"Delete entry from Algolia index: {indexName}"; |
|||
} |
|||
else |
|||
{ |
|||
ruleDescription = $"Add entry to Algolia index: {indexName}"; |
|||
|
|||
try |
|||
{ |
|||
string? jsonString; |
|||
|
|||
if (!string.IsNullOrEmpty(action.Document)) |
|||
{ |
|||
jsonString = await FormatAsync(action.Document, @event); |
|||
jsonString = jsonString?.Trim(); |
|||
} |
|||
else |
|||
{ |
|||
jsonString = ToJson(@event); |
|||
} |
|||
|
|||
content = serializer.Deserialize<AlgoliaContent>(jsonString!); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
content = new AlgoliaContent |
|||
{ |
|||
More = new Dictionary<string, object> |
|||
{ |
|||
["error"] = $"Invalid JSON: {ex.Message}", |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
content.ObjectID = contentId; |
|||
} |
|||
|
|||
var ruleJob = new AlgoliaJob |
|||
{ |
|||
AppId = action.AppId, |
|||
ApiKey = action.ApiKey, |
|||
Content = delete ? null : serializer.Serialize(content, true), |
|||
ContentId = contentId, |
|||
IndexName = indexName, |
|||
}; |
|||
|
|||
return (ruleDescription, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(AlgoliaJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(job.AppId)) |
|||
{ |
|||
return Result.Ignored(); |
|||
} |
|||
|
|||
var index = await clients.GetClientAsync((job.AppId, job.ApiKey, job.IndexName)); |
|||
try |
|||
{ |
|||
if (job.Content != null) |
|||
{ |
|||
var raw = new[] |
|||
{ |
|||
new JRaw(job.Content), |
|||
}; |
|||
|
|||
var response = await index.SaveObjectsAsync(raw, null, ct, true); |
|||
|
|||
return Result.Success(serializer.Serialize(response, true)); |
|||
} |
|||
else |
|||
{ |
|||
var response = await index.DeleteObjectAsync(job.ContentId, null, ct); |
|||
|
|||
return Result.Success(serializer.Serialize(response, true)); |
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
return Result.Failed(ex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public sealed class AlgoliaContent |
|||
{ |
|||
[JsonPropertyName("objectID")] |
|||
public string ObjectID { get; set; } |
|||
|
|||
[JsonExtensionData] |
|||
public Dictionary<string, object> More { get; set; } = []; |
|||
} |
|||
|
|||
public sealed class AlgoliaJob |
|||
{ |
|||
public string AppId { get; set; } |
|||
|
|||
public string ApiKey { get; set; } |
|||
|
|||
public string ContentId { get; set; } |
|||
|
|||
public string IndexName { get; set; } |
|||
|
|||
public string? Content { get; set; } |
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text.Json.Serialization; |
|||
using Algolia.Search.Clients; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.Algolia; |
|||
|
|||
[FlowStep( |
|||
Title = "Algolia", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M16 .842C7.633.842.842 7.625.842 16S7.625 31.158 16 31.158c8.374 0 15.158-6.791 15.158-15.166S24.375.842 16 .842zm0 25.83c-5.898 0-10.68-4.781-10.68-10.68S10.101 5.313 16 5.313s10.68 4.781 10.68 10.679-4.781 10.68-10.68 10.68zm0-19.156v7.956c0 .233.249.388.458.279l7.055-3.663a.312.312 0 0 0 .124-.434 8.807 8.807 0 0 0-7.319-4.447z'/></svg>", |
|||
IconColor = "#0d9bf9", |
|||
Display = "Populate Algolia index", |
|||
Description = "Populate a full text search index in Algolia.", |
|||
ReadMore = "https://www.algolia.com/")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public record AlgoliaFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
private static readonly ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex> Clients = new ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex>(key => |
|||
{ |
|||
var client = new SearchClient(key.AppId, key.ApiKey); |
|||
|
|||
return client.InitIndex(key.IndexName); |
|||
}); |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Application Id", Description = "The application ID.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string AppId { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string ApiKey { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Index Name", Description = "The name of the index.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string IndexName { get; set; } |
|||
|
|||
[Display(Name = "Document", Description = "The optional custom document.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression(ExpressionFallback.Event)] |
|||
public string? Document { get; set; } |
|||
|
|||
[Display(Name = "Deletion", Description = "The condition when to delete the entry.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? Delete { get; set; } |
|||
|
|||
public override ValueTask PrepareAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
|
|||
if (!@event.ShouldDelete(executionContext, Delete)) |
|||
{ |
|||
Document = null; |
|||
return default; |
|||
} |
|||
|
|||
AlgoliaContent content; |
|||
try |
|||
{ |
|||
content = executionContext.DeserializeJson<AlgoliaContent>(Document!); |
|||
content.ObjectID = @event.GetOrCreateId().Id; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
content = new AlgoliaContent |
|||
{ |
|||
More = new Dictionary<string, object> |
|||
{ |
|||
["error"] = $"Invalid JSON: {ex.Message}", |
|||
}, |
|||
ObjectID = @event.GetOrCreateId().Id, |
|||
}; |
|||
} |
|||
|
|||
Document = executionContext.SerializeJson(content); |
|||
return default; |
|||
} |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
|
|||
var (id, isGenerated) = @event.GetOrCreateId(); |
|||
if (isGenerated && Document == null) |
|||
{ |
|||
executionContext.LogSkipped("Can only delete content for static identities."); |
|||
return Next(); |
|||
} |
|||
|
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var serializer = executionContext.Resolve<IJsonSerializer>(); |
|||
|
|||
var index = await Clients.GetClientAsync((AppId, ApiKey, IndexName)); |
|||
if (Document != null) |
|||
{ |
|||
var response = await index.SaveObjectsAsync([new JRaw(Document)], null, ct, true); |
|||
|
|||
executionContext.Log($"Document with ID '{id}' upserted", serializer.Serialize(response, true)); |
|||
} |
|||
else |
|||
{ |
|||
var response = await index.DeleteObjectAsync(id, null, ct); |
|||
|
|||
executionContext.Log($"Document with ID '{id}' deleted", serializer.Serialize(response, true)); |
|||
} |
|||
|
|||
return Next(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
executionContext.Log("Failed with error", ex.Message); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new AlgoliaAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
|
|||
private sealed class AlgoliaContent |
|||
{ |
|||
[JsonPropertyName("objectID")] |
|||
public string ObjectID { get; set; } |
|||
|
|||
[JsonExtensionData] |
|||
public Dictionary<string, object> More { get; set; } = []; |
|||
} |
|||
} |
|||
@ -1,73 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.WindowsAzure.Storage; |
|||
using Microsoft.WindowsAzure.Storage.Queue; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.AzureQueue; |
|||
|
|||
public sealed class AzureQueueActionHandler(RuleEventFormatter formatter) : RuleActionHandler<AzureQueueAction, AzureQueueJob>(formatter) |
|||
{ |
|||
private readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> clients = new ClientPool<(string ConnectionString, string QueueName), CloudQueue>(key => |
|||
{ |
|||
var storageAccount = CloudStorageAccount.Parse(key.ConnectionString); |
|||
|
|||
var queueClient = storageAccount.CreateCloudQueueClient(); |
|||
var queueRef = queueClient.GetQueueReference(key.QueueName); |
|||
|
|||
return queueRef; |
|||
}); |
|||
|
|||
protected override async Task<(string Description, AzureQueueJob Data)> CreateJobAsync(EnrichedEvent @event, AzureQueueAction action) |
|||
{ |
|||
var queueName = await FormatAsync(action.Queue, @event); |
|||
|
|||
string? requestBody; |
|||
|
|||
if (!string.IsNullOrEmpty(action.Payload)) |
|||
{ |
|||
requestBody = await FormatAsync(action.Payload, @event); |
|||
} |
|||
else |
|||
{ |
|||
requestBody = ToEnvelopeJson(@event); |
|||
} |
|||
|
|||
var ruleText = $"Send AzureQueueJob to azure queue '{queueName}'"; |
|||
var ruleJob = new AzureQueueJob |
|||
{ |
|||
QueueConnectionString = action.ConnectionString, |
|||
QueueName = queueName!, |
|||
MessageBodyV2 = requestBody, |
|||
}; |
|||
|
|||
return (ruleText, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(AzureQueueJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var queue = await clients.GetClientAsync((job.QueueConnectionString, job.QueueName)); |
|||
|
|||
await queue.AddMessageAsync(new CloudQueueMessage(job.MessageBodyV2), null, null, null, null, ct); |
|||
|
|||
return Result.Complete(); |
|||
} |
|||
} |
|||
|
|||
public sealed class AzureQueueJob |
|||
{ |
|||
public string QueueConnectionString { get; set; } |
|||
|
|||
public string QueueName { get; set; } |
|||
|
|||
public string? MessageBodyV2 { get; set; } |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text.RegularExpressions; |
|||
using Microsoft.WindowsAzure.Storage; |
|||
using Microsoft.WindowsAzure.Storage.Queue; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.AzureQueue; |
|||
|
|||
[FlowStep( |
|||
Title = "Azure Queue", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M.011 16L0 6.248l12-1.63V16zM14 4.328L29.996 2v14H14zM30 18l-.004 14L14 29.75V18zM12 29.495L.01 27.851.009 18H12z'/></svg>", |
|||
IconColor = "#0d9bf9", |
|||
Display = "Send to Azure Queue", |
|||
Description = "Send an event to azure queue storage.", |
|||
ReadMore = "https://azure.microsoft.com/en-us/services/storage/queues/")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed partial record AzureQueueFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "Connection", Description = "The connection string to the storage account.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string ConnectionString { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Queue", Description = "The name of the queue.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string Queue { get; set; } |
|||
|
|||
[Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression(ExpressionFallback.Envelope)] |
|||
public string? Payload { get; set; } |
|||
|
|||
private static readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> Clients = new ClientPool<(string ConnectionString, string QueueName), CloudQueue>(key => |
|||
{ |
|||
var storageAccount = CloudStorageAccount.Parse(key.ConnectionString); |
|||
|
|||
var queueClient = storageAccount.CreateCloudQueueClient(); |
|||
var queueRef = queueClient.GetQueueReference(key.QueueName); |
|||
|
|||
return queueRef; |
|||
}); |
|||
|
|||
public override ValueTask ValidateAsync(FlowValidationContext validationContext, AddStepError addError, |
|||
CancellationToken ct) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(Queue) && !QueueNameRegex().IsMatch(Queue)) |
|||
{ |
|||
addError(nameof(Queue), "Queue must be valid azure queue name."); |
|||
} |
|||
|
|||
return base.ValidateAsync(validationContext, addError, ct); |
|||
} |
|||
|
|||
public async override ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var queue = await Clients.GetClientAsync((ConnectionString, Queue)); |
|||
|
|||
await queue.AddMessageAsync(new CloudQueueMessage(Payload), null, null, null, null, ct); |
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new AzureQueueAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
|
|||
[GeneratedRegex("^[a-z][a-z0-9]{2,}(\\-[a-z0-9]+)*$")]
|
|||
private static partial Regex QueueNameRegex(); |
|||
} |
|||
@ -1,67 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Entities.Collaboration; |
|||
using Squidex.Domain.Apps.Events.Comments; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Extensions.Actions.Comment; |
|||
|
|||
public sealed class CommentActionHandler(RuleEventFormatter formatter, ICollaborationService collaboration) : RuleActionHandler<CommentAction, CommentCreated>(formatter) |
|||
{ |
|||
private const string Description = "Send a Comment"; |
|||
|
|||
protected override async Task<(string Description, CommentCreated Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action) |
|||
{ |
|||
if (@event is not EnrichedContentEvent contentEvent) |
|||
{ |
|||
return ("Ignore", new CommentCreated()); |
|||
} |
|||
|
|||
var ruleJob = new CommentCreated |
|||
{ |
|||
AppId = contentEvent.AppId, |
|||
}; |
|||
|
|||
var text = await FormatAsync(action.Text, @event); |
|||
|
|||
if (string.IsNullOrWhiteSpace(text)) |
|||
{ |
|||
return ("NoText", new CommentCreated()); |
|||
} |
|||
|
|||
ruleJob.Text = text; |
|||
|
|||
if (!string.IsNullOrEmpty(action.Client)) |
|||
{ |
|||
ruleJob.Actor = RefToken.Client(action.Client); |
|||
} |
|||
else |
|||
{ |
|||
ruleJob.Actor = contentEvent.Actor; |
|||
} |
|||
|
|||
ruleJob.CommentsId = contentEvent.Id; |
|||
|
|||
return (Description, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(CommentCreated job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
if (job.CommentsId == default) |
|||
{ |
|||
return Result.Ignored(); |
|||
} |
|||
|
|||
await collaboration.CommentAsync(job.AppId, job.CommentsId, job.Text, job.Actor, job.Url, true, ct); |
|||
|
|||
return Result.Success($"Commented: {job.Text}"); |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Entities.Collaboration; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.Comment; |
|||
|
|||
[FlowStep( |
|||
Title = "Comment", |
|||
IconImage = "<svg version='1.1' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path d='M20.016 15.984v-12h-16.031v14.016l2.016-2.016h14.016zM20.016 2.016c1.078 0 1.969 0.891 1.969 1.969v12c0 1.078-0.891 2.016-1.969 2.016h-14.016l-3.984 3.984v-18c0-1.078 0.891-1.969 1.969-1.969h16.031z'></path></svg>", |
|||
IconColor = "#3389ff", |
|||
Display = "Create comment", |
|||
Description = "Create a comment for a content event.")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record CommentFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "Text", Description = "The comment text.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression] |
|||
public string Text { get; set; } |
|||
|
|||
[Display(Name = "Client", Description = "An optional client name.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? Client { get; set; } |
|||
|
|||
public async override ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
if (@event is not EnrichedContentEvent contentEvent) |
|||
{ |
|||
executionContext.LogSkipped("Invalid event."); |
|||
return Next(); |
|||
} |
|||
|
|||
if (string.IsNullOrWhiteSpace(Text)) |
|||
{ |
|||
executionContext.LogSkipped("No text."); |
|||
return Next(); |
|||
} |
|||
|
|||
RefToken actor; |
|||
if (!string.IsNullOrEmpty(Client)) |
|||
{ |
|||
actor = RefToken.Client(Client); |
|||
} |
|||
else |
|||
{ |
|||
actor = contentEvent.Actor; |
|||
} |
|||
|
|||
var collaboration = executionContext.Resolve<ICollaborationService>(); |
|||
|
|||
await collaboration.CommentAsync( |
|||
contentEvent.AppId, |
|||
contentEvent.Id, |
|||
Text, |
|||
actor, |
|||
null, |
|||
true, |
|||
ct); |
|||
|
|||
executionContext.Log("Commented", Text); |
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new CommentAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
} |
|||
@ -1,67 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Squidex.Infrastructure.Json; |
|||
using Command = Squidex.Domain.Apps.Entities.Contents.Commands.CreateContent; |
|||
|
|||
namespace Squidex.Extensions.Actions.CreateContent; |
|||
|
|||
public sealed class CreateContentActionHandler(RuleEventFormatter formatter, IAppProvider appProvider, ICommandBus commandBus, IJsonSerializer jsonSerializer) : RuleActionHandler<CreateContentAction, Command>(formatter) |
|||
{ |
|||
private const string Description = "Create a content"; |
|||
|
|||
protected override async Task<(string Description, Command Data)> CreateJobAsync(EnrichedEvent @event, CreateContentAction action) |
|||
{ |
|||
var ruleJob = new Command |
|||
{ |
|||
AppId = @event.AppId, |
|||
}; |
|||
|
|||
var schema = await appProvider.GetSchemaAsync(@event.AppId.Id, action.Schema, true) |
|||
?? throw new InvalidOperationException($"Cannot find schema '{action.Schema}'"); |
|||
|
|||
ruleJob.SchemaId = schema.NamedId(); |
|||
ruleJob.FromRule = true; |
|||
|
|||
var json = await FormatAsync(action.Data, @event); |
|||
|
|||
ruleJob.Data = jsonSerializer.Deserialize<ContentData>(json!); |
|||
|
|||
if (!string.IsNullOrEmpty(action.Client)) |
|||
{ |
|||
ruleJob.Actor = RefToken.Client(action.Client); |
|||
} |
|||
else if (@event is EnrichedUserEventBase userEvent) |
|||
{ |
|||
ruleJob.Actor = userEvent.Actor; |
|||
} |
|||
|
|||
if (action.Publish) |
|||
{ |
|||
ruleJob.Status = Status.Published; |
|||
} |
|||
|
|||
return (Description, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(Command job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var command = job; |
|||
|
|||
await commandBus.PublishAsync(command, ct); |
|||
|
|||
return Result.Success($"Created to: {command.SchemaId.Name}"); |
|||
} |
|||
} |
|||
@ -0,0 +1,111 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
using Command = Squidex.Domain.Apps.Entities.Contents.Commands.CreateContent; |
|||
|
|||
namespace Squidex.Extensions.Actions.CreateContent; |
|||
|
|||
[FlowStep( |
|||
Title = "CreateContent", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'><path d='M21.875 28H6.125A6.087 6.087 0 010 21.875V6.125A6.087 6.087 0 016.125 0h15.75A6.087 6.087 0 0128 6.125v15.75A6.088 6.088 0 0121.875 28zM6.125 1.75A4.333 4.333 0 001.75 6.125v15.75a4.333 4.333 0 004.375 4.375h15.75a4.333 4.333 0 004.375-4.375V6.125a4.333 4.333 0 00-4.375-4.375H6.125z'/><path d='M13.125 12.25H7.35c-1.575 0-2.888-1.313-2.888-2.888V7.349c0-1.575 1.313-2.888 2.888-2.888h5.775c1.575 0 2.887 1.313 2.887 2.888v2.013c0 1.575-1.312 2.888-2.887 2.888zM7.35 6.212c-.613 0-1.138.525-1.138 1.138v2.012A1.16 1.16 0 007.35 10.5h5.775a1.16 1.16 0 001.138-1.138V7.349a1.16 1.16 0 00-1.138-1.138H7.35zM22.662 16.713H5.337c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h17.237c.525 0 .875.35.875.875s-.35.875-.787.875zM15.138 21.262h-9.8c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h9.713c.525 0 .875.35.875.875s-.35.875-.787.875z'/></svg>", |
|||
IconColor = "#3389ff", |
|||
Display = "Create content", |
|||
Description = "Create a a new content item for any schema.")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record CreateContentFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "Data", Description = "The content data.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression] |
|||
public string Data { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Schema", Description = "The name of the schema.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string Schema { get; set; } |
|||
|
|||
[Display(Name = "Client", Description = "An optional client name.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? Client { get; set; } |
|||
|
|||
[Display(Name = "Publish", Description = "Publish the content.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public bool Publish { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
|
|||
var command = new Command |
|||
{ |
|||
AppId = @event.AppId, |
|||
}; |
|||
|
|||
var schema = |
|||
await executionContext.Resolve<IAppProvider>() |
|||
.GetSchemaAsync(@event.AppId.Id, Schema, true, ct); |
|||
|
|||
if (schema == null) |
|||
{ |
|||
executionContext.LogSkipped("Invalid Schema."); |
|||
return Next(); |
|||
} |
|||
|
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
command.SchemaId = schema.NamedId(); |
|||
command.FromRule = true; |
|||
command.Data = executionContext.DeserializeJson<ContentData>(Data); |
|||
|
|||
if (!string.IsNullOrEmpty(Client)) |
|||
{ |
|||
command.Actor = RefToken.Client(Client); |
|||
} |
|||
else if (@event is EnrichedUserEventBase userEvent) |
|||
{ |
|||
command.Actor = userEvent.Actor; |
|||
} |
|||
|
|||
if (Publish) |
|||
{ |
|||
command.Status = Status.Published; |
|||
} |
|||
|
|||
await executionContext.Resolve<ICommandBus>() |
|||
.PublishAsync(command, ct); |
|||
|
|||
executionContext.Log($"Content created for schema '{schema.Name}', ID: {command.ContentId}"); |
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new CreateContentAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
} |
|||
@ -1,183 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Net.Http.Json; |
|||
using System.Text.RegularExpressions; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Assets; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Entities; |
|||
using Squidex.Domain.Apps.Entities.Assets; |
|||
using Squidex.Domain.Apps.Entities.Assets.Commands; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Text; |
|||
|
|||
namespace Squidex.Extensions.Actions.DeepDetect; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
internal sealed partial class DeepDetectActionHandler( |
|||
RuleEventFormatter formatter, |
|||
IHttpClientFactory httpClientFactory, |
|||
IJsonSerializer jsonSerializer, |
|||
IAppProvider appProvider, |
|||
IAssetQueryService assetQuery, |
|||
ICommandBus commandBus, |
|||
IUrlGenerator urlGenerator) |
|||
: RuleActionHandler<DeepDetectAction, DeepDetectJob>(formatter) |
|||
{ |
|||
private const string Description = "Analyze Image"; |
|||
|
|||
protected override Task<(string Description, DeepDetectJob Data)> CreateJobAsync(EnrichedEvent @event, DeepDetectAction action) |
|||
{ |
|||
if (@event is not EnrichedAssetEvent assetEvent) |
|||
{ |
|||
return Task.FromResult(("Ignore", new DeepDetectJob())); |
|||
} |
|||
|
|||
if (assetEvent.AssetType != AssetType.Image) |
|||
{ |
|||
return Task.FromResult(("Ignore", new DeepDetectJob())); |
|||
} |
|||
|
|||
var ruleJob = new DeepDetectJob |
|||
{ |
|||
Actor = assetEvent.Actor, |
|||
AppId = assetEvent.AppId.Id, |
|||
AssetId = assetEvent.Id, |
|||
MaximumTags = action.MaximumTags, |
|||
MinimumPropability = action.MinimumProbability, |
|||
Url = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString(), assetEvent.FileVersion), |
|||
}; |
|||
|
|||
return Task.FromResult((Description, ruleJob)); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(DeepDetectJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(job.Url)) |
|||
{ |
|||
return Result.Ignored(); |
|||
} |
|||
|
|||
var httpClient = httpClientFactory.CreateClient("DeepDetect"); |
|||
|
|||
var response = await httpClient.PostAsJsonAsync("predict", new |
|||
{ |
|||
service = "squidexdetector", |
|||
output = new |
|||
{ |
|||
best = job.MaximumTags, |
|||
confidence_threshold = job.MinimumPropability / 100d, |
|||
}, |
|||
data = new[] |
|||
{ |
|||
job.Url, |
|||
}, |
|||
}, ct); |
|||
|
|||
var body = await response.Content.ReadAsStringAsync(ct); |
|||
|
|||
if (!response.IsSuccessStatusCode) |
|||
{ |
|||
return Result.Failed(new InvalidOperationException($"Failed with status code {response.StatusCode}\n\n{body}")); |
|||
} |
|||
|
|||
var responseJson = jsonSerializer.Deserialize<DetectResponse>(body); |
|||
|
|||
var tags = responseJson!.Body.Predictions.SelectMany(x => x.Classes); |
|||
|
|||
if (!tags.Any()) |
|||
{ |
|||
return Result.Success(body); |
|||
} |
|||
|
|||
var app = await appProvider.GetAppAsync(job.AppId, true, ct); |
|||
if (app == null) |
|||
{ |
|||
return Result.Failed(new InvalidOperationException("App not found.")); |
|||
} |
|||
|
|||
var context = Context.Admin(app); |
|||
|
|||
var asset = await assetQuery.FindAsync(context, job.AssetId, ct: ct); |
|||
if (asset == null) |
|||
{ |
|||
return Result.Failed(new InvalidOperationException("Asset not found.")); |
|||
} |
|||
|
|||
var command = new AnnotateAsset |
|||
{ |
|||
Tags = asset.TagNames, |
|||
AssetId = asset.Id, |
|||
AppId = asset.AppId, |
|||
Actor = job.Actor, |
|||
FromRule = true, |
|||
}; |
|||
|
|||
foreach (var tag in tags) |
|||
{ |
|||
var tagParts = tag.Cat.Split(',')[0].Split(' ', StringSplitOptions.RemoveEmptyEntries); |
|||
|
|||
if (IdRegex().IsMatch(tagParts[0])) |
|||
{ |
|||
tagParts = tagParts.Skip(1).ToArray(); |
|||
} |
|||
|
|||
var tagName = string.Join('_', tagParts.Select(x => x.Slugify())); |
|||
|
|||
command.Tags.Add($"ai/{tagName}"); |
|||
} |
|||
|
|||
await commandBus.PublishAsync(command, ct); |
|||
return Result.Success(body); |
|||
} |
|||
|
|||
private sealed class DetectResponse |
|||
{ |
|||
public DetectBody Body { get; set; } |
|||
} |
|||
|
|||
private sealed class DetectBody |
|||
{ |
|||
public DetectPredications[] Predictions { get; set; } |
|||
} |
|||
|
|||
private sealed class DetectPredications |
|||
{ |
|||
public DetectClass[] Classes { get; set; } |
|||
} |
|||
|
|||
private sealed class DetectClass |
|||
{ |
|||
public double Prob { get; set; } |
|||
|
|||
public string Cat { get; set; } |
|||
} |
|||
|
|||
[GeneratedRegex("^n[0-9]+$")]
|
|||
private static partial Regex IdRegex(); |
|||
} |
|||
|
|||
public sealed class DeepDetectJob |
|||
{ |
|||
public DomainId AppId { get; set; } |
|||
|
|||
public DomainId AssetId { get; set; } |
|||
|
|||
public RefToken Actor { get; set; } |
|||
|
|||
public long MaximumTags { get; set; } |
|||
|
|||
public long MinimumPropability { get; set; } |
|||
|
|||
public string? Url { get; set; } |
|||
} |
|||
@ -0,0 +1,188 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Net.Http.Json; |
|||
using System.Text.RegularExpressions; |
|||
using Google.Apis.Json; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Assets; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Entities; |
|||
using Squidex.Domain.Apps.Entities.Assets; |
|||
using Squidex.Domain.Apps.Entities.Assets.Commands; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Text; |
|||
|
|||
namespace Squidex.Extensions.Actions.DeepDetect; |
|||
|
|||
[FlowStep( |
|||
Title = "DeepDetect", |
|||
IconImage = "<svg viewBox='0 0 28 28' xmlns='http://www.w3.org/2000/svg'><g style='stroke-width:1.24962' fill='none'><path fill='#ff5252' d='M13 21.92H0v-8.032h9.386V10.92h3.57v11zm-9.386-4.889v1.702H9.43v-1.702z' style='stroke-width:1.24962' transform='matrix(.78667 0 0 .81405 2.529 2.668)'/><path fill='#fff' d='M29.164 21.92h-13V14.028H25.7V5.92h3.464zm-9.536-4.804v1.673H25.7v-1.673z' style='stroke-width:1.24962' transform='matrix(.78667 0 0 .81405 2.529 2.668)'/></g></svg>", |
|||
IconColor = "#526a75", |
|||
Display = "Annotate image", |
|||
Description = "Annotate an image using deep detect.")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed partial record DeepDetectFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[Display(Name = "Min Propability", Description = "The minimum probability for objects to be recognized (0 - 100).")] |
|||
[Editor(FlowStepEditor.Number)] |
|||
public long MinimumPropability { get; set; } |
|||
|
|||
[Display(Name = "Max Tags", Description = "The maximum number of tags to use.")] |
|||
[Editor(FlowStepEditor.Number)] |
|||
public long MaximumTags { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
if (@event is not EnrichedAssetEvent assetEvent) |
|||
{ |
|||
executionContext.LogSkipped("Invalid event."); |
|||
return Next(); |
|||
} |
|||
|
|||
if (assetEvent.AssetType != AssetType.Image) |
|||
{ |
|||
executionContext.LogSkipped("Invalid event (not an image)."); |
|||
return Next(); |
|||
} |
|||
|
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var urlToDownload = |
|||
executionContext.Resolve<IUrlGenerator>() |
|||
.AssetContent(assetEvent.AppId, assetEvent.Id.ToString(), assetEvent.FileVersion); |
|||
|
|||
var httpClient = |
|||
executionContext.Resolve<IHttpClientFactory>() |
|||
.CreateClient("DeepDetect"); |
|||
|
|||
var response = await httpClient.PostAsJsonAsync("predict", new |
|||
{ |
|||
service = "squidexdetector", |
|||
output = new |
|||
{ |
|||
best = MaximumTags, |
|||
confidence_threshold = MinimumPropability / 100d, |
|||
}, |
|||
data = new[] |
|||
{ |
|||
urlToDownload, |
|||
}, |
|||
}, ct); |
|||
|
|||
var responseBody = await response.Content.ReadAsStringAsync(ct); |
|||
if (!response.IsSuccessStatusCode) |
|||
{ |
|||
executionContext.Log($"Failed with status code {response.StatusCode}", responseBody); |
|||
response.EnsureSuccessStatusCode(); |
|||
} |
|||
|
|||
var jsonResponse = executionContext.DeserializeJson<DetectResponse>(responseBody); |
|||
|
|||
var tags = jsonResponse!.Body.Predictions.SelectMany(x => x.Classes); |
|||
if (!tags.Any()) |
|||
{ |
|||
executionContext.Log("Warning: No tags returned.", responseBody); |
|||
return Next(); |
|||
} |
|||
|
|||
var app = |
|||
await executionContext.Resolve<IAppProvider>() |
|||
.GetAppAsync(assetEvent.AppId.Id, true, ct); |
|||
|
|||
if (app == null) |
|||
{ |
|||
executionContext.LogSkipped("App not found."); |
|||
return Next(); |
|||
} |
|||
|
|||
var context = Context.Admin(app); |
|||
|
|||
var asset = |
|||
await executionContext.Resolve<IAssetQueryService>() |
|||
.FindAsync(context, assetEvent.Id, ct: ct); |
|||
|
|||
if (asset == null) |
|||
{ |
|||
executionContext.LogSkipped("Asset not found."); |
|||
return Next(); |
|||
} |
|||
|
|||
var command = new AnnotateAsset |
|||
{ |
|||
Tags = asset.TagNames, |
|||
AssetId = asset.Id, |
|||
AppId = asset.AppId, |
|||
Actor = assetEvent.Actor, |
|||
FromRule = true, |
|||
}; |
|||
|
|||
foreach (var tag in tags) |
|||
{ |
|||
var tagParts = tag.Cat.Split(',')[0].Split(' ', StringSplitOptions.RemoveEmptyEntries); |
|||
|
|||
if (IdRegex().IsMatch(tagParts[0])) |
|||
{ |
|||
tagParts = tagParts.Skip(1).ToArray(); |
|||
} |
|||
|
|||
var tagName = string.Join('_', tagParts.Select(x => x.Slugify())); |
|||
|
|||
command.Tags.Add($"ai/{tagName}"); |
|||
} |
|||
|
|||
await executionContext.Resolve<ICommandBus>() |
|||
.PublishAsync(command, ct); |
|||
|
|||
executionContext.Log("Tags Added.", responseBody); |
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new DeepDetectAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
|
|||
private sealed class DetectResponse |
|||
{ |
|||
public DetectBody Body { get; set; } |
|||
} |
|||
|
|||
private sealed class DetectBody |
|||
{ |
|||
public DetectPredications[] Predictions { get; set; } |
|||
} |
|||
|
|||
private sealed class DetectPredications |
|||
{ |
|||
public DetectClass[] Classes { get; set; } |
|||
} |
|||
|
|||
private sealed class DetectClass |
|||
{ |
|||
public double Prob { get; set; } |
|||
|
|||
public string Cat { get; set; } |
|||
} |
|||
|
|||
[GeneratedRegex("^n[0-9]+$")]
|
|||
private static partial Regex IdRegex(); |
|||
} |
|||
@ -1,86 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Discourse; |
|||
|
|||
public sealed class DiscourseActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) : RuleActionHandler<DiscourseAction, DiscourseJob>(formatter) |
|||
{ |
|||
private const string DescriptionCreatePost = "Create discourse Post"; |
|||
private const string DescriptionCreateTopic = "Create discourse Topic"; |
|||
|
|||
protected override async Task<(string Description, DiscourseJob Data)> CreateJobAsync(EnrichedEvent @event, DiscourseAction action) |
|||
{ |
|||
var url = $"{action.Url.ToString().TrimEnd('/')}/posts.json?api_key={action.ApiKey}&api_username={action.ApiUsername}"; |
|||
|
|||
var json = new Dictionary<string, object?> |
|||
{ |
|||
["title"] = await FormatAsync(action.Title!, @event), |
|||
}; |
|||
|
|||
if (action.Topic != null) |
|||
{ |
|||
json.Add("topic_id", action.Topic.Value); |
|||
} |
|||
|
|||
if (action.Category != null) |
|||
{ |
|||
json.Add("category", action.Category.Value); |
|||
} |
|||
|
|||
json["raw"] = await FormatAsync(action.Text, @event); |
|||
|
|||
var requestBody = ToJson(json); |
|||
|
|||
var ruleJob = new DiscourseJob |
|||
{ |
|||
ApiKey = action.ApiKey, |
|||
ApiUserName = action.ApiUsername, |
|||
RequestUrl = url, |
|||
RequestBody = requestBody, |
|||
}; |
|||
|
|||
var description = |
|||
action.Topic != null ? |
|||
DescriptionCreateTopic : |
|||
DescriptionCreatePost; |
|||
|
|||
return (description, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(DiscourseJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var httpClient = httpClientFactory.CreateClient("DiscourseAction"); |
|||
|
|||
var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) |
|||
{ |
|||
Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json"), |
|||
}; |
|||
|
|||
request.Headers.TryAddWithoutValidation("Api-Key", job.ApiKey); |
|||
request.Headers.TryAddWithoutValidation("Api-Username", job.ApiUserName); |
|||
|
|||
return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); |
|||
} |
|||
} |
|||
|
|||
public sealed class DiscourseJob |
|||
{ |
|||
public string ApiKey { get; set; } |
|||
|
|||
public string ApiUserName { get; set; } |
|||
|
|||
public string RequestUrl { get; set; } |
|||
|
|||
public string RequestBody { get; set; } |
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Extensions.Actions.Algolia; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.Discourse; |
|||
|
|||
[FlowStep( |
|||
Title = "Discourse", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M16.137 0C7.376 0 0 7.037 0 15.721V32l16.134-.016C24.895 31.984 32 24.676 32 15.995S24.888 0 16.137 0zm.336 6.062a9.862 9.862 0 0 1 5.119 1.555l-.038-.023a.747.747 0 0 1 .05.033l-.033-.021c.288.183.529.353.762.534l-.022-.016c.058.044.094.073.131.103l-.018-.014c.218.174.411.34.597.514l-.005-.005a9.48 9.48 0 0 1 .639.655l.009.01c.073.082.154.176.233.272l.014.018c.053.06.116.133.177.206l.013.017-.052-.047-.008-.007c.104.126.218.273.328.423l.02.028.001.001-.001-.001c-.01-.018.005.005.019.028l.024.042c.145.206.301.451.445.704l.025.048c.131.226.273.51.402.801l.025.063a9.504 9.504 0 0 1 .802 3.853c0 5.38-4.401 9.741-9.831 9.741a9.866 9.866 0 0 1-4.106-.888l.061.025-6.39 1.43 1.78-5.672a7.888 7.888 0 0 1-.293-.584l-.025-.061a8.226 8.226 0 0 1-.254-.617l-.022-.068A1.043 1.043 0 0 1 7 19.017l-.022-.067a8.428 8.428 0 0 1-.246-.829l-.014-.067a9.402 9.402 0 0 1-.265-2.248c0-5.381 4.403-9.744 9.834-9.744l.194.002h-.01z'/></svg>", |
|||
IconColor = "#eB6121", |
|||
Display = "Post to discourse", |
|||
Description = "Create a post or topic at discourse.", |
|||
ReadMore = "https://www.discourse.org/")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record DiscourseFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[AbsoluteUrl] |
|||
[LocalizedRequired] |
|||
[Display(Name = "Server Url", Description = "The url to the discourse server.")] |
|||
[Editor(FlowStepEditor.Url)] |
|||
public Uri Url { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Api Key", Description = "The api key to authenticate to your discourse server.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string ApiKey { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Api User", Description = "The api username to authenticate to your discourse server.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string ApiUsername { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Text", Description = "The text as markdown.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression] |
|||
public string Text { get; set; } |
|||
|
|||
[Display(Name = "Title", Description = "The optional title when creating new topics.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string? Title { get; set; } |
|||
|
|||
[Display(Name = "Topic", Description = "The optional topic id.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public int? Topic { get; set; } |
|||
|
|||
[Display(Name = "Category", Description = "The optional category id.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public int? Category { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var url = $"{Url.ToString().TrimEnd('/')}/posts.json?api_key={ApiKey}&api_username={ApiUsername}"; |
|||
|
|||
var body = new Dictionary<string, object?> |
|||
{ |
|||
["title"] = Title, |
|||
}; |
|||
|
|||
if (Topic != null) |
|||
{ |
|||
body.Add("topic_id", Topic.Value); |
|||
} |
|||
|
|||
if (Category != null) |
|||
{ |
|||
body.Add("category", Category.Value); |
|||
} |
|||
|
|||
body["raw"] = Text; |
|||
|
|||
var jsonRequest = executionContext.SerializeJson(body); |
|||
|
|||
var httpClient = |
|||
executionContext.Resolve<IHttpClientFactory>() |
|||
.CreateClient("DiscourseAction"); |
|||
|
|||
var request = new HttpRequestMessage(HttpMethod.Post, url) |
|||
{ |
|||
Content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"), |
|||
}; |
|||
|
|||
request.Headers.TryAddWithoutValidation("Api-Key", ApiKey); |
|||
request.Headers.TryAddWithoutValidation("Api-Username", ApiUsername); |
|||
|
|||
var (_, dump) = await httpClient.SendAsync(executionContext, request, jsonRequest, ct); |
|||
|
|||
if (Topic != null) |
|||
{ |
|||
executionContext.Log("Created post in topic {Topic}", dump); |
|||
} |
|||
else |
|||
{ |
|||
executionContext.Log("Created Topic", dump); |
|||
} |
|||
|
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new AlgoliaAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
} |
|||
@ -1,157 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text.Json.Serialization; |
|||
using Elasticsearch.Net; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
#pragma warning disable IDE0059 // Value assigned to symbol is never used
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.ElasticSearch; |
|||
|
|||
public sealed class ElasticSearchActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer) : RuleActionHandler<ElasticSearchAction, ElasticSearchJob>(formatter) |
|||
{ |
|||
private readonly ClientPool<(Uri Host, string? Username, string? Password), ElasticLowLevelClient> clients = new ClientPool<(Uri Host, string? Username, string? Password), ElasticLowLevelClient>(key => |
|||
{ |
|||
var config = new ConnectionConfiguration(key.Host); |
|||
|
|||
if (!string.IsNullOrEmpty(key.Username) && !string.IsNullOrWhiteSpace(key.Password)) |
|||
{ |
|||
config = config.BasicAuthentication(key.Username, key.Password); |
|||
} |
|||
|
|||
return new ElasticLowLevelClient(config); |
|||
}); |
|||
|
|||
protected override async Task<(string Description, ElasticSearchJob Data)> CreateJobAsync(EnrichedEvent @event, ElasticSearchAction action) |
|||
{ |
|||
var delete = @event.ShouldDelete(scriptEngine, action.Delete); |
|||
|
|||
string contentId; |
|||
|
|||
if (@event is IEnrichedEntityEvent enrichedEntityEvent) |
|||
{ |
|||
contentId = enrichedEntityEvent.Id.ToString(); |
|||
} |
|||
else |
|||
{ |
|||
contentId = DomainId.NewGuid().ToString(); |
|||
} |
|||
|
|||
var ruleText = string.Empty; |
|||
var ruleJob = new ElasticSearchJob |
|||
{ |
|||
IndexName = (await FormatAsync(action.IndexName, @event))!, |
|||
ServerHost = action.Host.ToString(), |
|||
ServerUser = action.Username, |
|||
ServerPassword = action.Password, |
|||
ContentId = contentId, |
|||
}; |
|||
|
|||
if (delete) |
|||
{ |
|||
ruleText = $"Delete entry index: {ruleJob.IndexName}"; |
|||
} |
|||
else |
|||
{ |
|||
ruleText = $"Upsert to index: {ruleJob.IndexName}"; |
|||
|
|||
ElasticSearchContent content; |
|||
try |
|||
{ |
|||
string? jsonString; |
|||
|
|||
if (!string.IsNullOrEmpty(action.Document)) |
|||
{ |
|||
jsonString = await FormatAsync(action.Document, @event); |
|||
jsonString = jsonString?.Trim(); |
|||
} |
|||
else |
|||
{ |
|||
jsonString = ToJson(@event); |
|||
} |
|||
|
|||
content = serializer.Deserialize<ElasticSearchContent>(jsonString!); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
content = new ElasticSearchContent |
|||
{ |
|||
More = new Dictionary<string, object> |
|||
{ |
|||
["error"] = $"Invalid JSON: {ex.Message}", |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
content.ContentId = contentId; |
|||
|
|||
ruleJob.Content = serializer.Serialize(content, true); |
|||
} |
|||
|
|||
return (ruleText, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(ElasticSearchJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(job.ServerHost)) |
|||
{ |
|||
return Result.Ignored(); |
|||
} |
|||
|
|||
var client = await clients.GetClientAsync((new Uri(job.ServerHost, UriKind.Absolute), job.ServerUser, job.ServerPassword)); |
|||
|
|||
try |
|||
{ |
|||
if (job.Content != null) |
|||
{ |
|||
var response = await client.IndexAsync<StringResponse>(job.IndexName, job.ContentId, job.Content, ctx: ct); |
|||
|
|||
return Result.SuccessOrFailed(response.OriginalException, response.Body); |
|||
} |
|||
else |
|||
{ |
|||
var response = await client.DeleteAsync<StringResponse>(job.IndexName, job.ContentId, ctx: ct); |
|||
|
|||
return Result.SuccessOrFailed(response.OriginalException, response.Body); |
|||
} |
|||
} |
|||
catch (ElasticsearchClientException ex) |
|||
{ |
|||
return Result.Failed(ex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public sealed class ElasticSearchContent |
|||
{ |
|||
public string ContentId { get; set; } |
|||
|
|||
[JsonExtensionData] |
|||
public Dictionary<string, object> More { get; set; } = []; |
|||
} |
|||
|
|||
public sealed class ElasticSearchJob |
|||
{ |
|||
public string ServerHost { get; set; } |
|||
|
|||
public string? ServerUser { get; set; } |
|||
|
|||
public string? ServerPassword { get; set; } |
|||
|
|||
public string ContentId { get; set; } |
|||
|
|||
public string Content { get; set; } |
|||
|
|||
public string IndexName { get; set; } |
|||
} |
|||
@ -0,0 +1,174 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text.Json.Serialization; |
|||
using Elasticsearch.Net; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.ElasticSearch; |
|||
|
|||
[FlowStep( |
|||
Title = "ElasticSearch", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 28'><path d='M13.427 17.436H4.163C3.827 16.354 3.636 15.2 3.636 14s.182-2.355.527-3.436h15.245c1.891 0 3.418 1.545 3.418 3.445a3.421 3.421 0 0 1-3.418 3.427h-5.982zm-.436 1.146H4.6a11.508 11.508 0 0 0 4.2 4.982 11.443 11.443 0 0 0 15.827-3.209 5.793 5.793 0 0 0-4.173-1.773H12.99zm7.464-9.164a5.794 5.794 0 0 0 4.173-1.773 11.45 11.45 0 0 0-9.536-5.1c-2.327 0-4.491.7-6.3 1.891a11.554 11.554 0 0 0-4.2 4.982h15.864z'/></svg>", |
|||
IconColor = "#1e5470", |
|||
Display = "Populate ElasticSearch index", |
|||
Description = "Populate a full text search index in ElasticSearch.", |
|||
ReadMore = "https://www.elastic.co/")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record ElasticSearchFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[AbsoluteUrl] |
|||
[LocalizedRequired] |
|||
[Display(Name = "Server Url", Description = "The url to the instance or cluster.")] |
|||
[Editor(FlowStepEditor.Url)] |
|||
public Uri Host { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Index Name", Description = "The name of the index.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string IndexName { get; set; } |
|||
|
|||
[Display(Name = "Username", Description = "The optional username.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? Username { get; set; } |
|||
|
|||
[Display(Name = "Password", Description = "The optional password.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? Password { get; set; } |
|||
|
|||
[Display(Name = "Document", Description = "The optional custom document.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression(ExpressionFallback.Event)] |
|||
public string? Document { get; set; } |
|||
|
|||
[Display(Name = "Deletion", Description = "The condition when to delete the document.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? Delete { get; set; } |
|||
|
|||
private static readonly ClientPool<(Uri Host, string? Username, string? Password), ElasticLowLevelClient> Clients = new (key => |
|||
{ |
|||
var config = new ConnectionConfiguration(key.Host); |
|||
|
|||
if (!string.IsNullOrEmpty(key.Username) && !string.IsNullOrWhiteSpace(key.Password)) |
|||
{ |
|||
config = config.BasicAuthentication(key.Username, key.Password); |
|||
} |
|||
|
|||
return new ElasticLowLevelClient(config); |
|||
}); |
|||
|
|||
public override ValueTask PrepareAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
|
|||
if (!@event.ShouldDelete(executionContext, Delete)) |
|||
{ |
|||
Document = null; |
|||
return default; |
|||
} |
|||
|
|||
ElasticSearchContent content; |
|||
try |
|||
{ |
|||
content = executionContext.DeserializeJson<ElasticSearchContent>(Document!); |
|||
content.ContentId = @event.GetOrCreateId().Id; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
content = new ElasticSearchContent |
|||
{ |
|||
More = new Dictionary<string, object> |
|||
{ |
|||
["error"] = $"Invalid JSON: {ex.Message}", |
|||
}, |
|||
ContentId = @event.GetOrCreateId().Id, |
|||
}; |
|||
} |
|||
|
|||
Document = executionContext.SerializeJson(content); |
|||
return default; |
|||
} |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
|
|||
var (id, isGenerated) = @event.GetOrCreateId(); |
|||
if (isGenerated && Document == null) |
|||
{ |
|||
executionContext.LogSkipped("Can only delete content for static identities."); |
|||
return Next(); |
|||
} |
|||
|
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
void HandleResult(StringResponse response, string message) |
|||
{ |
|||
if (response.OriginalException != null) |
|||
{ |
|||
executionContext.Log("Failed with error", response.OriginalException.Message); |
|||
throw response.OriginalException; |
|||
} |
|||
|
|||
var serializer = executionContext.Resolve<IJsonSerializer>(); |
|||
executionContext.Log(message, serializer.Serialize(response, true)); |
|||
} |
|||
|
|||
var client = await Clients.GetClientAsync((Host, Username, Password)); |
|||
if (Document != null) |
|||
{ |
|||
var response = await client.IndexAsync<StringResponse>(IndexName, id, Document, ctx: ct); |
|||
|
|||
HandleResult(response, $"Document with ID '{id}' upserted"); |
|||
} |
|||
else |
|||
{ |
|||
var response = await client.DeleteAsync<StringResponse>(IndexName, id, ctx: ct); |
|||
|
|||
HandleResult(response, $"Document with ID '{id}' deleted"); |
|||
} |
|||
|
|||
return Next(); |
|||
} |
|||
catch (ElasticsearchClientException ex) |
|||
{ |
|||
executionContext.Log("Failed with error", ex.Message); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new ElasticSearchAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
|
|||
private sealed class ElasticSearchContent |
|||
{ |
|||
public string ContentId { get; set; } |
|||
|
|||
[JsonExtensionData] |
|||
public Dictionary<string, object> More { get; set; } = []; |
|||
} |
|||
} |
|||
@ -1,90 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using MailKit.Net.Smtp; |
|||
using MimeKit; |
|||
using MimeKit.Text; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Email; |
|||
|
|||
public sealed class EmailActionHandler(RuleEventFormatter formatter) : RuleActionHandler<EmailAction, EmailJob>(formatter) |
|||
{ |
|||
protected override async Task<(string Description, EmailJob Data)> CreateJobAsync(EnrichedEvent @event, EmailAction action) |
|||
{ |
|||
var ruleJob = new EmailJob |
|||
{ |
|||
ServerHost = action.ServerHost, |
|||
ServerPassword = action.ServerPassword, |
|||
ServerPort = action.ServerPort, |
|||
ServerUsername = (await FormatAsync(action.ServerUsername, @event))!, |
|||
MessageFrom = (await FormatAsync(action.MessageFrom, @event))!, |
|||
MessageTo = (await FormatAsync(action.MessageTo, @event))!, |
|||
MessageSubject = (await FormatAsync(action.MessageSubject, @event))!, |
|||
MessageBody = (await FormatAsync(action.MessageBody, @event))!, |
|||
}; |
|||
|
|||
var description = $"Send an email to {ruleJob.MessageTo}"; |
|||
|
|||
return (description, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(EmailJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
using (var smtpClient = new SmtpClient()) |
|||
{ |
|||
await smtpClient.ConnectAsync(job.ServerHost, job.ServerPort, cancellationToken: ct); |
|||
|
|||
if (!string.IsNullOrWhiteSpace(job.ServerUsername) && !string.IsNullOrWhiteSpace(job.ServerPassword)) |
|||
{ |
|||
await smtpClient.AuthenticateAsync(job.ServerUsername, job.ServerPassword, ct); |
|||
} |
|||
|
|||
var smtpMessage = new MimeMessage(); |
|||
|
|||
smtpMessage.From.Add(MailboxAddress.Parse( |
|||
job.MessageFrom)); |
|||
|
|||
smtpMessage.To.Add(MailboxAddress.Parse( |
|||
job.MessageTo)); |
|||
|
|||
smtpMessage.Body = new TextPart(TextFormat.Html) |
|||
{ |
|||
Text = job.MessageBody, |
|||
}; |
|||
|
|||
smtpMessage.Subject = job.MessageSubject; |
|||
|
|||
await smtpClient.SendAsync(smtpMessage, ct); |
|||
} |
|||
|
|||
return Result.Complete(); |
|||
} |
|||
} |
|||
|
|||
public sealed class EmailJob |
|||
{ |
|||
public int ServerPort { get; set; } |
|||
|
|||
public string ServerHost { get; set; } |
|||
|
|||
public string ServerUsername { get; set; } |
|||
|
|||
public string ServerPassword { get; set; } |
|||
|
|||
public string MessageFrom { get; set; } |
|||
|
|||
public string MessageTo { get; set; } |
|||
|
|||
public string MessageSubject { get; set; } |
|||
|
|||
public string MessageBody { get; set; } |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using MailKit.Net.Smtp; |
|||
using MimeKit; |
|||
using MimeKit.Text; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.Email; |
|||
|
|||
[FlowStep( |
|||
Title = "Email", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M28 5h-24c-2.209 0-4 1.792-4 4v13c0 2.209 1.791 4 4 4h24c2.209 0 4-1.791 4-4v-13c0-2.208-1.791-4-4-4zM2 10.25l6.999 5.25-6.999 5.25v-10.5zM30 22c0 1.104-0.898 2-2 2h-24c-1.103 0-2-0.896-2-2l7.832-5.875 4.368 3.277c0.533 0.398 1.166 0.6 1.8 0.6 0.633 0 1.266-0.201 1.799-0.6l4.369-3.277 7.832 5.875zM30 20.75l-7-5.25 7-5.25v10.5zM17.199 18.602c-0.349 0.262-0.763 0.4-1.199 0.4s-0.851-0.139-1.2-0.4l-12.8-9.602c0-1.103 0.897-2 2-2h24c1.102 0 2 0.897 2 2l-12.801 9.602z'/></svg>", |
|||
IconColor = "#333300", |
|||
Display = "Send an email", |
|||
Description = "Send an email with a custom SMTP server.", |
|||
ReadMore = "https://en.wikipedia.org/wiki/Email")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
internal sealed record EmailFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "Server Host", Description = "The IP address or host to the SMTP server.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string ServerHost { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Server Port", Description = "The port to the SMTP server.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public int ServerPort { get; set; } |
|||
|
|||
[Display(Name = "Username", Description = "The username for the SMTP server.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string ServerUsername { get; set; } |
|||
|
|||
[Display(Name = "Password", Description = "The password for the SMTP server.")] |
|||
[Editor(FlowStepEditor.Password)] |
|||
public string ServerPassword { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "From Address", Description = "The email sending address.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string MessageFrom { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "To Address", Description = "The email message will be sent to.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string MessageTo { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Subject", Description = "The subject line for this email message.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string MessageSubject { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Body", Description = "The message body.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression] |
|||
public string MessageBody { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
using var smtpClient = new SmtpClient(); |
|||
|
|||
await smtpClient.ConnectAsync(ServerHost, ServerPort, cancellationToken: ct); |
|||
|
|||
if (!string.IsNullOrWhiteSpace(ServerUsername) && !string.IsNullOrWhiteSpace(ServerPassword)) |
|||
{ |
|||
await smtpClient.AuthenticateAsync(ServerUsername, ServerPassword, ct); |
|||
} |
|||
|
|||
var smtpMessage = new MimeMessage |
|||
{ |
|||
Body = new TextPart(TextFormat.Html) |
|||
{ |
|||
Text = MessageBody, |
|||
}, |
|||
Subject = MessageSubject, |
|||
}; |
|||
|
|||
smtpMessage.From.Add(MailboxAddress.Parse( |
|||
MessageFrom)); |
|||
|
|||
smtpMessage.To.Add(MailboxAddress.Parse( |
|||
MessageTo)); |
|||
|
|||
await smtpClient.SendAsync(smtpMessage, ct); |
|||
|
|||
executionContext.Log($"Email sent to {MessageTo}"); |
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new EmailAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
} |
|||
@ -1,60 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Infrastructure; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Fastly; |
|||
|
|||
public sealed class FastlyActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) : RuleActionHandler<FastlyAction, FastlyJob>(formatter) |
|||
{ |
|||
private const string Description = "Purge key in fastly"; |
|||
|
|||
protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action) |
|||
{ |
|||
var id = string.Empty; |
|||
|
|||
if (@event is IEnrichedEntityEvent entityEvent) |
|||
{ |
|||
id = DomainId.Combine(@event.AppId.Id, entityEvent.Id).ToString(); |
|||
} |
|||
|
|||
var ruleJob = new FastlyJob |
|||
{ |
|||
Key = id, |
|||
FastlyApiKey = action.ApiKey, |
|||
FastlyServiceID = action.ServiceId, |
|||
}; |
|||
|
|||
return (Description, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(FastlyJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var httpClient = httpClientFactory.CreateClient("FastlyAction"); |
|||
|
|||
var requestUrl = $"/service/{job.FastlyServiceID}/purge/{job.Key}"; |
|||
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); |
|||
|
|||
request.Headers.Add("Fastly-Key", job.FastlyApiKey); |
|||
|
|||
return await httpClient.OneWayRequestAsync(request, ct: ct); |
|||
} |
|||
} |
|||
|
|||
public sealed class FastlyJob |
|||
{ |
|||
public string FastlyApiKey { get; set; } |
|||
|
|||
public string FastlyServiceID { get; set; } |
|||
|
|||
public string Key { get; set; } |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.Fastly; |
|||
|
|||
[FlowStep( |
|||
Title = "Fastly", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 32'><path d='M10.68.948v1.736h.806v2.6A12.992 12.992 0 0 0 .951 18.051c0 7.178 5.775 12.996 12.9 12.996 7.124 0 12.9-5.819 12.9-12.996-.004-6.332-4.502-11.605-10.455-12.755l-.081-.013V2.684h.807V.948H10.68zm3.53 10.605c3.218.173 5.81 2.713 6.09 5.922v.211h-.734v.737h.734v.201c-.279 3.21-2.871 5.752-6.09 5.925v-.723h-.733v.721c-3.281-.192-5.905-2.845-6.077-6.152h.728v-.737h-.724c.195-3.284 2.808-5.911 6.073-6.103v.725h.733v-.727zm2.513 3.051l-2.462 2.282a1.13 1.13 0 0 0-.41-.078c-.633 0-1.147.517-1.147 1.155a1.15 1.15 0 0 0 1.147 1.155c.633 0 1.147-.517 1.147-1.155 0-.117-.018-.23-.05-.337l.002.008 2.223-2.505-.449-.526z'/></svg>", |
|||
IconColor = "#e23335", |
|||
Display = "Purge fastly cache", |
|||
Description = "Remove entries from the fastly CDN cache.", |
|||
ReadMore = "https://www.fastly.com/")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record FastlyFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string ApiKey { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Service Id", Description = "The ID of the fastly service.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string ServiceId { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
|
|||
var id = string.Empty; |
|||
if (@event is IEnrichedEntityEvent entityEvent) |
|||
{ |
|||
id = DomainId.Combine(@event.AppId.Id, entityEvent.Id).ToString(); |
|||
} |
|||
|
|||
var httpClient = |
|||
executionContext.Resolve<IHttpClientFactory>() |
|||
.CreateClient("FastlyAction"); |
|||
|
|||
var requestUrl = $"/service/{ServiceId}/purge/{id}"; |
|||
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl); |
|||
|
|||
request.Headers.Add("Fastly-Key", ApiKey); |
|||
|
|||
var (_, dump) = await httpClient.SendAsync(executionContext, request, ct: ct); |
|||
|
|||
executionContext.Log("Cache invalidated", dump); |
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new FastlyAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
} |
|||
@ -1,117 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
#if INCLUDE_KAFKA
|
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Kafka; |
|||
|
|||
public sealed class KafkaActionHandler(RuleEventFormatter formatter, KafkaProducer kafkaProducer) : RuleActionHandler<KafkaAction, KafkaJob>(formatter) |
|||
{ |
|||
private const string Description = "Push to Kafka"; |
|||
|
|||
protected override async Task<(string Description, KafkaJob Data)> CreateJobAsync(EnrichedEvent @event, KafkaAction action) |
|||
{ |
|||
string? value, key; |
|||
|
|||
if (!string.IsNullOrEmpty(action.Payload)) |
|||
{ |
|||
value = await FormatAsync(action.Payload, @event); |
|||
} |
|||
else |
|||
{ |
|||
value = ToEnvelopeJson(@event); |
|||
} |
|||
|
|||
if (!string.IsNullOrEmpty(action.Key)) |
|||
{ |
|||
key = await FormatAsync(action.Key, @event); |
|||
} |
|||
else |
|||
{ |
|||
key = @event.Name; |
|||
} |
|||
|
|||
var ruleJob = new KafkaJob |
|||
{ |
|||
TopicName = action.TopicName, |
|||
MessageKey = key, |
|||
MessageValue = value, |
|||
Headers = await ParseHeadersAsync(action.Headers, @event), |
|||
Schema = action.Schema, |
|||
PartitionKey = await FormatAsync(action.PartitionKey, @event), |
|||
PartitionCount = action.PartitionCount, |
|||
}; |
|||
|
|||
return (Description, ruleJob); |
|||
} |
|||
|
|||
private async Task<Dictionary<string, string>?> ParseHeadersAsync(string? headers, EnrichedEvent @event) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(headers)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var headersDictionary = new Dictionary<string, string>(); |
|||
|
|||
var lines = headers.Split('\n'); |
|||
|
|||
foreach (var line in lines) |
|||
{ |
|||
var indexEqual = line.IndexOf('=', StringComparison.Ordinal); |
|||
|
|||
if (indexEqual > 0 && indexEqual < line.Length - 1) |
|||
{ |
|||
var headerKey = line[..indexEqual]; |
|||
var headerValue = line[(indexEqual + 1)..]; |
|||
|
|||
headerValue = await FormatAsync(headerValue, @event); |
|||
|
|||
headersDictionary[headerKey] = headerValue!; |
|||
} |
|||
} |
|||
|
|||
return headersDictionary; |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(KafkaJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
try |
|||
{ |
|||
await kafkaProducer.SendAsync(job, ct); |
|||
|
|||
return Result.Success($"Event pushed to {job.TopicName} kafka topic with {job.MessageKey} message key."); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
return Result.Failed(ex, $"Push to Kafka failed: {ex}"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public sealed class KafkaJob |
|||
{ |
|||
public string TopicName { get; set; } |
|||
|
|||
public string? MessageKey { get; set; } |
|||
|
|||
public string? MessageValue { get; set; } |
|||
|
|||
public string? Schema { get; set; } |
|||
|
|||
public string? PartitionKey { get; set; } |
|||
|
|||
public Dictionary<string, string>? Headers { get; set; } |
|||
|
|||
public int PartitionCount { get; set; } |
|||
} |
|||
#endif
|
|||
@ -0,0 +1,138 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
#if INCLUDE_KAFKA
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.Kafka; |
|||
|
|||
[FlowStep( |
|||
Title = "Kafka", |
|||
IconImage = "<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 1000 1000' enable-background='new 0 0 1000 1000' xml:space='preserve'><g><path d = 'M674.2,552.7c-38.2,0-72.4,17-95.9,43.6l-60.1-42.5c6.5-17.4,10.1-36.4,10.1-56.1c0-19.5-3.6-38-9.6-55.2l59.9-42c23.5,26.4,57.5,43.4,95.7,43.4c70.4,0,127.7-57.2,127.7-127.7c0-70.4-57.2-127.7-127.7-127.7c-70.4,0-127.7,57.2-127.7,127.7c0,12.5,2,24.8,5.4,36.2l-60.1,42c-25-31.1-61.3-52.8-102.2-59.5v-72.2c57.9-12.3,101.5-63.7,101.5-125C491.1,67.2,433.8,10,363.4,10S235.7,67.2,235.7,137.7c0,60.6,42.5,111.3,99.3,124.5v73.1c-77.8,13.4-136.8,80.9-136.8,162.3c0,81.6,59.7,149.4,137.5,162.5v77.4c-57.2,12.5-100.4,63.7-100.4,124.8c0,70.4,57.2,127.7,127.7,127.7c70.4,0,128.1-57.2,128.1-127.9c0-61-43.2-112.2-100.4-124.8V660c40.2-6.7,75.6-27.9,100.4-58.4l60.4,42.7c-3.4,11.4-5.1,23.5-5.1,36c0,70.4,57.2,127.7,127.7,127.7c70.4,0,127.7-57.2,127.7-127.7C801.6,609.9,744.6,552.7,674.2,552.7L674.2,552.7z M674.2,253.9c34.2,0,61.9,27.7,61.9,61.9c0,34.2-27.7,61.9-61.9,61.9c-34.2,0-62.2-27.7-62.2-61.9C612,281.7,640,253.9,674.2,253.9L674.2,253.9z M301.2,137.7c0-34.2,27.7-61.9,61.9-61.9c34.2,0,61.9,27.7,61.9,61.9s-27.7,61.9-61.9,61.9C329,199.6,301.2,171.7,301.2,137.7L301.2,137.7z M425.1,862.1c0,34.2-27.7,61.9-61.9,61.9c-34.2,0-61.9-27.7-61.9-61.9c0-34.2,27.7-61.9,61.9-61.9C397.4,800.2,425.1,828.1,425.1,862.1L425.1,862.1z M363.2,584c-47.6,0-86.3-38.7-86.3-86.3c0-47.6,38.7-86.3,86.3-86.3c47.6,0,86.3,38.7,86.3,86.3C449.7,545.3,410.8,584,363.2,584L363.2,584z M674.2,742.5c-34.2,0-61.9-27.7-61.9-61.9c0-34.2,27.7-61.9,61.9-61.9c34.2,0,61.9,27.7,61.9,61.9C736.1,714.8,708.2,742.5,674.2,742.5L674.2,742.5z'/></g></svg>", |
|||
IconColor = "#404244", |
|||
Display = "Push to kafka", |
|||
Description = "Connect to Kafka stream and push data to that stream.", |
|||
ReadMore = "https://kafka.apache.org/quickstart")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record KafkaFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "Topic Name", Description = "The name of the topic.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string TopicName { get; set; } |
|||
|
|||
[Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression(ExpressionFallback.Envelope)] |
|||
public string? Payload { get; set; } |
|||
|
|||
[Display(Name = "Key", Description = "The message key, commonly used for partitioning.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string? Key { get; set; } |
|||
|
|||
[Display(Name = "Partition Key", Description = "The partition key, only used when we don't want to define partiontionig with key.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string? PartitionKey { get; set; } |
|||
|
|||
[Display(Name = "Partition Count", Description = "Define the number of partitions for specific topic.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public int PartitionCount { get; set; } |
|||
|
|||
[Display(Name = "Headers (Optional)", Description = "The message headers in the format '[Key]=[Value]', one entry per line.")]
|
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression] |
|||
public string? Headers { get; set; } |
|||
|
|||
[Display(Name = "Schema (Optional)", Description = "Define a specific AVRO schema in JSON format.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
public string? Schema { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
|
|||
var key = Key; |
|||
if (string.IsNullOrWhiteSpace(key)) |
|||
{ |
|||
key = @event.Name; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var request = new KafkaMessageRequest |
|||
{ |
|||
Headers = ParseHeaders(Headers), |
|||
MessageKey = key, |
|||
MessageValue = Payload, |
|||
PartitionCount = PartitionCount, |
|||
PartitionKey = PartitionKey, |
|||
Schema = Schema, |
|||
TopicName = TopicName, |
|||
}; |
|||
|
|||
await executionContext.Resolve<KafkaProducer>() |
|||
.SendAsync(request, ct); |
|||
|
|||
executionContext.Log($"Event pushed to {TopicName} kafka topic with '{key}' message key."); |
|||
return Next(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
executionContext.Log("Push to kafka failed", ex.Message); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
private static Dictionary<string, string>? ParseHeaders(string? headers) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(headers)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var headersDictionary = new Dictionary<string, string>(); |
|||
|
|||
foreach (var line in headers.Split('\n')) |
|||
{ |
|||
var indexEqual = line.IndexOf('=', StringComparison.Ordinal); |
|||
|
|||
if (indexEqual > 0 && indexEqual < line.Length - 1) |
|||
{ |
|||
var headerKey = line[..indexEqual]; |
|||
var headerValue = line[(indexEqual + 1)..]; |
|||
|
|||
headersDictionary[headerKey] = headerValue!; |
|||
} |
|||
} |
|||
|
|||
return headersDictionary; |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new KafkaAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
} |
|||
#endif
|
|||
@ -0,0 +1,27 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
#if INCLUDE_KAFKA
|
|||
namespace Squidex.Extensions.Actions.Kafka; |
|||
|
|||
public sealed class KafkaMessageRequest |
|||
{ |
|||
public string TopicName { get; set; } |
|||
|
|||
public string? MessageKey { get; set; } |
|||
|
|||
public string? MessageValue { get; set; } |
|||
|
|||
public string? Schema { get; set; } |
|||
|
|||
public string? PartitionKey { get; set; } |
|||
|
|||
public Dictionary<string, string>? Headers { get; set; } |
|||
|
|||
public int PartitionCount { get; set; } |
|||
} |
|||
#endif
|
|||
@ -1,136 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Infrastructure.Http; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Medium; |
|||
|
|||
public sealed class MediumActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory, IJsonSerializer serializer) : RuleActionHandler<MediumAction, MediumJob>(formatter) |
|||
{ |
|||
private const string Description = "Post to medium"; |
|||
|
|||
private sealed class UserResponse |
|||
{ |
|||
public UserResponseData Data { get; set; } |
|||
} |
|||
|
|||
private sealed class UserResponseData |
|||
{ |
|||
public string Id { get; set; } |
|||
} |
|||
|
|||
protected override async Task<(string Description, MediumJob Data)> CreateJobAsync(EnrichedEvent @event, MediumAction action) |
|||
{ |
|||
var ruleJob = new MediumJob { AccessToken = action.AccessToken, PublicationId = action.PublicationId }; |
|||
|
|||
var requestBody = new |
|||
{ |
|||
title = await FormatAsync(action.Title, @event), |
|||
contentFormat = action.IsHtml ? "html" : "markdown", |
|||
content = await FormatAsync(action.Content, @event), |
|||
canonicalUrl = await FormatAsync(action.CanonicalUrl, @event), |
|||
tags = await ParseTagsAsync(@event, action), |
|||
}; |
|||
|
|||
ruleJob.RequestBody = ToJson(requestBody); |
|||
|
|||
return (Description, ruleJob); |
|||
} |
|||
|
|||
private async Task<string[]?> ParseTagsAsync(EnrichedEvent @event, MediumAction action) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(action.Tags)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var jsonTags = await FormatAsync(action.Tags, @event); |
|||
|
|||
return serializer.Deserialize<string[]>(jsonTags!); |
|||
} |
|||
catch |
|||
{ |
|||
return action.Tags.Split(','); |
|||
} |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(MediumJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var httpClient = httpClientFactory.CreateClient("MediumAction"); |
|||
|
|||
string path; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(job.PublicationId)) |
|||
{ |
|||
path = $"/v1/publications/{job.PublicationId}/posts"; |
|||
} |
|||
else |
|||
{ |
|||
HttpResponseMessage? response = null; |
|||
|
|||
var meRequest = BuildGetRequest(job, "/v1/me"); |
|||
try |
|||
{ |
|||
response = await httpClient.SendAsync(meRequest, ct); |
|||
|
|||
var responseString = await response.Content.ReadAsStringAsync(ct); |
|||
var responseJson = serializer.Deserialize<UserResponse>(responseString); |
|||
|
|||
var id = responseJson.Data?.Id; |
|||
|
|||
path = $"/v1/users/{id}/posts"; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
var requestDump = DumpFormatter.BuildDump(meRequest, response, ex.ToString()); |
|||
|
|||
return Result.Failed(ex, requestDump); |
|||
} |
|||
} |
|||
|
|||
return await httpClient.OneWayRequestAsync(BuildPostRequest(job, path), job.RequestBody, ct); |
|||
} |
|||
|
|||
private static HttpRequestMessage BuildPostRequest(MediumJob job, string path) |
|||
{ |
|||
var request = new HttpRequestMessage(HttpMethod.Post, path) |
|||
{ |
|||
Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json"), |
|||
}; |
|||
|
|||
request.Headers.Add("Authorization", $"Bearer {job.AccessToken}"); |
|||
|
|||
return request; |
|||
} |
|||
|
|||
private static HttpRequestMessage BuildGetRequest(MediumJob job, string path) |
|||
{ |
|||
var request = new HttpRequestMessage(HttpMethod.Get, path); |
|||
|
|||
request.Headers.Add("Authorization", $"Bearer {job.AccessToken}"); |
|||
|
|||
return request; |
|||
} |
|||
} |
|||
|
|||
public sealed class MediumJob |
|||
{ |
|||
public string RequestBody { get; set; } |
|||
|
|||
public string? PublicationId { get; set; } |
|||
|
|||
public string AccessToken { get; set; } |
|||
} |
|||
@ -0,0 +1,165 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.Medium; |
|||
|
|||
[FlowStep( |
|||
Title = "Medium", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M3.795 8.48a1.239 1.239 0 0 0-.404-1.045l-2.987-3.6v-.537H9.68l7.171 15.727 6.304-15.727H32v.537l-2.556 2.449a.749.749 0 0 0-.284.717v18a.749.749 0 0 0 .284.716l2.493 2.449v.537H19.39v-.537l2.583-2.509c.253-.253.253-.328.253-.717V10.392l-7.187 18.251h-.969L5.703 10.392v12.232a1.69 1.69 0 0 0 .463 1.404l3.36 4.08v.536H-.001v-.537l3.36-4.08c.36-.371.52-.893.435-1.403V8.48z'/></svg>", |
|||
IconColor = "#00ab6c", |
|||
Display = "Post to Medium", |
|||
Description = "Create a new story or post at medium.", |
|||
ReadMore = "https://medium.com/")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record MediumFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "Access Token", Description = "The self issued access token.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string AccessToken { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Title", Description = "The title, used for the url.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string Title { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Content", Description = "The content, either html or markdown.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression] |
|||
public string Content { get; set; } |
|||
|
|||
[Display(Name = "Canonical Url", Description = "The original home of this content, if it was originally published elsewhere.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string? CanonicalUrl { get; set; } |
|||
|
|||
[Display(Name = "Tags", Description = "The optional comma separated list of tags.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string? Tags { get; set; } |
|||
|
|||
[Display(Name = "Publication Id", Description = "Optional publication id.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? PublicationId { get; set; } |
|||
|
|||
[Display(Name = "Is Html", Description = "Indicates whether the content is markdown or html.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public bool IsHtml { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var httpClient = |
|||
executionContext.Resolve<IHttpClientFactory>() |
|||
.CreateClient("MediumAction"); |
|||
|
|||
string path; |
|||
if (!string.IsNullOrWhiteSpace(PublicationId)) |
|||
{ |
|||
path = $"/v1/publications/{PublicationId}/posts"; |
|||
} |
|||
else |
|||
{ |
|||
var meRequest = BuildGetRequest("/v1/me"); |
|||
|
|||
var (responseString, _) = await httpClient.SendAsync(executionContext, meRequest, null, ct); |
|||
var responseJson = executionContext.DeserializeJson<UserResponse>(responseString); |
|||
|
|||
var id = responseJson.Data?.Id; |
|||
|
|||
path = $"/v1/users/{id}/posts"; |
|||
} |
|||
|
|||
var requestBody = new |
|||
{ |
|||
title = Title, |
|||
contentFormat = IsHtml ? "html" : "markdown", |
|||
content = Content, |
|||
canonicalUrl = CanonicalUrl, |
|||
tags = ParseTags(executionContext), |
|||
}; |
|||
|
|||
var requestJson = executionContext.SerializeJson(requestBody); |
|||
|
|||
var (_, dump) = await httpClient.SendAsync(executionContext, BuildPostRequest(path, requestJson), requestJson, ct); |
|||
|
|||
executionContext.Log("Post created", dump); |
|||
return Next(); |
|||
} |
|||
|
|||
private HttpRequestMessage BuildPostRequest(string path, string body) |
|||
{ |
|||
var request = new HttpRequestMessage(HttpMethod.Post, path) |
|||
{ |
|||
Content = new StringContent(body, Encoding.UTF8, "application/json"), |
|||
}; |
|||
|
|||
request.Headers.Add("Authorization", $"Bearer {AccessToken}"); |
|||
|
|||
return request; |
|||
} |
|||
|
|||
private HttpRequestMessage BuildGetRequest(string path) |
|||
{ |
|||
var request = new HttpRequestMessage(HttpMethod.Get, path); |
|||
|
|||
request.Headers.Add("Authorization", $"Bearer {AccessToken}"); |
|||
|
|||
return request; |
|||
} |
|||
|
|||
private string[]? ParseTags(FlowExecutionContext executionContext) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(Tags)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
return executionContext.DeserializeJson<string[]>(Tags); |
|||
} |
|||
catch |
|||
{ |
|||
return Tags.Split(','); |
|||
} |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new MediumAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
|
|||
private sealed class UserResponse |
|||
{ |
|||
public UserResponseData Data { get; set; } |
|||
} |
|||
|
|||
private sealed class UserResponseData |
|||
{ |
|||
public string Id { get; set; } |
|||
} |
|||
} |
|||
@ -1,72 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Entities.Collaboration; |
|||
using Squidex.Domain.Apps.Events.Comments; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Extensions.Actions.Notification; |
|||
|
|||
public sealed class NotificationActionHandler(RuleEventFormatter formatter, ICollaborationService collaboration, IUserResolver userResolver) : RuleActionHandler<NotificationAction, CommentCreated>(formatter) |
|||
{ |
|||
private const string Description = "Send a Notification"; |
|||
|
|||
protected override async Task<(string Description, CommentCreated Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action) |
|||
{ |
|||
if (@event is not EnrichedUserEventBase userEvent) |
|||
{ |
|||
return ("Ignore", new CommentCreated()); |
|||
} |
|||
|
|||
var user = await userResolver.FindByIdOrEmailAsync(action.User) |
|||
?? throw new InvalidOperationException($"Cannot find user by '{action.User}'"); |
|||
|
|||
var actor = userEvent.Actor; |
|||
|
|||
if (!string.IsNullOrEmpty(action.Client)) |
|||
{ |
|||
actor = RefToken.Client(action.Client); |
|||
} |
|||
|
|||
var ruleJob = new CommentCreated |
|||
{ |
|||
Actor = actor, |
|||
CommentId = DomainId.NewGuid(), |
|||
CommentsId = DomainId.Create(user.Id), |
|||
FromRule = true, |
|||
Text = (await FormatAsync(action.Text, @event))!, |
|||
}; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(action.Url)) |
|||
{ |
|||
var url = await FormatAsync(action.Url, @event); |
|||
|
|||
if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri)) |
|||
{ |
|||
ruleJob.Url = uri; |
|||
} |
|||
} |
|||
|
|||
return (Description, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(CommentCreated job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
if (job.CommentsId == default) |
|||
{ |
|||
return Result.Ignored(); |
|||
} |
|||
|
|||
await collaboration.NotifyAsync(job.CommentsId.ToString(), job.Text, job.Actor, job.Url, true, ct); |
|||
|
|||
return Result.Success($"Notified: {job.Text}"); |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Entities.Collaboration; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Extensions.Actions.Notification; |
|||
|
|||
[FlowStep( |
|||
Title = "Notification", |
|||
IconImage = "<svg version='1.1' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path d='M20.016 15.984v-12h-16.031v14.016l2.016-2.016h14.016zM20.016 2.016c1.078 0 1.969 0.891 1.969 1.969v12c0 1.078-0.891 2.016-1.969 2.016h-14.016l-3.984 3.984v-18c0-1.078 0.891-1.969 1.969-1.969h16.031z'></path></svg>", |
|||
IconColor = "#3389ff", |
|||
Display = "Send a notification", |
|||
Description = "Send an integrated notification to a user.")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record NotificationFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "User", Description = "The user id or email.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string User { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Title", Description = "The text to send.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression] |
|||
public string Text { get; set; } |
|||
|
|||
[Display(Name = "Url", Description = "The optional url to attach to the notification.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string? Url { get; set; } |
|||
|
|||
[Display(Name = "Client", Description = "An optional client name.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? Client { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
if (@event is not EnrichedUserEventBase userEvent) |
|||
{ |
|||
executionContext.LogSkipped("Not an event with user information"); |
|||
return Next(); |
|||
} |
|||
|
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var user = |
|||
await executionContext.Resolve<IUserResolver>() |
|||
.FindByIdOrEmailAsync(User, ct) |
|||
?? throw new InvalidOperationException($"Cannot find user by '{User}'"); |
|||
|
|||
var actor = userEvent.Actor; |
|||
if (!string.IsNullOrEmpty(Client)) |
|||
{ |
|||
actor = RefToken.Client(Client); |
|||
} |
|||
|
|||
Uri? url = null; |
|||
if (!string.IsNullOrWhiteSpace(Url) && !Uri.TryCreate(Url, UriKind.RelativeOrAbsolute, out url)) |
|||
{ |
|||
executionContext.Log($"Invalid URL: {Url}"); |
|||
} |
|||
|
|||
await executionContext.Resolve<ICollaborationService>() |
|||
.NotifyAsync(user.Id, Text, actor, url, true, ct); |
|||
|
|||
executionContext.Log("Notified", Text); |
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new NotificationAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
} |
|||
@ -1,157 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text.Json.Serialization; |
|||
using OpenSearch.Net; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
#pragma warning disable IDE0059 // Value assigned to symbol is never used
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.OpenSearch; |
|||
|
|||
public sealed class OpenSearchActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer) : RuleActionHandler<OpenSearchAction, OpenSearchJob>(formatter) |
|||
{ |
|||
private readonly ClientPool<(Uri Host, string? Username, string? Password), OpenSearchLowLevelClient> clients = new ClientPool<(Uri Host, string? Username, string? Password), OpenSearchLowLevelClient>(key => |
|||
{ |
|||
var config = new ConnectionConfiguration(key.Host); |
|||
|
|||
if (!string.IsNullOrEmpty(key.Username) && !string.IsNullOrWhiteSpace(key.Password)) |
|||
{ |
|||
config = config.BasicAuthentication(key.Username, key.Password); |
|||
} |
|||
|
|||
return new OpenSearchLowLevelClient(config); |
|||
}); |
|||
|
|||
protected override async Task<(string Description, OpenSearchJob Data)> CreateJobAsync(EnrichedEvent @event, OpenSearchAction action) |
|||
{ |
|||
var delete = @event.ShouldDelete(scriptEngine, action.Delete); |
|||
|
|||
string contentId; |
|||
|
|||
if (@event is IEnrichedEntityEvent enrichedEntityEvent) |
|||
{ |
|||
contentId = enrichedEntityEvent.Id.ToString(); |
|||
} |
|||
else |
|||
{ |
|||
contentId = DomainId.NewGuid().ToString(); |
|||
} |
|||
|
|||
var ruleText = string.Empty; |
|||
var ruleJob = new OpenSearchJob |
|||
{ |
|||
IndexName = (await FormatAsync(action.IndexName, @event))!, |
|||
ServerHost = action.Host.ToString(), |
|||
ServerUser = action.Username, |
|||
ServerPassword = action.Password, |
|||
ContentId = contentId, |
|||
}; |
|||
|
|||
if (delete) |
|||
{ |
|||
ruleText = $"Delete entry index: {ruleJob.IndexName}"; |
|||
} |
|||
else |
|||
{ |
|||
ruleText = $"Upsert to index: {ruleJob.IndexName}"; |
|||
|
|||
OpenSearchContent content; |
|||
try |
|||
{ |
|||
string? jsonString; |
|||
|
|||
if (!string.IsNullOrEmpty(action.Document)) |
|||
{ |
|||
jsonString = await FormatAsync(action.Document, @event); |
|||
jsonString = jsonString?.Trim(); |
|||
} |
|||
else |
|||
{ |
|||
jsonString = ToJson(@event); |
|||
} |
|||
|
|||
content = serializer.Deserialize<OpenSearchContent>(jsonString!); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
content = new OpenSearchContent |
|||
{ |
|||
More = new Dictionary<string, object> |
|||
{ |
|||
["error"] = $"Invalid JSON: {ex.Message}", |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
content.ContentId = contentId; |
|||
|
|||
ruleJob.Content = serializer.Serialize(content, true); |
|||
} |
|||
|
|||
return (ruleText, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(OpenSearchJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(job.ServerHost)) |
|||
{ |
|||
return Result.Ignored(); |
|||
} |
|||
|
|||
var client = await clients.GetClientAsync((new Uri(job.ServerHost, UriKind.Absolute), job.ServerUser, job.ServerPassword)); |
|||
|
|||
try |
|||
{ |
|||
if (job.Content != null) |
|||
{ |
|||
var response = await client.IndexAsync<StringResponse>(job.IndexName, job.ContentId, job.Content, ctx: ct); |
|||
|
|||
return Result.SuccessOrFailed(response.OriginalException, response.Body); |
|||
} |
|||
else |
|||
{ |
|||
var response = await client.DeleteAsync<StringResponse>(job.IndexName, job.ContentId, ctx: ct); |
|||
|
|||
return Result.SuccessOrFailed(response.OriginalException, response.Body); |
|||
} |
|||
} |
|||
catch (OpenSearchClientException ex) |
|||
{ |
|||
return Result.Failed(ex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public sealed class OpenSearchContent |
|||
{ |
|||
public string ContentId { get; set; } |
|||
|
|||
[JsonExtensionData] |
|||
public Dictionary<string, object> More { get; set; } = []; |
|||
} |
|||
|
|||
public sealed class OpenSearchJob |
|||
{ |
|||
public string ServerHost { get; set; } |
|||
|
|||
public string? ServerUser { get; set; } |
|||
|
|||
public string? ServerPassword { get; set; } |
|||
|
|||
public string ContentId { get; set; } |
|||
|
|||
public string Content { get; set; } |
|||
|
|||
public string IndexName { get; set; } |
|||
} |
|||
@ -0,0 +1,174 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text.Json.Serialization; |
|||
using OpenSearch.Net; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.OpenSearch; |
|||
|
|||
[FlowStep( |
|||
Title = "OpenSearch", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><path d='M61.737 23.5a2.263 2.263 0 0 0-2.262 2.263c0 18.618-15.094 33.712-33.712 33.712a2.263 2.263 0 1 0 0 4.525C46.88 64 64 46.88 64 25.763a2.263 2.263 0 0 0-2.263-2.263Z' fill='#fff'/><path d='M48.081 38c2.176-3.55 4.28-8.282 3.866-14.908C51.09 9.367 38.66-1.045 26.921.084c-4.596.441-9.314 4.187-8.895 10.896.182 2.916 1.61 4.637 3.928 5.96 2.208 1.26 5.044 2.057 8.259 2.961 3.883 1.092 8.388 2.32 11.85 4.87 4.15 3.058 6.986 6.603 6.018 13.229Z' fill='#fff'/><path d='M3.919 14C1.743 17.55-.361 22.282.052 28.908.91 42.633 13.342 53.045 25.08 51.916c4.596-.441 9.314-4.187 8.895-10.896-.182-2.916-1.61-4.637-3.928-5.96-2.208-1.26-5.044-2.057-8.259-2.961-3.883-1.092-8.388-2.32-11.85-4.87C5.787 24.17 2.95 20.625 3.919 14Z' fill='#fff'/></svg>", |
|||
IconColor = "#005EB8", |
|||
Display = "Populate OpenSearch index", |
|||
Description = "Populate a full text search index in OpenSearch.", |
|||
ReadMore = "https://opensearch.org/")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record OpenSearchFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[AbsoluteUrl] |
|||
[LocalizedRequired] |
|||
[Display(Name = "Server Url", Description = "The url to the instance or cluster.")] |
|||
[Editor(FlowStepEditor.Url)] |
|||
public Uri Host { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Index Name", Description = "The name of the index.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string IndexName { get; set; } |
|||
|
|||
[Display(Name = "Username", Description = "The optional username.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? Username { get; set; } |
|||
|
|||
[Display(Name = "Password", Description = "The optional password.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? Password { get; set; } |
|||
|
|||
[Display(Name = "Document", Description = "The optional custom document.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression(ExpressionFallback.Event)] |
|||
public string? Document { get; set; } |
|||
|
|||
[Display(Name = "Deletion", Description = "The condition when to delete the document.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? Delete { get; set; } |
|||
|
|||
private static readonly ClientPool<(Uri Host, string? Username, string? Password), OpenSearchLowLevelClient> Clients = new (key => |
|||
{ |
|||
var config = new ConnectionConfiguration(key.Host); |
|||
|
|||
if (!string.IsNullOrEmpty(key.Username) && !string.IsNullOrWhiteSpace(key.Password)) |
|||
{ |
|||
config = config.BasicAuthentication(key.Username, key.Password); |
|||
} |
|||
|
|||
return new OpenSearchLowLevelClient(config); |
|||
}); |
|||
|
|||
public override ValueTask PrepareAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
|
|||
if (@event.ShouldDelete(executionContext, Delete)) |
|||
{ |
|||
OpenSearchContent content; |
|||
try |
|||
{ |
|||
content = executionContext.DeserializeJson<OpenSearchContent>(Document!); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
content = new OpenSearchContent |
|||
{ |
|||
More = new Dictionary<string, object> |
|||
{ |
|||
["error"] = $"Invalid JSON: {ex.Message}", |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
Document = executionContext.SerializeJson(content); |
|||
} |
|||
else |
|||
{ |
|||
Document = null; |
|||
} |
|||
|
|||
return base.PrepareAsync(executionContext, ct); |
|||
} |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
|
|||
var (id, isGenerated) = @event.GetOrCreateId(); |
|||
if (isGenerated && Document == null) |
|||
{ |
|||
executionContext.LogSkipped("Can only delete content for static identities."); |
|||
return Next(); |
|||
} |
|||
|
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
void HandleResult(StringResponse response, string message) |
|||
{ |
|||
if (response.OriginalException != null) |
|||
{ |
|||
executionContext.Log("Failed with error", response.OriginalException.Message); |
|||
throw response.OriginalException; |
|||
} |
|||
|
|||
var serializer = executionContext.Resolve<IJsonSerializer>(); |
|||
executionContext.Log(message, serializer.Serialize(response, true)); |
|||
} |
|||
|
|||
var client = await Clients.GetClientAsync((Host, Username, Password)); |
|||
if (Document != null) |
|||
{ |
|||
var response = await client.IndexAsync<StringResponse>(IndexName, id, Document, ctx: ct); |
|||
|
|||
HandleResult(response, $"Document with ID '{id}' upserted"); |
|||
} |
|||
else |
|||
{ |
|||
var response = await client.DeleteAsync<StringResponse>(IndexName, id, ctx: ct); |
|||
|
|||
HandleResult(response, $"Document with ID '{id}' deleted"); |
|||
} |
|||
|
|||
return Next(); |
|||
} |
|||
catch (OpenSearchClientException ex) |
|||
{ |
|||
executionContext.Log("Failed with error", ex.Message); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new OpenSearchAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
|
|||
private sealed class OpenSearchContent |
|||
{ |
|||
public string ContentId { get; set; } |
|||
|
|||
[JsonExtensionData] |
|||
public Dictionary<string, object> More { get; set; } = []; |
|||
} |
|||
} |
|||
@ -1,45 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Prerender; |
|||
|
|||
public sealed class PrerenderActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) : RuleActionHandler<PrerenderAction, PrerenderJob>(formatter) |
|||
{ |
|||
protected override async Task<(string Description, PrerenderJob Data)> CreateJobAsync(EnrichedEvent @event, PrerenderAction action) |
|||
{ |
|||
var url = await FormatAsync(action.Url, @event); |
|||
|
|||
var requestObject = new { prerenderToken = action.Token, url }; |
|||
var requestBody = ToJson(requestObject); |
|||
|
|||
return ($"Recache {url}", new PrerenderJob { RequestBody = requestBody }); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(PrerenderJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var httpClient = httpClientFactory.CreateClient("Prerender"); |
|||
|
|||
var request = new HttpRequestMessage(HttpMethod.Post, "/recache") |
|||
{ |
|||
Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json"), |
|||
}; |
|||
|
|||
return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); |
|||
} |
|||
} |
|||
|
|||
public sealed class PrerenderJob |
|||
{ |
|||
public string RequestBody { get; set; } |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.Prerender; |
|||
|
|||
[FlowStep( |
|||
Title = "Prerender", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M2.073 17.984l8.646-5.36v-1.787L.356 17.325v1.318l10.363 6.488v-1.787zM29.927 17.984l-8.646-5.36v-1.787l10.363 6.488v1.318l-10.363 6.488v-1.787zM18.228 6.693l-6.276 19.426 1.656.548 6.276-19.426z'/></svg>", |
|||
IconColor = "#2c3e50", |
|||
Display = "Recache URL", |
|||
Description = "Prerender a javascript website for bots.", |
|||
ReadMore = "https://prerender.io")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record PrerenderFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "Token", Description = "The prerender token from your account.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string Token { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Url", Description = "The url to recache.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string Url { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var requestObject = new { prerenderToken = Token, Url }; |
|||
var requestBody = executionContext.SerializeJson(requestObject); |
|||
|
|||
var request = new HttpRequestMessage(HttpMethod.Post, "/recache") |
|||
{ |
|||
Content = new StringContent(requestBody, Encoding.UTF8, "application/json"), |
|||
}; |
|||
|
|||
var httpClient = |
|||
executionContext.Resolve<IHttpClientFactory>() |
|||
.CreateClient("Prerender"); |
|||
|
|||
var (_, dump) = await httpClient.SendAsync(executionContext, request, requestBody, ct); |
|||
|
|||
executionContext.Log("Success", dump); |
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new PrerenderAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
} |
|||
@ -1,65 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Security.Claims; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Shared; |
|||
using Squidex.Shared.Identity; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Script; |
|||
|
|||
public sealed class ScriptActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine) : RuleActionHandler<ScriptAction, ScriptJob>(formatter) |
|||
{ |
|||
protected override Task<(string Description, ScriptJob Data)> CreateJobAsync(EnrichedEvent @event, ScriptAction action) |
|||
{ |
|||
var job = new ScriptJob { Script = action.Script, Event = @event }; |
|||
|
|||
return Task.FromResult(($"Run a script", job)); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(ScriptJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
// Script vars are just wrappers over dictionaries for better performance.
|
|||
var vars = new EventScriptVars |
|||
{ |
|||
Event = job.Event, |
|||
AppId = job.Event.AppId.Id, |
|||
AppName = job.Event.AppId.Name, |
|||
}; |
|||
|
|||
if (job.Event is EnrichedUserEventBase) |
|||
{ |
|||
vars.User = AllPrinicpal(); |
|||
} |
|||
|
|||
var result = await scriptEngine.ExecuteAsync(vars, job.Script, ct: ct); |
|||
|
|||
return Result.Success(result.ToString()); |
|||
} |
|||
|
|||
private static ClaimsPrincipal AllPrinicpal() |
|||
{ |
|||
var claimsIdentity = new ClaimsIdentity(); |
|||
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); |
|||
|
|||
claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.All)); |
|||
|
|||
return claimsPrincipal; |
|||
} |
|||
} |
|||
|
|||
public sealed class ScriptJob |
|||
{ |
|||
public EnrichedEvent Event { get; set; } |
|||
|
|||
public string Script { get; set; } |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
|
|||
namespace Squidex.Extensions.Actions.Script; |
|||
|
|||
[FlowStep( |
|||
Title = "Script", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24'><path d='M300-360q-25 0-42.5-17.5T240-420v-40h60v40h60v-180h60v180q0 25-17.5 42.5T360-360h-60zm220 0q-17 0-28.5-11.5T480-400v-40h60v20h80v-40H520q-17 0-28.5-11.5T480-500v-60q0-17 11.5-28.5T520-600h120q17 0 28.5 11.5T680-560v40h-60v-20h-80v40h100q17 0 28.5 11.5T680-460v60q0 17-11.5 28.5T640-360H520z'/></svg>", |
|||
IconColor = "#3389ff", |
|||
Display = "Execute a script", |
|||
Description = "Execute custom code in Javascript.")] |
|||
[NoRetry] |
|||
public sealed record ScriptFlowStep : FlowStep, IConvertibleToAction |
|||
{ |
|||
[Script] |
|||
[Display(Name = "Script", Description = "The script to execute.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
public string? Script { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(Script)) |
|||
{ |
|||
await executionContext.RenderAsync($"Script({Script})", executionContext.Context); |
|||
} |
|||
|
|||
return Next(); |
|||
} |
|||
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new ScriptAction()); |
|||
} |
|||
} |
|||
@ -1,103 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.AspNetCore.SignalR; |
|||
using Microsoft.Azure.SignalR.Management; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.SignalR; |
|||
|
|||
public sealed class SignalRActionHandler(RuleEventFormatter formatter) : RuleActionHandler<SignalRAction, SignalRJob>(formatter) |
|||
{ |
|||
private readonly ClientPool<(string ConnectionString, string HubName), ServiceManager> clients = new ClientPool<(string ConnectionString, string HubName), ServiceManager>(key => |
|||
{ |
|||
var serviceManager = new ServiceManagerBuilder() |
|||
.WithOptions(option => |
|||
{ |
|||
option.ConnectionString = key.ConnectionString; |
|||
option.ServiceTransportType = ServiceTransportType.Transient; |
|||
}) |
|||
.BuildServiceManager(); |
|||
|
|||
return serviceManager; |
|||
}); |
|||
|
|||
protected override async Task<(string Description, SignalRJob Data)> CreateJobAsync(EnrichedEvent @event, SignalRAction action) |
|||
{ |
|||
var hubName = await FormatAsync(action.HubName, @event); |
|||
|
|||
string? requestBody; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(action.Payload)) |
|||
{ |
|||
requestBody = await FormatAsync(action.Payload, @event); |
|||
} |
|||
else |
|||
{ |
|||
requestBody = ToEnvelopeJson(@event); |
|||
} |
|||
|
|||
var target = (await FormatAsync(action.Target, @event)) ?? string.Empty; |
|||
|
|||
var ruleText = $"Send SignalRJob to signalR hub '{hubName}'"; |
|||
var ruleJob = new SignalRJob |
|||
{ |
|||
Action = action.Action, |
|||
ConnectionString = action.ConnectionString, |
|||
HubName = hubName!, |
|||
MethodName = action.MethodName, |
|||
MethodPayload = requestBody!, |
|||
Targets = target.Split("\n"), |
|||
}; |
|||
|
|||
return (ruleText, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(SignalRJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var signalR = await clients.GetClientAsync((job.ConnectionString, job.HubName)); |
|||
|
|||
await using (var signalRContext = await signalR.CreateHubContextAsync(job.HubName, cancellationToken: ct)) |
|||
{ |
|||
var methodeName = !string.IsNullOrWhiteSpace(job.MethodName) ? job.MethodName : "push"; |
|||
|
|||
switch (job.Action) |
|||
{ |
|||
case ActionTypeEnum.Broadcast: |
|||
await signalRContext.Clients.All.SendAsync(methodeName, job.MethodPayload, ct); |
|||
break; |
|||
case ActionTypeEnum.User: |
|||
await signalRContext.Clients.Users(job.Targets).SendAsync(methodeName, job.MethodPayload, ct); |
|||
break; |
|||
case ActionTypeEnum.Group: |
|||
await signalRContext.Clients.Groups(job.Targets).SendAsync(methodeName, job.MethodPayload, ct); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return Result.Complete(); |
|||
} |
|||
} |
|||
|
|||
public sealed class SignalRJob |
|||
{ |
|||
public string ConnectionString { get; set; } |
|||
|
|||
public string HubName { get; set; } |
|||
|
|||
public ActionTypeEnum Action { get; set; } |
|||
|
|||
public string? MethodName { get; set; } |
|||
|
|||
public string MethodPayload { get; set; } |
|||
|
|||
public string[] Targets { get; set; } |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text.RegularExpressions; |
|||
using Microsoft.AspNetCore.SignalR; |
|||
using Microsoft.Azure.SignalR.Management; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.SignalR; |
|||
|
|||
[FlowStep( |
|||
Title = "Azure SignalR", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M.011 16L0 6.248l12-1.63V16zM14 4.328L29.996 2v14H14zM30 18l-.004 14L14 29.75V18zM12 29.495L.01 27.851.009 18H12z'/></svg>", |
|||
IconColor = "#1566BF", |
|||
Display = "Send to Azure SignalR", |
|||
Description = "Send a message to Azure SignalR.", |
|||
ReadMore = "https://azure.microsoft.com/en-en/services/signalr-service/")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed partial record SignalRFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "Connection", Description = "The connection string to the Azure SignalR.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string ConnectionString { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Hub Name", Description = "The name of the hub.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string HubName { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Action", Description = "* Broadcast = send to all users.\n * User = send to all target users(s).\n * Group = send to all target group(s).")] |
|||
public SignalRActionType Action { get; set; } |
|||
|
|||
[Display(Name = "Methode Name", Description = "Set the Name of the hub method received by the customer.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? MethodName { get; set; } |
|||
|
|||
[Display(Name = "Target (Optional)", Description = "Define target users or groups by id or name. One item per line. Not needed for Broadcast action.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression] |
|||
public string? Target { get; set; } |
|||
|
|||
[Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression(ExpressionFallback.Envelope)] |
|||
public string? Payload { get; set; } |
|||
|
|||
private static readonly ClientPool<(string ConnectionString, string HubName), ServiceManager> Clients = new (key => |
|||
{ |
|||
var serviceManager = new ServiceManagerBuilder() |
|||
.WithOptions(option => |
|||
{ |
|||
option.ConnectionString = key.ConnectionString; |
|||
option.ServiceTransportType = ServiceTransportType.Transient; |
|||
}) |
|||
.BuildServiceManager(); |
|||
|
|||
return serviceManager; |
|||
}); |
|||
|
|||
public override ValueTask ValidateAsync(FlowValidationContext validationContext, AddStepError addError, |
|||
CancellationToken ct) |
|||
{ |
|||
if (HubName != null && !HubNameRegex().IsMatch(HubName)) |
|||
{ |
|||
addError(nameof(HubName), "Hub must be valid azure hub name."); |
|||
} |
|||
|
|||
if (Action != SignalRActionType.Broadcast && string.IsNullOrWhiteSpace(Target)) |
|||
{ |
|||
addError(nameof(HubName), "Hub must be valid azure hub name."); |
|||
} |
|||
|
|||
return default; |
|||
} |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var signalR = await Clients.GetClientAsync((ConnectionString, HubName)); |
|||
|
|||
var targets = Target?.Split("\n") ?? []; |
|||
|
|||
await using (var signalRContext = await signalR.CreateHubContextAsync(HubName, ct)) |
|||
{ |
|||
var methodeName = !string.IsNullOrWhiteSpace(MethodName) ? MethodName : "push"; |
|||
|
|||
switch (Action) |
|||
{ |
|||
case SignalRActionType.Broadcast: |
|||
await signalRContext.Clients.All.SendAsync(methodeName, Payload, ct); |
|||
break; |
|||
case SignalRActionType.User: |
|||
await signalRContext.Clients.Users(targets).SendAsync(methodeName, Payload, ct); |
|||
break; |
|||
case SignalRActionType.Group: |
|||
await signalRContext.Clients.Groups(targets).SendAsync(methodeName, Payload, ct); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new SignalRAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
|
|||
[GeneratedRegex("^[a-z][a-z0-9]{2,}(\\-[a-z0-9]+)*$")]
|
|||
private static partial Regex HubNameRegex(); |
|||
} |
|||
@ -1,52 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Slack; |
|||
|
|||
public sealed class SlackActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) : RuleActionHandler<SlackAction, SlackJob>(formatter) |
|||
{ |
|||
private const string Description = "Send message to slack"; |
|||
|
|||
protected override async Task<(string Description, SlackJob Data)> CreateJobAsync(EnrichedEvent @event, SlackAction action) |
|||
{ |
|||
var body = new { text = await FormatAsync(action.Text, @event) }; |
|||
|
|||
var ruleJob = new SlackJob |
|||
{ |
|||
RequestUrl = action.WebhookUrl.ToString(), |
|||
RequestBody = ToJson(body), |
|||
}; |
|||
|
|||
return (Description, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(SlackJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var httpClient = httpClientFactory.CreateClient("SlackAction"); |
|||
|
|||
var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) |
|||
{ |
|||
Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json"), |
|||
}; |
|||
|
|||
return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); |
|||
} |
|||
} |
|||
|
|||
public sealed class SlackJob |
|||
{ |
|||
public string RequestUrl { get; set; } |
|||
|
|||
public string RequestBody { get; set; } |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text; |
|||
using Google.Apis.Json; |
|||
using Migrations.OldActions; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.Slack; |
|||
|
|||
[FlowStep( |
|||
Title = "Slack", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 28'><path d='M23.734 12.125c1.281 0 2.266.938 2.266 2.219 0 1-.516 1.703-1.453 2.031l-2.688.922.875 2.609c.078.234.109.484.109.734 0 1.234-1 2.266-2.234 2.266a2.271 2.271 0 0 1-2.172-1.547l-.859-2.578-4.844 1.656.859 2.562c.078.234.125.484.125.734 0 1.219-1 2.266-2.25 2.266a2.25 2.25 0 0 1-2.156-1.547l-.859-2.547-2.391.828c-.25.078-.516.141-.781.141-1.266 0-2.219-.938-2.219-2.203 0-.969.625-1.844 1.547-2.156l2.438-.828-1.641-4.891-2.438.844c-.25.078-.5.125-.75.125-1.25 0-2.219-.953-2.219-2.203 0-.969.625-1.844 1.547-2.156l2.453-.828-.828-2.484a2.337 2.337 0 0 1-.125-.734c0-1.234 1-2.266 2.25-2.266a2.25 2.25 0 0 1 2.156 1.547l.844 2.5L13.14 5.5 12.296 3a2.337 2.337 0 0 1-.125-.734c0-1.234 1.016-2.266 2.25-2.266.984 0 1.859.625 2.172 1.547l.828 2.516 2.531-.859c.219-.063.438-.094.672-.094 1.219 0 2.266.906 2.266 2.156 0 .969-.75 1.781-1.625 2.078l-2.453.844 1.641 4.937 2.562-.875a2.32 2.32 0 0 1 .719-.125zm-12.406 4.094l4.844-1.641-1.641-4.922-4.844 1.672z'/></svg>", |
|||
IconColor = "#5c3a58", |
|||
Display = "Send to Slack", |
|||
Description = "Create a status update to a slack channel.", |
|||
ReadMore = "https://slack.com")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record SlackFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[AbsoluteUrl] |
|||
[LocalizedRequired] |
|||
[Display(Name = "Webhook Url", Description = "The slack webhook url.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public Uri WebhookUrl { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Text", Description = "The text that is sent as message to slack.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression] |
|||
public string Text { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var body = new { text = Text }; |
|||
|
|||
var jsonRequest = executionContext.SerializeJson(body); |
|||
|
|||
var request = new HttpRequestMessage(HttpMethod.Post, WebhookUrl) |
|||
{ |
|||
Content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"), |
|||
}; |
|||
|
|||
var httpClient = |
|||
executionContext.Resolve<IHttpClientFactory>() |
|||
.CreateClient("SlackAction"); |
|||
|
|||
var (_, dump) = await httpClient.SendAsync(executionContext, request, jsonRequest, ct); |
|||
|
|||
executionContext.Log("Notification sent", dump); |
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new SlackAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
} |
|||
@ -1,62 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using CoreTweet; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Twitter; |
|||
|
|||
public sealed class TweetActionHandler(RuleEventFormatter formatter, IOptions<TwitterOptions> twitterOptions) : RuleActionHandler<TweetAction, TweetJob>(formatter) |
|||
{ |
|||
private const string Description = "Send a tweet"; |
|||
|
|||
private readonly TwitterOptions twitterOptions = twitterOptions.Value; |
|||
|
|||
protected override async Task<(string Description, TweetJob Data)> CreateJobAsync(EnrichedEvent @event, TweetAction action) |
|||
{ |
|||
var ruleJob = new TweetJob |
|||
{ |
|||
Text = (await FormatAsync(action.Text, @event))!, |
|||
AccessToken = action.AccessToken, |
|||
AccessSecret = action.AccessSecret, |
|||
}; |
|||
|
|||
return (Description, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(TweetJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var tokens = Tokens.Create( |
|||
twitterOptions.ClientId, |
|||
twitterOptions.ClientSecret, |
|||
job.AccessToken, |
|||
job.AccessSecret); |
|||
|
|||
var request = new Dictionary<string, object> |
|||
{ |
|||
["status"] = job.Text, |
|||
}; |
|||
|
|||
await tokens.Statuses.UpdateAsync(request, ct); |
|||
|
|||
return Result.Success($"Tweeted: {job.Text}"); |
|||
} |
|||
} |
|||
|
|||
public sealed class TweetJob |
|||
{ |
|||
public string AccessToken { get; set; } |
|||
|
|||
public string AccessSecret { get; set; } |
|||
|
|||
public string Text { get; set; } |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using CoreTweet; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.Twitter; |
|||
|
|||
[FlowStep( |
|||
Title = "Twitter", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M32 7.075a12.941 12.941 0 0 1-3.769 1.031 6.601 6.601 0 0 0 2.887-3.631 13.21 13.21 0 0 1-4.169 1.594A6.565 6.565 0 0 0 22.155 4a6.563 6.563 0 0 0-6.563 6.563c0 .512.056 1.012.169 1.494A18.635 18.635 0 0 1 2.23 5.195a6.56 6.56 0 0 0-.887 3.3 6.557 6.557 0 0 0 2.919 5.463 6.565 6.565 0 0 1-2.975-.819v.081a6.565 6.565 0 0 0 5.269 6.437 6.574 6.574 0 0 1-2.968.112 6.588 6.588 0 0 0 6.131 4.563 13.17 13.17 0 0 1-9.725 2.719 18.568 18.568 0 0 0 10.069 2.95c12.075 0 18.681-10.006 18.681-18.681 0-.287-.006-.569-.019-.85A13.216 13.216 0 0 0 32 7.076z'/></svg>", |
|||
IconColor = "#1da1f2", |
|||
Display = "Tweet", |
|||
Description = "Tweet an update with your twitter account.", |
|||
ReadMore = "https://twitter.com")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record TweetFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "Access Token", Description = " The generated access token.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string AccessToken { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Access Secret", Description = " The generated access secret.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string AccessSecret { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Text", Description = "The text that is sent as tweet to twitter.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression] |
|||
public string Text { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var twitterOptions = executionContext.Resolve<IOptions<TwitterOptions>>().Value; |
|||
|
|||
var tokens = Tokens.Create( |
|||
twitterOptions.ClientId, |
|||
twitterOptions.ClientSecret, |
|||
AccessToken, |
|||
AccessSecret); |
|||
|
|||
var request = new Dictionary<string, object> |
|||
{ |
|||
["status"] = Text, |
|||
}; |
|||
|
|||
await tokens.Statuses.UpdateAsync(request, ct); |
|||
|
|||
executionContext.Log("Tweeted", Text); |
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new TweetAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
} |
|||
@ -1,138 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text; |
|||
using System.Text.Json.Serialization; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
#pragma warning disable IDE0059 // Value assigned to symbol is never used
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Typesense; |
|||
|
|||
public sealed class TypesenseActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory, IScriptEngine scriptEngine, IJsonSerializer serializer) : RuleActionHandler<TypesenseAction, TypesenseJob>(formatter) |
|||
{ |
|||
protected override async Task<(string Description, TypesenseJob Data)> CreateJobAsync(EnrichedEvent @event, TypesenseAction action) |
|||
{ |
|||
var delete = @event.ShouldDelete(scriptEngine, action.Delete); |
|||
|
|||
string contentId; |
|||
|
|||
if (@event is IEnrichedEntityEvent enrichedEntityEvent) |
|||
{ |
|||
contentId = enrichedEntityEvent.Id.ToString(); |
|||
} |
|||
else |
|||
{ |
|||
contentId = DomainId.NewGuid().ToString(); |
|||
} |
|||
|
|||
var indexName = await FormatAsync(action.IndexName, @event); |
|||
var ruleText = string.Empty; |
|||
var ruleJob = new TypesenseJob |
|||
{ |
|||
ServerUrl = $"{action.Host.ToString().TrimEnd('/')}/collections/{indexName}/documents", |
|||
ServerKey = action.ApiKey, |
|||
ContentId = contentId, |
|||
}; |
|||
|
|||
if (delete) |
|||
{ |
|||
ruleText = $"Delete entry index: {indexName}"; |
|||
} |
|||
else |
|||
{ |
|||
ruleText = $"Upsert to index: {indexName}"; |
|||
|
|||
TypesenseContent content; |
|||
try |
|||
{ |
|||
string? jsonString; |
|||
|
|||
if (!string.IsNullOrEmpty(action.Document)) |
|||
{ |
|||
jsonString = await FormatAsync(action.Document, @event); |
|||
jsonString = jsonString?.Trim(); |
|||
} |
|||
else |
|||
{ |
|||
jsonString = ToJson(@event); |
|||
} |
|||
|
|||
content = serializer.Deserialize<TypesenseContent>(jsonString!); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
content = new TypesenseContent |
|||
{ |
|||
More = new Dictionary<string, object> |
|||
{ |
|||
["error"] = $"Invalid JSON: {ex.Message}", |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
content.Id = contentId; |
|||
|
|||
ruleJob.Content = serializer.Serialize(content, true); |
|||
} |
|||
|
|||
return (ruleText, ruleJob); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(TypesenseJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(job.ServerUrl)) |
|||
{ |
|||
return Result.Ignored(); |
|||
} |
|||
|
|||
var httpClient = httpClientFactory.CreateClient("TypesenseAction"); |
|||
|
|||
HttpRequestMessage request; |
|||
|
|||
if (job.Content != null) |
|||
{ |
|||
request = new HttpRequestMessage(HttpMethod.Post, $"{job.ServerUrl}?action=upsert") |
|||
{ |
|||
Content = new StringContent(job.Content, Encoding.UTF8, "application/json"), |
|||
}; |
|||
} |
|||
else |
|||
{ |
|||
request = new HttpRequestMessage(HttpMethod.Delete, $"{job.ServerUrl}/{job.ContentId}"); |
|||
} |
|||
|
|||
request.Headers.TryAddWithoutValidation("X-Typesense-Api-Key", job.ServerKey); |
|||
|
|||
return await httpClient.OneWayRequestAsync(request, job.Content, ct); |
|||
} |
|||
} |
|||
|
|||
public sealed class TypesenseContent |
|||
{ |
|||
public string Id { get; set; } |
|||
|
|||
[JsonExtensionData] |
|||
public Dictionary<string, object> More { get; set; } = []; |
|||
} |
|||
|
|||
public sealed class TypesenseJob |
|||
{ |
|||
public string ServerUrl { get; set; } |
|||
|
|||
public string ServerKey { get; set; } |
|||
|
|||
public string Content { get; set; } |
|||
|
|||
public string ContentId { get; set; } |
|||
} |
|||
@ -0,0 +1,155 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text; |
|||
using System.Text.Json.Serialization; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Extensions.Actions.Typesense; |
|||
|
|||
[FlowStep( |
|||
Title = "Typesense", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 49.293 50.853'><path d='M15.074 15.493a8.19 8.19 0 0 1 .165 1.601c0 .479-.055.994-.165 1.546l-7.013-.055v18.552c0 1.546.718 2.32 2.154 2.32h4.196c.258.625.386 1.25.386 1.877 0 .625-.036 1.012-.11 1.159-1.693.22-3.442.331-5.245.331-3.57 0-5.356-1.527-5.356-4.582V18.585l-3.92.055A7.91 7.91 0 0 1 0 17.094c0-.515.055-1.049.166-1.601l3.92.055V9.751c0-.994.147-1.694.442-2.098.294-.442.865-.663 1.711-.663H7.73l.331.331v8.283z'/><path d='M18.296 40.848c.036-.81.257-1.693.662-2.65.442-.994.94-1.767 1.491-2.32 2.908 1.583 5.466 2.375 7.675 2.375 1.214 0 2.19-.24 2.926-.718.773-.479 1.16-1.123 1.16-1.933 0-1.288-.994-2.319-2.982-3.092l-3.092-1.16c-4.638-1.692-6.957-4.398-6.957-8.116 0-1.325.24-2.503.718-3.533a7.992 7.992 0 0 1 2.098-2.706c.92-.773 2.006-1.362 3.258-1.767 1.251-.405 2.65-.607 4.196-.607.7 0 1.472.055 2.32.165.882.11 1.766.277 2.65.497.883.184 1.73.405 2.54.663s1.508.534 2.097.828c0 .92-.184 1.877-.552 2.871-.368.994-.865 1.73-1.49 2.209-2.909-1.288-5.43-1.933-7.565-1.933-.957 0-1.712.24-2.264.718-.552.442-.828 1.03-.828 1.767 0 1.141.92 2.043 2.761 2.706l3.368 1.214c2.43.847 4.233 2.006 5.411 3.479 1.178 1.472 1.767 3.184 1.767 5.135 0 2.613-.976 4.711-2.927 6.294-1.95 1.546-4.748 2.32-8.392 2.32-3.57 0-6.92-.903-10.049-2.706z' style='fill:#fffff;fill-opacity:1' transform='translate(0 -.354)'/><path d='M45.373 50.687V.166A9.626 9.626 0 0 1 47.25 0c.736 0 1.417.055 2.042.166v50.521a11.8 11.8 0 0 1-2.042.166c-.7 0-1.326-.056-1.878-.166z'/></svg>", |
|||
IconColor = "#1035bc", |
|||
Display = "Populate Typesense index", |
|||
Description = "Populate a full text search index in Typesense.", |
|||
ReadMore = "https://www.elastic.co/")] |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public sealed record TypesenseFlowStep : FlowStep, IConvertibleToAction |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
{ |
|||
[AbsoluteUrl] |
|||
[LocalizedRequired] |
|||
[Display(Name = "Server Url", Description = "The url to the instance or cluster.")] |
|||
[Editor(FlowStepEditor.Url)] |
|||
public Uri Host { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Index Name", Description = "The name of the index.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
[Expression] |
|||
public string IndexName { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Api Key", Description = "The api key.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string ApiKey { get; set; } |
|||
|
|||
[Display(Name = "Document", Description = "The optional custom document.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
[Expression] |
|||
public string? Document { get; set; } |
|||
|
|||
[Display(Name = "Deletion", Description = "The condition when to delete the document.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? Delete { get; set; } |
|||
|
|||
public override ValueTask PrepareAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
|
|||
if (!@event.ShouldDelete(executionContext, Delete)) |
|||
{ |
|||
Document = null; |
|||
return default; |
|||
} |
|||
|
|||
TypesenseContent content; |
|||
try |
|||
{ |
|||
content = executionContext.DeserializeJson<TypesenseContent>(Document!); |
|||
content.Id = @event.GetOrCreateId().Id; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
content = new TypesenseContent |
|||
{ |
|||
More = new Dictionary<string, object> |
|||
{ |
|||
["error"] = $"Invalid JSON: {ex.Message}", |
|||
}, |
|||
Id = @event.GetOrCreateId().Id, |
|||
}; |
|||
} |
|||
|
|||
Document = executionContext.SerializeJson(content); |
|||
|
|||
return base.PrepareAsync(executionContext, ct); |
|||
} |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var @event = ((FlowEventContext)executionContext.Context).Event; |
|||
|
|||
var (id, isGenerated) = @event.GetOrCreateId(); |
|||
if (isGenerated && Document == null) |
|||
{ |
|||
executionContext.LogSkipped("Can only delete content for static identities."); |
|||
return Next(); |
|||
} |
|||
|
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
async Task SendAsync(HttpRequestMessage request, string? body, string message) |
|||
{ |
|||
request.Headers.TryAddWithoutValidation("X-Typesense-Api-Key", ApiKey); |
|||
|
|||
var httpClient = |
|||
executionContext.Resolve<IHttpClientFactory>() |
|||
.CreateClient("TypesenseAction"); |
|||
|
|||
var (_, dump) = await httpClient.SendAsync(executionContext, request, body, ct); |
|||
|
|||
executionContext.Log(message, dump); |
|||
} |
|||
|
|||
if (Document != null) |
|||
{ |
|||
var request = new HttpRequestMessage(HttpMethod.Post, $"{Host}?action=upsert") |
|||
{ |
|||
Content = new StringContent(Document, Encoding.UTF8, "application/json"), |
|||
}; |
|||
|
|||
await SendAsync(request, Document, $"Document with ID '{id}' upserted"); |
|||
} |
|||
else |
|||
{ |
|||
var request = new HttpRequestMessage(HttpMethod.Delete, $"{Host}/{id}"); |
|||
|
|||
await SendAsync(request, Document, $"Document with ID '{id}' deleted"); |
|||
} |
|||
|
|||
return Next(); |
|||
} |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new TypesenseAction()); |
|||
} |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
|
|||
public sealed class TypesenseContent |
|||
{ |
|||
public string Id { get; set; } |
|||
|
|||
[JsonExtensionData] |
|||
public Dictionary<string, object> More { get; set; } = []; |
|||
} |
|||
} |
|||
@ -1,144 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Infrastructure; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Extensions.Actions.Webhook; |
|||
|
|||
public sealed class WebhookActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory) : RuleActionHandler<WebhookAction, WebhookJob>(formatter) |
|||
{ |
|||
protected override async Task<(string Description, WebhookJob Data)> CreateJobAsync(EnrichedEvent @event, WebhookAction action) |
|||
{ |
|||
var requestUrl = await FormatAsync(action.Url, @event); |
|||
var requestBody = string.Empty; |
|||
var requestSignature = string.Empty; |
|||
|
|||
if (action.Method != WebhookMethod.GET) |
|||
{ |
|||
if (!string.IsNullOrEmpty(action.Payload)) |
|||
{ |
|||
requestBody = await FormatAsync(action.Payload, @event); |
|||
} |
|||
else |
|||
{ |
|||
requestBody = ToEnvelopeJson(@event); |
|||
} |
|||
|
|||
requestSignature = $"{requestBody}{action.SharedSecret}".ToSha256Base64(); |
|||
} |
|||
|
|||
var ruleText = $"Send event to webhook '{requestUrl}'"; |
|||
var ruleJob = new WebhookJob |
|||
{ |
|||
Method = action.Method, |
|||
RequestUrl = (await FormatAsync(action.Url.ToString(), @event))!, |
|||
RequestSignature = requestSignature, |
|||
RequestBody = requestBody!, |
|||
RequestBodyType = action.PayloadType, |
|||
Headers = await ParseHeadersAsync(action.Headers, @event), |
|||
}; |
|||
|
|||
return (ruleText, ruleJob); |
|||
} |
|||
|
|||
private async Task<Dictionary<string, string>?> ParseHeadersAsync(string? headers, EnrichedEvent @event) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(headers)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var headersDictionary = new Dictionary<string, string>(); |
|||
|
|||
var lines = headers.Split('\n'); |
|||
|
|||
foreach (var line in lines) |
|||
{ |
|||
var indexEqual = line.IndexOf('=', StringComparison.Ordinal); |
|||
|
|||
if (indexEqual > 0 && indexEqual < line.Length - 1) |
|||
{ |
|||
var headerKey = line[..indexEqual]; |
|||
var headerValue = line[(indexEqual + 1)..]; |
|||
|
|||
headerValue = await FormatAsync(headerValue, @event); |
|||
|
|||
headersDictionary[headerKey] = headerValue!; |
|||
} |
|||
} |
|||
|
|||
return headersDictionary; |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(WebhookJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
var httpClient = httpClientFactory.CreateClient("WebhookAction"); |
|||
|
|||
var method = HttpMethod.Post; |
|||
|
|||
switch (job.Method) |
|||
{ |
|||
case WebhookMethod.PUT: |
|||
method = HttpMethod.Put; |
|||
break; |
|||
case WebhookMethod.GET: |
|||
method = HttpMethod.Get; |
|||
break; |
|||
case WebhookMethod.DELETE: |
|||
method = HttpMethod.Delete; |
|||
break; |
|||
case WebhookMethod.PATCH: |
|||
method = HttpMethod.Patch; |
|||
break; |
|||
} |
|||
|
|||
var request = new HttpRequestMessage(method, job.RequestUrl); |
|||
|
|||
if (!string.IsNullOrEmpty(job.RequestBody) && job.Method != WebhookMethod.GET) |
|||
{ |
|||
var mediaType = job.RequestBodyType.Or("application/json"); |
|||
|
|||
request.Content = new StringContent(job.RequestBody, Encoding.UTF8, mediaType); |
|||
} |
|||
|
|||
if (job.Headers != null) |
|||
{ |
|||
foreach (var (key, value) in job.Headers) |
|||
{ |
|||
request.Headers.TryAddWithoutValidation(key, value); |
|||
} |
|||
} |
|||
|
|||
if (!string.IsNullOrWhiteSpace(job.RequestSignature)) |
|||
{ |
|||
request.Headers.Add("X-Signature", job.RequestSignature); |
|||
} |
|||
|
|||
return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); |
|||
} |
|||
} |
|||
|
|||
public sealed class WebhookJob |
|||
{ |
|||
public WebhookMethod Method { get; set; } |
|||
|
|||
public string RequestUrl { get; set; } |
|||
|
|||
public string RequestSignature { get; set; } |
|||
|
|||
public string RequestBody { get; set; } |
|||
|
|||
public string? RequestBodyType { get; set; } |
|||
|
|||
public Dictionary<string, string>? Headers { get; set; } |
|||
} |
|||
@ -0,0 +1,146 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text; |
|||
using Squidex.Domain.Apps.Core.Rules.Deprecated; |
|||
using Squidex.Flows; |
|||
using Squidex.Flows.Steps; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
|
|||
namespace Squidex.Extensions.Actions.Webhook; |
|||
|
|||
[FlowStep( |
|||
Title = "Webhook", |
|||
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'><path d='M5.95 27.125h-.262C1.75 26.425 0 23.187 0 20.3c0-2.713 1.575-5.688 5.075-6.563V9.712c0-.525.35-.875.875-.875s.875.35.875.875v4.725c0 .438-.35.787-.7.875-2.975.438-4.375 2.8-4.375 4.988s1.313 4.55 4.2 5.075h.175a.907.907 0 0 1 .7 1.05c-.088.438-.438.7-.875.7zM21.175 27.387c-2.8 0-5.775-1.662-6.65-5.075H9.712c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h5.512c.438 0 .787.35.875.7.438 2.975 2.8 4.288 4.988 4.375 2.188 0 4.55-1.313 5.075-4.2v-.088a.908.908 0 0 1 1.05-.7.908.908 0 0 1 .7 1.05v.088c-.612 3.85-3.85 5.6-6.737 5.6zM21.525 18.55c-.525 0-.875-.35-.875-.875v-4.813c0-.438.35-.787.7-.875 2.975-.438 4.288-2.8 4.375-4.987 0-2.188-1.313-4.55-4.2-5.075h-.088c-.525-.175-.875-.613-.787-1.05s.525-.788 1.05-.7h.088c3.938.7 5.688 3.937 5.688 6.825 0 2.713-1.662 5.688-5.075 6.563v4.113c0 .438-.438.875-.875.875zM1.137 6.737H.962c-.438-.087-.788-.525-.7-.963v-.087c.7-3.938 3.85-5.688 6.737-5.688h.087c2.712 0 5.688 1.662 6.563 5.075h4.025c.525 0 .875.35.875.875s-.35.875-.875.875h-4.725c-.438 0-.788-.35-.875-.7-.438-2.975-2.8-4.288-4.988-4.375-2.188 0-4.55 1.313-5.075 4.2v.087c-.088.438-.438.7-.875.7z'/><path d='M7 10.588c-.875 0-1.837-.35-2.538-1.05a3.591 3.591 0 0 1 0-5.075C5.162 3.851 6.037 3.5 7 3.5s1.838.35 2.537 1.05c.7.7 1.05 1.575 1.05 2.537s-.35 1.837-1.05 2.538c-.7.612-1.575.963-2.537.963zM7 5.25c-.438 0-.875.175-1.225.525a1.795 1.795 0 0 0 2.538 2.538c.35-.35.525-.788.525-1.313s-.175-.875-.525-1.225S7.525 5.25 7 5.25zM21.088 23.887a3.65 3.65 0 0 1-2.537-1.05 3.591 3.591 0 0 1 0-5.075c.7-.7 1.575-1.05 2.537-1.05s1.838.35 2.537 1.05c.7.7 1.05 1.575 1.05 2.538s-.35 1.837-1.05 2.537c-.787.7-1.662 1.05-2.537 1.05zm0-5.337c-.525 0-.963.175-1.313.525a1.795 1.795 0 0 0 2.537 2.538c.35-.35.525-.788.525-1.313s-.175-.963-.525-1.313-.787-.438-1.225-.438zM20.387 10.588c-.875 0-1.837-.35-2.537-1.05S16.8 7.963 16.8 7.001s.35-1.837 1.05-2.538c.7-.612 1.662-.962 2.537-.962s1.838.35 2.538 1.05c1.4 1.4 1.4 3.675 0 5.075-.7.612-1.575.963-2.538.963zm0-5.338c-.525 0-.962.175-1.313.525s-.525.788-.525 1.313.175.962.525 1.313c.7.7 1.838.7 2.538 0s.7-1.838 0-2.538c-.263-.438-.7-.612-1.225-.612zM7.087 23.887c-.875 0-1.837-.35-2.538-1.05s-1.05-1.575-1.05-2.537.35-1.838 1.05-2.538c.7-.612 1.575-.962 2.538-.962s1.837.35 2.538 1.05c1.4 1.4 1.4 3.675 0 5.075-.7.612-1.575.962-2.538.962zm0-5.337c-.525 0-.962.175-1.313.525s-.525.788-.525 1.313.175.963.525 1.313a1.794 1.794 0 1 0 2.538-2.537c-.263-.438-.7-.612-1.225-.612z'/></svg>", |
|||
IconColor = "#4bb958", |
|||
Display = "Send webhook", |
|||
Description = "Invoke HTTP endpoints on a target system.", |
|||
ReadMore = "https://en.wikipedia.org/wiki/Webhook")] |
|||
public sealed record WebhookFlowStep : FlowStep, IConvertibleToAction |
|||
{ |
|||
[LocalizedRequired] |
|||
[Display(Name = "Method", Description = "The type of the request.")] |
|||
public WebhookMethod Method { get; set; } |
|||
|
|||
[LocalizedRequired] |
|||
[Display(Name = "Url", Description = "The URL to the webhook.")] |
|||
[Expression] |
|||
public Uri Url { get; set; } |
|||
|
|||
[Expression(ExpressionFallback.Envelope)] |
|||
[Display(Name = "Payload (Optional)", Description = "Leave it empty to use the full event as body.")] |
|||
[Editor(FlowStepEditor.TextArea)] |
|||
public string? Payload { get; set; } |
|||
|
|||
[Expression] |
|||
[Display(Name = "Headers (Optional)", Description = "The message headers in the format '[Key]=[Value]', one entry per line.")]
|
|||
[Editor(FlowStepEditor.TextArea)] |
|||
public string? Headers { get; set; } |
|||
|
|||
[Display(Name = "Payload Type", Description = "The mime type of the payload.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? PayloadType { get; set; } |
|||
|
|||
[Display(Name = "Shared Secret", Description = "The shared secret that is used to calculate the payload signature.")] |
|||
[Editor(FlowStepEditor.Text)] |
|||
public string? SharedSecret { get; set; } |
|||
|
|||
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext, |
|||
CancellationToken ct) |
|||
{ |
|||
var method = HttpMethod.Post; |
|||
switch (Method) |
|||
{ |
|||
case WebhookMethod.PUT: |
|||
method = HttpMethod.Put; |
|||
break; |
|||
case WebhookMethod.GET: |
|||
method = HttpMethod.Get; |
|||
break; |
|||
case WebhookMethod.DELETE: |
|||
method = HttpMethod.Delete; |
|||
break; |
|||
case WebhookMethod.PATCH: |
|||
method = HttpMethod.Patch; |
|||
break; |
|||
} |
|||
|
|||
if (executionContext.IsSimulation) |
|||
{ |
|||
executionContext.LogSkipSimulation(); |
|||
return Next(); |
|||
} |
|||
|
|||
var httpClient = executionContext.Resolve<IHttpClientFactory>().CreateClient("FlowClient"); |
|||
|
|||
var request = new HttpRequestMessage(method, Url); |
|||
if (!string.IsNullOrEmpty(Payload) && Method != WebhookMethod.GET) |
|||
{ |
|||
var mediaType = PayloadType; |
|||
if (string.IsNullOrEmpty(mediaType)) |
|||
{ |
|||
mediaType = "application/json"; |
|||
} |
|||
|
|||
request.Content = new StringContent(Payload, Encoding.UTF8, mediaType); |
|||
} |
|||
|
|||
var headers = ParseHeaders(); |
|||
if (headers != null) |
|||
{ |
|||
foreach (var (key, value) in headers) |
|||
{ |
|||
request.Headers.TryAddWithoutValidation(key, value); |
|||
} |
|||
} |
|||
|
|||
var signature = $"{Payload}{SharedSecret}".ToSha256Base64(); |
|||
|
|||
request.Headers.Add("X-Signature", signature); |
|||
|
|||
var (_, dump) = await httpClient.SendAsync(executionContext, request, Payload, ct); |
|||
|
|||
executionContext.Log("HTTP request sent", dump); |
|||
return Next(); |
|||
} |
|||
|
|||
private Dictionary<string, string>? ParseHeaders() |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(Headers)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var headersDictionary = new Dictionary<string, string>(); |
|||
|
|||
var lines = Headers.Split('\n'); |
|||
|
|||
foreach (var line in lines) |
|||
{ |
|||
var indexEqual = line.IndexOf('=', StringComparison.Ordinal); |
|||
|
|||
if (indexEqual > 0 && indexEqual < line.Length - 1) |
|||
{ |
|||
var headerKey = line[..indexEqual]; |
|||
var headerValue = line[(indexEqual + 1)..]; |
|||
headersDictionary[headerKey] = headerValue!; |
|||
} |
|||
} |
|||
|
|||
return headersDictionary; |
|||
} |
|||
|
|||
public RuleAction ToAction() |
|||
{ |
|||
return SimpleMapper.Map(this, new WebhookAction()); |
|||
} |
|||
} |
|||
@ -1,5 +0,0 @@ |
|||
cd translator\Squidex.Translator |
|||
|
|||
dotnet run translate clean-backend ..\..\..\.. |
|||
dotnet run translate clean-frontend ..\..\..\.. |
|||
|
|||
@ -0,0 +1,7 @@ |
|||
cd translator\Squidex.Translator |
|||
|
|||
dotnet run --no-restore translate clean-backend ..\..\..\.. |
|||
dotnet run --no-restore translate clean-frontend ..\..\..\.. |
|||
|
|||
cd ..\.. |
|||
|
|||
@ -0,0 +1,7 @@ |
|||
cd translator\Squidex.Translator |
|||
|
|||
/usr/local/share/dotnet/dotnet run --no-restore run translate clean-backend ..\..\..\.. |
|||
/usr/local/share/dotnet/dotnet run --no-restore run translate clean-frontend ..\..\..\.. |
|||
|
|||
cd ..\.. |
|||
|
|||
@ -1,3 +0,0 @@ |
|||
cd translator\Squidex.Translator |
|||
|
|||
dotnet run translate check-frontend ..\..\..\.. -l en --fix |
|||
@ -0,0 +1,5 @@ |
|||
cd translator\Squidex.Translator |
|||
|
|||
dotnet run translate check-frontend --no-restore ..\..\..\.. -l en --fix |
|||
|
|||
cd ..\.. |
|||
@ -0,0 +1,5 @@ |
|||
cd translator\Squidex.Translator |
|||
|
|||
/usr/local/share/dotnet/dotnet run --no-restore run translate check-frontend ..\..\..\.. -l en --fix |
|||
|
|||
cd ..\.. |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue