diff --git a/uSync.BackOffice/BackOfficeConstants.cs b/uSync.BackOffice/BackOfficeConstants.cs index d8e1673e..5a8b5923 100644 --- a/uSync.BackOffice/BackOfficeConstants.cs +++ b/uSync.BackOffice/BackOfficeConstants.cs @@ -108,11 +108,16 @@ public static class Priorites /// public const int DataTypeMappings = USYNC_RESERVED_LOWER + 220; - /// - /// RelationTypes priority - /// - public const int RelationTypes = USYNC_RESERVED_LOWER + 230; - } + /// + /// RelationTypes priority + /// + public const int RelationTypes = USYNC_RESERVED_LOWER + 230; + + /// + /// Webhooks priority. + /// + public const int Webhooks = USYNC_RESERVED_LOWER + 250; + } /// /// Default group names @@ -238,6 +243,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..ce90a3f7 --- /dev/null +++ b/uSync.BackOffice/SyncHandlers/Handlers/WebhookHandler.cs @@ -0,0 +1,84 @@ +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; + +/// +/// handler for webhook events. +/// +[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; + + /// + /// constructor + /// + 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 f8c03fc1..e3c10542 100644 --- a/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs +++ b/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs @@ -160,11 +160,14 @@ internal static void AddHandlerNotifications(this IUmbracoBuilder builder) builder.AddNotificationHandler(); builder.AddNotificationHandler(); - // roots - pre-notifications for stopping things - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + // roots - pre-notifications for stopping things + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() @@ -192,8 +195,12 @@ internal static void AddHandlerNotifications(this IUmbracoBuilder builder) .AddNotificationHandler() .AddNotificationHandler() - .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..55c6174b --- /dev/null +++ b/uSync.Core/Serialization/Serializers/WebhookSerializer.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +using Microsoft.Extensions.Logging; + +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +using uSync.Core.Models; + +namespace uSync.Core.Serialization.Serializers; + +/// +/// serializing webhook events +/// +[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/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 63ea989c..36b42edd 100644 --- a/uSync.Core/uSyncConstants.cs +++ b/uSync.Core/uSyncConstants.cs @@ -51,9 +51,11 @@ public static class Serialization public const string Empty = "Empty"; - public const string RelationType = "RelationType"; - public const string Relation = "Relation"; - } + public const string RelationType = "RelationType"; + public const string Relation = "Relation"; + + public const string Webhook = "Webhook"; + } /// /// Key used in settings and xml to indicate only partial cultures are included in file