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