mirror of https://github.com/Squidex/squidex.git
22 changed files with 435 additions and 42 deletions
@ -0,0 +1,29 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoSchemaEntityWebhook.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using MongoDB.Bson.Serialization.Attributes; |
||||
|
using Squidex.Read.Schemas; |
||||
|
|
||||
|
namespace Squidex.Read.MongoDb.Schemas |
||||
|
{ |
||||
|
public sealed class MongoSchemaEntityWebhook : ISchemaWebhookEntity |
||||
|
{ |
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public Uri Url { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public string SecurityToken { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoSchemaWebhookEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using MongoDB.Bson; |
||||
|
using MongoDB.Bson.Serialization.Attributes; |
||||
|
using Squidex.Read.Schemas; |
||||
|
|
||||
|
namespace Squidex.Read.MongoDb.Schemas |
||||
|
{ |
||||
|
public class MongoSchemaWebhookEntity : ISchemaWebhookEntity |
||||
|
{ |
||||
|
[BsonId] |
||||
|
[BsonElement] |
||||
|
[BsonRepresentation(BsonType.String)] |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public Uri Url { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public string SecurityToken { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public Guid SchemaId { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,121 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoSchemaWebhookRepository.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Bson; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Events.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
using Squidex.Infrastructure.Dispatching; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
using Squidex.Read.Schemas; |
||||
|
using Squidex.Read.Schemas.Repositories; |
||||
|
|
||||
|
namespace Squidex.Read.MongoDb.Schemas |
||||
|
{ |
||||
|
public class MongoSchemaWebhookRepository : MongoRepositoryBase<MongoSchemaWebhookEntity>, ISchemaWebhookRepository, IEventConsumer |
||||
|
{ |
||||
|
private static readonly List<ISchemaWebhookEntity> EmptyWebhooks = new List<ISchemaWebhookEntity>(); |
||||
|
private Dictionary<Guid, List<MongoSchemaWebhookEntity>> inMemoryWebhooks; |
||||
|
private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); |
||||
|
|
||||
|
public string Name |
||||
|
{ |
||||
|
get { return GetType().Name; } |
||||
|
} |
||||
|
|
||||
|
public string EventsFilter |
||||
|
{ |
||||
|
get { return "^schema-"; } |
||||
|
} |
||||
|
|
||||
|
public MongoSchemaWebhookRepository(IMongoDatabase database) |
||||
|
: base(database) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return "Projections_SchemaWebhooks"; |
||||
|
} |
||||
|
|
||||
|
protected override Task SetupCollectionAsync(IMongoCollection<MongoSchemaWebhookEntity> collection) |
||||
|
{ |
||||
|
return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.SchemaId)); |
||||
|
} |
||||
|
|
||||
|
public Task On(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
return this.DispatchActionAsync(@event.Payload, @event.Headers); |
||||
|
} |
||||
|
|
||||
|
protected async Task On(WebhookAdded @event, EnvelopeHeaders headers) |
||||
|
{ |
||||
|
await EnsureWebooksLoadedAsync(); |
||||
|
|
||||
|
var webhook = SimpleMapper.Map(@event, new MongoSchemaWebhookEntity { SchemaId = @event.SchemaId.Id }); |
||||
|
|
||||
|
inMemoryWebhooks.GetOrAddNew(webhook.SchemaId).Add(webhook); |
||||
|
|
||||
|
await Collection.InsertOneAsync(webhook); |
||||
|
} |
||||
|
|
||||
|
protected async Task On(WebhookDeleted @event, EnvelopeHeaders headers) |
||||
|
{ |
||||
|
await EnsureWebooksLoadedAsync(); |
||||
|
|
||||
|
inMemoryWebhooks.GetOrDefault(@event.SchemaId.Id)?.RemoveAll(w => w.Id == @event.Id); |
||||
|
|
||||
|
await Collection.DeleteManyAsync(x => x.Id == @event.Id); |
||||
|
} |
||||
|
|
||||
|
protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers) |
||||
|
{ |
||||
|
await EnsureWebooksLoadedAsync(); |
||||
|
|
||||
|
inMemoryWebhooks.Remove(@event.SchemaId.Id); |
||||
|
|
||||
|
await Collection.DeleteManyAsync(x => x.SchemaId == @event.SchemaId.Id); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<ISchemaWebhookEntity>> QueryBySchemaAsync(Guid schemaId) |
||||
|
{ |
||||
|
await EnsureWebooksLoadedAsync(); |
||||
|
|
||||
|
return inMemoryWebhooks.GetOrDefault(schemaId)?.OfType<ISchemaWebhookEntity>()?.ToList() ?? EmptyWebhooks; |
||||
|
} |
||||
|
|
||||
|
private async Task EnsureWebooksLoadedAsync() |
||||
|
{ |
||||
|
if (inMemoryWebhooks == null) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await lockObject.WaitAsync(); |
||||
|
|
||||
|
if (inMemoryWebhooks == null) |
||||
|
{ |
||||
|
var webhooks = await Collection.Find(new BsonDocument()).ToListAsync(); |
||||
|
|
||||
|
inMemoryWebhooks = webhooks.GroupBy(x => x.SchemaId).ToDictionary(x => x.Key, x => x.ToList()); |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
lockObject.Release(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ISchemaWebhookEntity.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Read.Schemas |
||||
|
{ |
||||
|
public interface ISchemaWebhookEntity |
||||
|
{ |
||||
|
Guid Id { get; } |
||||
|
|
||||
|
Uri Url { get; } |
||||
|
|
||||
|
string SecurityToken { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ISchemaWebhookRepository.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Read.Schemas.Repositories |
||||
|
{ |
||||
|
public interface ISchemaWebhookRepository |
||||
|
{ |
||||
|
Task<IReadOnlyList<ISchemaWebhookEntity>> QueryBySchemaAsync(Guid schemaId); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,119 @@ |
|||||
|
// ==========================================================================
|
||||
|
// WebhookInvoker.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Net.Http; |
||||
|
using System.Text; |
||||
|
using System.Threading.Tasks; |
||||
|
using Newtonsoft.Json; |
||||
|
using Newtonsoft.Json.Linq; |
||||
|
using Squidex.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
using Squidex.Read.Schemas.Repositories; |
||||
|
|
||||
|
namespace Squidex.Read.Schemas |
||||
|
{ |
||||
|
public sealed class WebhookInvoker : IEventConsumer |
||||
|
{ |
||||
|
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); |
||||
|
|
||||
|
private readonly ISchemaWebhookRepository webhookRepository; |
||||
|
private readonly ISemanticLog log; |
||||
|
private readonly JsonSerializer webhookSerializer; |
||||
|
|
||||
|
public string Name |
||||
|
{ |
||||
|
get { return GetType().Name; } |
||||
|
} |
||||
|
|
||||
|
public string EventsFilter |
||||
|
{ |
||||
|
get { return "^content-"; } |
||||
|
} |
||||
|
|
||||
|
public WebhookInvoker(ISchemaWebhookRepository webhookRepository, JsonSerializer webhookSerializer, ISemanticLog log) |
||||
|
{ |
||||
|
Guard.NotNull(webhookRepository, nameof(webhookRepository)); |
||||
|
Guard.NotNull(webhookSerializer, nameof(webhookSerializer)); |
||||
|
Guard.NotNull(log, nameof(log)); |
||||
|
|
||||
|
this.webhookRepository = webhookRepository; |
||||
|
this.webhookSerializer = webhookSerializer; |
||||
|
|
||||
|
this.log = log; |
||||
|
} |
||||
|
|
||||
|
public Task ClearAsync() |
||||
|
{ |
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
|
||||
|
public async Task On(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
if (@event.Payload is ContentEvent contentEvent) |
||||
|
{ |
||||
|
var hooks = await webhookRepository.QueryBySchemaAsync(contentEvent.SchemaId.Id); |
||||
|
|
||||
|
if (hooks.Count > 0) |
||||
|
{ |
||||
|
var payload = CreatePayload(@event); |
||||
|
|
||||
|
foreach (var hook in hooks) |
||||
|
{ |
||||
|
DispatchEventAsync(payload, hook).Forget(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private JObject CreatePayload(Envelope<IEvent> @event) |
||||
|
{ |
||||
|
return new JObject( |
||||
|
new JProperty("type", @event.Payload.GetType().Name), |
||||
|
new JProperty("meta", JObject.FromObject(@event.Headers, webhookSerializer)), |
||||
|
new JProperty("data", JObject.FromObject(@event.Headers, webhookSerializer))); |
||||
|
} |
||||
|
|
||||
|
private async Task DispatchEventAsync(JObject payload, ISchemaWebhookEntity webhook) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
using (log.MeasureInformation(w => w |
||||
|
.WriteProperty("Action", "SendToHook") |
||||
|
.WriteProperty("Status", "Invoked"))) |
||||
|
{ |
||||
|
using (var client = new HttpClient()) |
||||
|
{ |
||||
|
client.Timeout = Timeout; |
||||
|
|
||||
|
var message = new HttpRequestMessage(HttpMethod.Post, webhook.Url) |
||||
|
{ |
||||
|
Content = new StringContent(payload.ToString(), Encoding.UTF8, "application/json") |
||||
|
}; |
||||
|
|
||||
|
message.Headers.TryAddWithoutValidation("X-SecurityToken", webhook.SecurityToken); |
||||
|
message.Headers.Add("User-Agent", "Squidex"); |
||||
|
|
||||
|
var response = await client.SendAsync(message); |
||||
|
|
||||
|
response.EnsureSuccessStatusCode(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
log.LogError(ex, w => w |
||||
|
.WriteProperty("Action", "SendToHook") |
||||
|
.WriteProperty("Status", "Failed")); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue