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";
}
///