diff --git a/uSync.BackOffice/BackOfficeConstants.cs b/uSync.BackOffice/BackOfficeConstants.cs index 27835631..2de92544 100644 --- a/uSync.BackOffice/BackOfficeConstants.cs +++ b/uSync.BackOffice/BackOfficeConstants.cs @@ -118,6 +118,11 @@ public static class Priorites /// RelationTypes priority /// public const int RelationTypes = USYNC_RESERVED_LOWER + 230; + + /// + /// Webhooks priority. + /// + public const int Webhooks = USYNC_RESERVED_LOWER + 250; } /// @@ -244,6 +249,11 @@ public static class Handlers /// public const string TemplateHandler = "TemplateHandler"; + /// + /// WebhooksHandler name + /// + public const string WebhookHandler = "WebhookHandler"; + } } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/WebhookHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/WebhookHandler.cs new file mode 100644 index 00000000..93a0d823 --- /dev/null +++ b/uSync.BackOffice/SyncHandlers/Handlers/WebhookHandler.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; + +using uSync.BackOffice.Configuration; +using uSync.BackOffice.Services; +using uSync.Core; + +using static Umbraco.Cms.Core.Constants; + +namespace uSync.BackOffice.SyncHandlers.Handlers; + +[SyncHandler(uSyncConstants.Handlers.WebhookHandler, "Webhooks", "Webhooks", + uSyncConstants.Priorites.Webhooks, + Icon = "icon-filter-arrows", + EntityType = UdiEntityType.Webhook, + IsTwoPass = false +)] +public class WebhookHandler : SyncHandlerRoot, ISyncHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> +{ + private readonly IWebhookService _webhookService; + + public WebhookHandler( + ILogger> logger, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory itemFactory, + IWebhookService webhookService) + : base(logger, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, itemFactory) + { + _webhookService = webhookService; + } + + protected override IEnumerable DeleteMissingItems(IWebhook parent, IEnumerable keysToKeep, bool reportOnly) + { + return []; + } + + protected override IEnumerable GetChildItems(IWebhook parent) + { + if (parent == null) + { + return _webhookService.GetAllAsync(0, 1000).Result.Items; + } + + return []; + } + + protected override IEnumerable GetFolders(IWebhook parent) => []; + + protected override IWebhook GetFromService(IWebhook item) + => _webhookService.GetAsync(item.Key).Result; + + protected override string GetItemName(IWebhook item) => item.Key.ToString(); +} diff --git a/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs b/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs index 70c4440a..fa9b31da 100644 --- a/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs +++ b/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs @@ -166,6 +166,9 @@ internal static void AddHandlerNotifications(this IUmbracoBuilder builder) builder.AddNotificationHandler(); builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + // roots - pre-notifications for stopping things builder .AddNotificationHandler() @@ -202,7 +205,10 @@ internal static void AddHandlerNotifications(this IUmbracoBuilder builder) .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler(); + .AddNotificationHandler() + + .AddNotificationHandler() + .AddNotificationHandler(); // content ones diff --git a/uSync.Core/Serialization/Serializers/WebhookSerializer.cs b/uSync.Core/Serialization/Serializers/WebhookSerializer.cs new file mode 100644 index 00000000..cc7cba9e --- /dev/null +++ b/uSync.Core/Serialization/Serializers/WebhookSerializer.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +using Lucene.Net.Queries.Function.ValueSources; + +using Microsoft.Extensions.Logging; + +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +using uSync.Core.Models; + +namespace uSync.Core.Serialization.Serializers; + +[SyncSerializer("ED18C89D-A9FF-4217-9F8E-6898CA63ED81", "Webhook Serializer", uSyncConstants.Serialization.Webhook, IsTwoPass = false)] +public class WebhookSerializer : SyncSerializerBase, ISyncSerializer +{ + private readonly IWebhookService _webhookService; + + public WebhookSerializer( + IEntityService entityService, + ILogger> logger, + IWebhookService webhookService) + : base(entityService, logger) + { + _webhookService = webhookService; + } + + public override void DeleteItem(IWebhook item) + { + _webhookService.DeleteAsync(item.Key).Wait(); + } + + public override IWebhook FindItem(int id) => null; + + public override IWebhook FindItem(Guid key) + => _webhookService.GetAsync(key).Result; + + public override IWebhook FindItem(string alias) + { + if (Guid.TryParse(alias, out Guid key)) + return FindItem(key); + + return null; + } + + public override string ItemAlias(IWebhook item) + => item.Key.ToString(); + + public override void SaveItem(IWebhook item) + { + if (item.Id > 0) + { + _webhookService.UpdateAsync(item).Wait(); + } + else + { + _webhookService.CreateAsync(item).Wait(); + } + } + + protected override SyncAttempt DeserializeCore(XElement node, SyncSerializerOptions options) + { + var key = node.GetKey(); + var alias = node.GetAlias(); + + var details = new List(); + + var item = FindItem(key); + if (item == null) + { + // try and find by url/etc??? + } + + var url = node.Element("Url").ValueOrDefault(string.Empty); + + if (item == null) + { + item = new Webhook(url); + } + + if (item.Key != key) + { + details.AddUpdate("Key", item.Key, key); + item.Key = key; + } + + if (item.Url != url) + { + details.AddUpdate("Url", item.Url, url); + item.Url = url; + } + + details.AddRange(DeserializeContentKeys(item, node)); + details.AddRange(DeserializeEvents(item, node)); + details.AddRange(DeserializeHeaders(item, node)); + + return SyncAttempt.Succeed(node.GetAlias(), item, ChangeType.Import, details); + } + + private static List DeserializeContentKeys(IWebhook item, XElement node) + { + var details = new List(); + + var keys = node.Element("ContentTypeKeys"); + if (keys == null) return details; + + List newKeys = []; + + foreach (var key in keys.Elements("Key")) + { + var keyValue = key.ValueOrDefault(Guid.Empty); + if (keyValue == Guid.Empty) continue; + newKeys.Add(keyValue); + } + + var newOrderedKeys = newKeys.Order().ToArray(); + var existingOrderedKeys = item.ContentTypeKeys.Order().ToArray(); + + if (existingOrderedKeys.Equals(newOrderedKeys) is false) + { + details.AddUpdate("ContentTypeKeys", + string.Join(",", existingOrderedKeys), + string.Join(",", newOrderedKeys) + , "/"); + item.ContentTypeKeys = newOrderedKeys; + } + + return details; + + } + + private static List DeserializeEvents(IWebhook item, XElement node) + { + var details = new List(); + + var keys = node.Element("Events"); + if (keys == null) return details; + + List newKeys = []; + + foreach (var eventNode in keys.Elements("Event")) + { + var eventValue = eventNode.ValueOrDefault(string.Empty); + if (eventValue == string.Empty) continue; + newKeys.Add(eventValue); + } + + var newOrderedEvents = newKeys.Order().ToArray(); + var existingOrderedEvents = item.Events.Order().ToArray(); + + if (existingOrderedEvents.Equals(newOrderedEvents) is false) + { + details.AddUpdate("Events", + string.Join(",", existingOrderedEvents), + string.Join(",", newOrderedEvents) + , "/"); + item.Events = newOrderedEvents; + } + + return details; + } + + private static List DeserializeHeaders(IWebhook item, XElement node) + { + var details = new List(); + + var keys = node.Element("Headers"); + if (keys == null) return details; + + Dictionary newHeaders = new(); + + foreach (var header in keys.Elements("Header")) + { + var headerKey = header.Attribute("Key").ValueOrDefault(string.Empty); + var headerValue = header.ValueOrDefault(string.Empty); + + if (headerKey == string.Empty) continue; + if (newHeaders.ContainsKey(headerKey)) continue; // stop duplicates. + newHeaders.Add(headerKey, headerValue); + } + + var existingOrderedEvents = item.Headers.OrderBy(x => x.Key).ToDictionary(); + var newOrderedHeaders = newHeaders.OrderBy(x => x.Key).ToDictionary(); + + if (existingOrderedEvents.Equals(newOrderedHeaders) is false) + { + details.AddUpdate("Events", + string.Join(",", existingOrderedEvents), + string.Join(",", newOrderedHeaders) + , "/"); + item.Headers = newOrderedHeaders; + } + + return details; + } + + + + protected override SyncAttempt SerializeCore(IWebhook item, SyncSerializerOptions options) + { + var node = InitializeBaseNode(item, item.Url); + + node.Add(new XElement("Url", item.Url)); + node.Add(new XElement("Enabled", item.Enabled)); + + node.Add(SerializeContentKeys(item)); + node.Add(SerializeEvents(item)); + node.Add(SerializeHeaders(item)); + + return SyncAttempt.Succeed(item.Url, node, typeof(IWebhook), ChangeType.Export); + + } + + private static XElement SerializeContentKeys(IWebhook item) + { + var keysNode = new XElement("ContentTypeKeys"); + foreach (var contentTypeKey in item.ContentTypeKeys.Order()) + { + keysNode.Add(new XElement("Key", contentTypeKey)); + } + + return keysNode; + } + + private static XElement SerializeEvents(IWebhook item) + { + var eventsNode = new XElement("Events"); + foreach(var eventItem in item.Events.Order()) + { + eventsNode.Add(new XElement("Event", eventItem)); + } + return eventsNode; + } + + private static XElement SerializeHeaders(IWebhook item) + { + var headerNode = new XElement("Headers"); + foreach(var headerItem in item.Headers.OrderBy(x => x.Key)) + { + headerNode.Add(new XElement("Header", + new XAttribute("Key", headerItem.Key), + new XCData(headerItem.Value))); + } + + return headerNode; + } +} diff --git a/uSync.Core/Tracking/Impliment/DomainTracker.cs b/uSync.Core/Tracking/Impliment/DomainTracker.cs index 3b4295fd..e6472ccd 100644 --- a/uSync.Core/Tracking/Impliment/DomainTracker.cs +++ b/uSync.Core/Tracking/Impliment/DomainTracker.cs @@ -6,7 +6,7 @@ namespace uSync.Core.Tracking.Impliment { - public class DomainTracker : SyncXmlTracker, ISyncTracker + public class DomainTracker : SyncXmlTracker, ISyncTracker { public DomainTracker(SyncSerializerCollection serializers) : base(serializers) diff --git a/uSync.Core/Tracking/Impliment/WebhookTracker.cs b/uSync.Core/Tracking/Impliment/WebhookTracker.cs new file mode 100644 index 00000000..ca429806 --- /dev/null +++ b/uSync.Core/Tracking/Impliment/WebhookTracker.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +using Umbraco.Cms.Core.Models; + +using uSync.Core.Serialization; + +namespace uSync.Core.Tracking.Impliment +{ + public class WebhookTracker : SyncXmlTracker, ISyncTracker + { + public WebhookTracker(SyncSerializerCollection serializers) : base(serializers) + { + } + + public override List TrackingItems => [ + TrackingItem.Single("Enabled", "/Enabled"), + TrackingItem.Many("Events", "/Events/Event", "Event"), + TrackingItem.Many("Headers", "/Headers/Header", "@Key"), + TrackingItem.Many("ContentKeys", "/ContentTypeKeys/Key", "Key"), + ]; + } +} diff --git a/uSync.Core/uSyncConstants.cs b/uSync.Core/uSyncConstants.cs index 7a359813..74a0ebf2 100644 --- a/uSync.Core/uSyncConstants.cs +++ b/uSync.Core/uSyncConstants.cs @@ -53,6 +53,8 @@ public static class Serialization public const string RelationType = "RelationType"; public const string Relation = "Relation"; + + public const string Webhook = "Webhook"; } ///