From ce4d8221b00010f4984dfdbeca094a70ae9a5665 Mon Sep 17 00:00:00 2001
From: cKey <35512826+colinin@users.noreply.github.com>
Date: Tue, 5 Apr 2022 11:06:49 +0800
Subject: [PATCH] fix: fix case in reference project paths
---
.../LINGYUN.Abp.Webhooks.ClientProxies.csproj | 16 ++
.../AbpWebhooksClientProxiesModule.cs | 10 +
.../ClientProxiesWebhookPublisher.cs | 91 +++++++++
.../LINGYUN.Abp.Webhooks/FodyWeavers.xml | 3 +
.../LINGYUN.Abp.Webhooks/FodyWeavers.xsd | 30 +++
.../LINGYUN.Abp.Webhooks.csproj | 18 ++
.../LINGYUN/Abp/Webhooks/AbpWebhooksModule.cs | 54 ++++++
.../Abp/Webhooks/AbpWebhooksOptions.cs | 35 ++++
.../BackgroundWorker/WebhookSenderJob.cs | 131 +++++++++++++
.../Abp/Webhooks/DefaultWebhookPublisher.cs | 140 ++++++++++++++
.../Abp/Webhooks/DefaultWebhookSender.cs | 129 +++++++++++++
.../WebhookSubscriptionExtensions.cs | 21 +++
.../Abp/Webhooks/IWebhookDefinitionContext.cs | 16 ++
.../Abp/Webhooks/IWebhookDefinitionManager.cs | 37 ++++
.../Abp/Webhooks/IWebhookEventStore.cs | 18 ++
.../LINGYUN/Abp/Webhooks/IWebhookManager.cs | 22 +++
.../LINGYUN/Abp/Webhooks/IWebhookPublisher.cs | 56 ++++++
.../Abp/Webhooks/IWebhookSendAttemptStore.cs | 25 +++
.../LINGYUN/Abp/Webhooks/IWebhookSender.cs | 16 ++
.../Webhooks/IWebhookSubscriptionManager.cs | 74 ++++++++
.../Webhooks/IWebhookSubscriptionsStore.cs | 83 +++++++++
.../Abp/Webhooks/NullWebhookEventStore.cs | 24 +++
.../Webhooks/NullWebhookSendAttemptStore.cs | 47 +++++
.../Webhooks/NullWebhookSubscriptionsStore.cs | 65 +++++++
.../LINGYUN/Abp/Webhooks/WebhookDefinition.cs | 67 +++++++
.../Abp/Webhooks/WebhookDefinitionContext.cs | 55 ++++++
.../Abp/Webhooks/WebhookDefinitionManager.cs | 139 ++++++++++++++
.../Abp/Webhooks/WebhookDefinitionProvider.cs | 13 ++
.../LINGYUN/Abp/Webhooks/WebhookEvent.cs | 30 +++
.../Abp/Webhooks/WebhookGroupDefinition.cs | 101 ++++++++++
.../LINGYUN/Abp/Webhooks/WebhookHeader.cs | 18 ++
.../LINGYUN/Abp/Webhooks/WebhookManager.cs | 80 ++++++++
.../LINGYUN/Abp/Webhooks/WebhookPayload.cs | 35 ++++
.../Abp/Webhooks/WebhookSendAttempt.cs | 39 ++++
.../LINGYUN/Abp/Webhooks/WebhookSenderArgs.cs | 62 +++++++
.../Abp/Webhooks/WebhookSubscriptionInfo.cs | 60 ++++++
.../Webhooks/WebhookSubscriptionManager.cs | 172 ++++++++++++++++++
.../System/AbpStringCryptographyExtensions.cs | 13 ++
38 files changed, 2045 insertions(+)
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN.Abp.Webhooks.ClientProxies.csproj
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN/Abp/Webhooks/ClientProxies/AbpWebhooksClientProxiesModule.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN/Abp/Webhooks/ClientProxies/ClientProxiesWebhookPublisher.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/FodyWeavers.xml
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/FodyWeavers.xsd
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN.Abp.Webhooks.csproj
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/AbpWebhooksModule.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/AbpWebhooksOptions.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/BackgroundWorker/WebhookSenderJob.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookPublisher.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookSender.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/Extensions/WebhookSubscriptionExtensions.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionContext.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionManager.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookEventStore.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookManager.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookPublisher.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSendAttemptStore.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSender.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionManager.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionsStore.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookEventStore.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookSendAttemptStore.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookSubscriptionsStore.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinition.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionContext.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionManager.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionProvider.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookEvent.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookGroupDefinition.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookHeader.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookManager.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookPayload.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSendAttempt.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSenderArgs.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionInfo.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionManager.cs
create mode 100644 aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/System/AbpStringCryptographyExtensions.cs
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN.Abp.Webhooks.ClientProxies.csproj b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN.Abp.Webhooks.ClientProxies.csproj
new file mode 100644
index 000000000..251dd53d3
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN.Abp.Webhooks.ClientProxies.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN/Abp/Webhooks/ClientProxies/AbpWebhooksClientProxiesModule.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN/Abp/Webhooks/ClientProxies/AbpWebhooksClientProxiesModule.cs
new file mode 100644
index 000000000..41e08a4a7
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN/Abp/Webhooks/ClientProxies/AbpWebhooksClientProxiesModule.cs
@@ -0,0 +1,10 @@
+using LINGYUN.Abp.WebhooksManagement;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Webhooks.ClientProxies;
+
+[DependsOn(typeof(AbpWebhooksModule))]
+[DependsOn(typeof(WebhooksManagementHttpApiClientModule))]
+public class AbpWebHooksClientProxiesModule : AbpModule
+{
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN/Abp/Webhooks/ClientProxies/ClientProxiesWebhookPublisher.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN/Abp/Webhooks/ClientProxies/ClientProxiesWebhookPublisher.cs
new file mode 100644
index 000000000..9e5ae8b35
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.ClientProxies/LINGYUN/Abp/Webhooks/ClientProxies/ClientProxiesWebhookPublisher.cs
@@ -0,0 +1,91 @@
+using LINGYUN.Abp.WebhooksManagement;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.Webhooks.ClientProxies;
+
+[Dependency(ReplaceServices = true)]
+public class ClientProxiesWebhookPublisher : IWebhookPublisher, ITransientDependency
+{
+ protected IWebhookPublishAppService PublishAppService { get; }
+
+ public ClientProxiesWebhookPublisher(
+ IWebhookPublishAppService publishAppService)
+ {
+ PublishAppService = publishAppService;
+ }
+
+ public async virtual Task PublishAsync(string webhookName, object data, bool sendExactSameData = false, WebhookHeader headers = null)
+ {
+ var input = new WebhookPublishInput
+ {
+ WebhookName = webhookName,
+ Data = JsonConvert.SerializeObject(data),
+ SendExactSameData = sendExactSameData,
+ };
+ if (headers != null)
+ {
+ input.Header = new WebhooksHeaderInput
+ {
+ UseOnlyGivenHeaders = headers.UseOnlyGivenHeaders,
+ Headers = headers.Headers
+ };
+ }
+
+ await PublishAsync(input);
+ }
+
+ public async virtual Task PublishAsync(string webhookName, object data, Guid? tenantId, bool sendExactSameData = false, WebhookHeader headers = null)
+ {
+ var input = new WebhookPublishInput
+ {
+ WebhookName = webhookName,
+ Data = JsonConvert.SerializeObject(data),
+ SendExactSameData = sendExactSameData,
+ TenantIds = new List
+ {
+ tenantId
+ },
+ };
+ if (headers != null)
+ {
+ input.Header = new WebhooksHeaderInput
+ {
+ UseOnlyGivenHeaders = headers.UseOnlyGivenHeaders,
+ Headers = headers.Headers
+ };
+ }
+
+ await PublishAsync(input);
+ }
+
+ public async virtual Task PublishAsync(Guid?[] tenantIds, string webhookName, object data, bool sendExactSameData = false, WebhookHeader headers = null)
+ {
+ var input = new WebhookPublishInput
+ {
+ WebhookName = webhookName,
+ Data = JsonConvert.SerializeObject(data),
+ SendExactSameData = sendExactSameData,
+ TenantIds = tenantIds.ToList(),
+ };
+ if (headers != null)
+ {
+ input.Header = new WebhooksHeaderInput
+ {
+ UseOnlyGivenHeaders = headers.UseOnlyGivenHeaders,
+ Headers = headers.Headers
+ };
+ }
+
+ await PublishAsync(input);
+ }
+
+ protected virtual async Task PublishAsync(WebhookPublishInput input)
+ {
+ await PublishAppService.PublishAsync(input);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/FodyWeavers.xml b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/FodyWeavers.xml
new file mode 100644
index 000000000..17d32672d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/FodyWeavers.xsd b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN.Abp.Webhooks.csproj b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN.Abp.Webhooks.csproj
new file mode 100644
index 000000000..2cd64673f
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN.Abp.Webhooks.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/AbpWebhooksModule.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/AbpWebhooksModule.cs
new file mode 100644
index 000000000..84798d8d2
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/AbpWebhooksModule.cs
@@ -0,0 +1,54 @@
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using Volo.Abp.BackgroundJobs;
+using Volo.Abp.Features;
+using Volo.Abp.Guids;
+using Volo.Abp.Http.Client;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Webhooks;
+
+//[DependsOn(typeof(AbpBackgroundJobsAbstractionsModule))]
+// 防止未引用实现无法发布到后台作业
+[DependsOn(typeof(AbpBackgroundJobsModule))]
+[DependsOn(typeof(AbpFeaturesModule))]
+[DependsOn(typeof(AbpGuidsModule))]
+[DependsOn(typeof(AbpHttpClientModule))]
+public class AbpWebhooksModule : AbpModule
+{
+ internal const string WebhooksClient = "__Abp_Webhooks_HttpClient";
+
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ AutoAddDefinitionProviders(context.Services);
+ }
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ var options = context.Services.ExecutePreConfiguredActions();
+
+ context.Services.AddHttpClient(WebhooksClient, client =>
+ {
+ client.Timeout = options.TimeoutDuration;
+ });
+ }
+
+ private static void AutoAddDefinitionProviders(IServiceCollection services)
+ {
+ var definitionProviders = new List();
+
+ services.OnRegistred(context =>
+ {
+ if (typeof(WebhookDefinitionProvider).IsAssignableFrom(context.ImplementationType))
+ {
+ definitionProviders.Add(context.ImplementationType);
+ }
+ });
+
+ services.Configure(options =>
+ {
+ options.DefinitionProviders.AddIfNotContains(definitionProviders);
+ });
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/AbpWebhooksOptions.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/AbpWebhooksOptions.cs
new file mode 100644
index 000000000..072a76bcc
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/AbpWebhooksOptions.cs
@@ -0,0 +1,35 @@
+using System;
+using Volo.Abp.Collections;
+
+namespace LINGYUN.Abp.Webhooks;
+
+public class AbpWebhooksOptions
+{
+ ///
+ /// 默认超时时间
+ ///
+ public TimeSpan TimeoutDuration { get; set; }
+ ///
+ /// 默认最大发送次数
+ ///
+ public int MaxSendAttemptCount { get; set; }
+ ///
+ /// 是否达到最大连续失败次数时自动取消订阅
+ ///
+ public bool IsAutomaticSubscriptionDeactivationEnabled { get; set; }
+ ///
+ /// 取消订阅前最大连续失败次数
+ ///
+ public int MaxConsecutiveFailCountBeforeDeactivateSubscription { get; set; }
+
+ public ITypeList DefinitionProviders { get; }
+
+ public AbpWebhooksOptions()
+ {
+ TimeoutDuration = TimeSpan.FromSeconds(60);
+ MaxSendAttemptCount = 5;
+ MaxConsecutiveFailCountBeforeDeactivateSubscription = MaxSendAttemptCount * 3;
+
+ DefinitionProviders = new TypeList();
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/BackgroundWorker/WebhookSenderJob.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/BackgroundWorker/WebhookSenderJob.cs
new file mode 100644
index 000000000..b411e3a84
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/BackgroundWorker/WebhookSenderJob.cs
@@ -0,0 +1,131 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.BackgroundJobs;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Uow;
+
+namespace LINGYUN.Abp.Webhooks.BackgroundWorker
+{
+ public class WebhookSenderJob : AsyncBackgroundJob, ITransientDependency
+ {
+ private readonly IUnitOfWorkManager _unitOfWorkManager;
+ private readonly IWebhookDefinitionManager _webhookDefinitionManager;
+ private readonly IWebhookSubscriptionManager _webhookSubscriptionManager;
+ private readonly IWebhookSendAttemptStore _webhookSendAttemptStore;
+ private readonly IWebhookSender _webhookSender;
+
+ private readonly AbpWebhooksOptions _options;
+
+ public WebhookSenderJob(
+ IUnitOfWorkManager unitOfWorkManager,
+ IWebhookDefinitionManager webhookDefinitionManager,
+ IWebhookSubscriptionManager webhookSubscriptionManager,
+ IWebhookSendAttemptStore webhookSendAttemptStore,
+ IWebhookSender webhookSender,
+ IOptions options)
+ {
+ _unitOfWorkManager = unitOfWorkManager;
+ _webhookDefinitionManager = webhookDefinitionManager;
+ _webhookSubscriptionManager = webhookSubscriptionManager;
+ _webhookSendAttemptStore = webhookSendAttemptStore;
+ _webhookSender = webhookSender;
+ _options = options.Value;
+ }
+
+ public override async Task ExecuteAsync(WebhookSenderArgs args)
+ {
+ var webhookDefinition = _webhookDefinitionManager.Get(args.WebhookName);
+
+ if (webhookDefinition.TryOnce)
+ {
+ try
+ {
+ await SendWebhook(args, webhookDefinition);
+ }
+ catch (Exception e)
+ {
+ Logger.LogWarning("An error occured while sending webhook with try once.", e);
+ // ignored
+ }
+ }
+ else
+ {
+ await SendWebhook(args, webhookDefinition);
+ }
+ }
+
+ private async Task SendWebhook(WebhookSenderArgs args, WebhookDefinition webhookDefinition)
+ {
+ if (args.WebhookEventId == default)
+ {
+ return;
+ }
+
+ if (args.WebhookSubscriptionId == default)
+ {
+ return;
+ }
+
+ if (!webhookDefinition.TryOnce)
+ {
+ var sendAttemptCount = await _webhookSendAttemptStore.GetSendAttemptCountAsync(
+ args.TenantId,
+ args.WebhookEventId,
+ args.WebhookSubscriptionId
+ );
+
+ if ((webhookDefinition.MaxSendAttemptCount > 0 && sendAttemptCount > webhookDefinition.MaxSendAttemptCount) ||
+ sendAttemptCount > _options.MaxSendAttemptCount)
+ {
+ return;
+ }
+ }
+
+ try
+ {
+ await _webhookSender.SendWebhookAsync(args);
+ }
+ catch (Exception)
+ {
+ // no need to retry to send webhook since subscription disabled
+ if (!await TryDeactivateSubscriptionIfReachedMaxConsecutiveFailCount(
+ args.TenantId,
+ args.WebhookSubscriptionId))
+ {
+ throw; //Throw exception to re-try sending webhook
+ }
+ }
+ }
+
+ private async Task TryDeactivateSubscriptionIfReachedMaxConsecutiveFailCount(
+ Guid? tenantId,
+ Guid subscriptionId)
+ {
+ if (!_options.IsAutomaticSubscriptionDeactivationEnabled)
+ {
+ return false;
+ }
+
+ var hasXConsecutiveFail = await _webhookSendAttemptStore
+ .HasXConsecutiveFailAsync(
+ tenantId,
+ subscriptionId,
+ _options.MaxConsecutiveFailCountBeforeDeactivateSubscription
+ );
+
+ if (!hasXConsecutiveFail)
+ {
+ return false;
+ }
+
+ using (var uow = _unitOfWorkManager.Begin())
+ {
+ await _webhookSubscriptionManager.ActivateWebhookSubscriptionAsync(subscriptionId, false);
+ await uow.CompleteAsync();
+ return true;
+ }
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookPublisher.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookPublisher.cs
new file mode 100644
index 000000000..be46197ae
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookPublisher.cs
@@ -0,0 +1,140 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.BackgroundJobs;
+using Volo.Abp.Guids;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class DefaultWebhookPublisher : IWebhookPublisher, ITransientDependency
+ {
+ public IWebhookEventStore WebhookEventStore { get; set; }
+
+ private readonly ICurrentTenant _currentTenant;
+ private readonly IBackgroundJobManager _backgroundJobManager;
+ private readonly IWebhookSubscriptionManager _webhookSubscriptionManager;
+
+ public DefaultWebhookPublisher(
+ IWebhookSubscriptionManager webhookSubscriptionManager,
+ ICurrentTenant currentTenant,
+ IBackgroundJobManager backgroundJobManager)
+ {
+ _currentTenant = currentTenant;
+ _backgroundJobManager = backgroundJobManager;
+ _webhookSubscriptionManager = webhookSubscriptionManager;
+
+ WebhookEventStore = NullWebhookEventStore.Instance;
+ }
+
+ #region Async Publish Methods
+
+ public virtual async Task PublishAsync(
+ string webhookName,
+ object data,
+ bool sendExactSameData = false,
+ WebhookHeader headers = null)
+ {
+ var subscriptions = await _webhookSubscriptionManager.GetAllSubscriptionsIfFeaturesGrantedAsync(_currentTenant.Id, webhookName);
+ await PublishAsync(webhookName, data, subscriptions, sendExactSameData, headers);
+ }
+
+ public virtual async Task PublishAsync(
+ string webhookName,
+ object data,
+ Guid? tenantId,
+ bool sendExactSameData = false,
+ WebhookHeader headers = null)
+ {
+ var subscriptions = await _webhookSubscriptionManager.GetAllSubscriptionsIfFeaturesGrantedAsync(tenantId, webhookName);
+ await PublishAsync(webhookName, data, subscriptions, sendExactSameData, headers);
+ }
+
+ public virtual async Task PublishAsync(
+ Guid?[] tenantIds,
+ string webhookName,
+ object data,
+ bool sendExactSameData = false,
+ WebhookHeader headers = null)
+ {
+ var subscriptions = await _webhookSubscriptionManager.GetAllSubscriptionsOfTenantsIfFeaturesGrantedAsync(tenantIds, webhookName);
+ await PublishAsync(webhookName, data, subscriptions, sendExactSameData, headers);
+ }
+
+ protected virtual async Task PublishAsync(
+ string webhookName,
+ object data,
+ List webhookSubscriptions,
+ bool sendExactSameData = false,
+ WebhookHeader headers = null)
+ {
+ if (webhookSubscriptions.IsNullOrEmpty())
+ {
+ return;
+ }
+
+ var subscriptionsGroupedByTenant = webhookSubscriptions.GroupBy(x => x.TenantId);
+
+ foreach (var subscriptionGroupedByTenant in subscriptionsGroupedByTenant)
+ {
+ var webhookInfo = await SaveAndGetWebhookAsync(subscriptionGroupedByTenant.Key, webhookName, data);
+
+ foreach (var webhookSubscription in subscriptionGroupedByTenant)
+ {
+ var headersToSend = webhookSubscription.Headers;
+ if (headers != null)
+ {
+ if (headers.UseOnlyGivenHeaders)//do not use the headers defined in subscription
+ {
+ headersToSend = headers.Headers;
+ }
+ else
+ {
+ //use the headers defined in subscription. If additional headers has same header, use additional headers value.
+ foreach (var additionalHeader in headers.Headers)
+ {
+ headersToSend[additionalHeader.Key] = additionalHeader.Value;
+ }
+ }
+ }
+
+ await _backgroundJobManager.EnqueueAsync(new WebhookSenderArgs
+ {
+ TenantId = webhookSubscription.TenantId,
+ WebhookEventId = webhookInfo.Id,
+ Data = webhookInfo.Data,
+ WebhookName = webhookInfo.WebhookName,
+ WebhookSubscriptionId = webhookSubscription.Id,
+ Headers = headersToSend,
+ Secret = webhookSubscription.Secret,
+ WebhookUri = webhookSubscription.WebhookUri,
+ SendExactSameData = sendExactSameData
+ });
+ }
+ }
+ }
+
+ #endregion
+
+ protected virtual async Task SaveAndGetWebhookAsync(
+ Guid? tenantId,
+ string webhookName,
+ object data)
+ {
+ var webhookInfo = new WebhookEvent
+ {
+ WebhookName = webhookName,
+ Data = JsonConvert.SerializeObject(data),
+ TenantId = tenantId
+ };
+
+ var webhookId = await WebhookEventStore.InsertAndGetIdAsync(webhookInfo);
+ webhookInfo.Id = webhookId;
+
+ return webhookInfo;
+ }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookSender.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookSender.cs
new file mode 100644
index 000000000..19982a68e
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookSender.cs
@@ -0,0 +1,129 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class DefaultWebhookSender : IWebhookSender, ITransientDependency
+ {
+ public ILogger Logger { protected get; set; }
+
+ private readonly IWebhookManager _webhookManager;
+ private readonly IHttpClientFactory _httpClientFactory;
+
+ private const string FailedRequestDefaultContent = "Webhook Send Request Failed";
+
+ public DefaultWebhookSender(
+ IWebhookManager webhookManager,
+ IHttpClientFactory httpClientFactory)
+ {
+ _webhookManager = webhookManager;
+ _httpClientFactory = httpClientFactory;
+
+ Logger = NullLogger.Instance;
+ }
+
+ public async Task SendWebhookAsync(WebhookSenderArgs webhookSenderArgs)
+ {
+ if (webhookSenderArgs.WebhookEventId == default)
+ {
+ throw new ArgumentNullException(nameof(webhookSenderArgs.WebhookEventId));
+ }
+
+ if (webhookSenderArgs.WebhookSubscriptionId == default)
+ {
+ throw new ArgumentNullException(nameof(webhookSenderArgs.WebhookSubscriptionId));
+ }
+
+ var webhookSendAttemptId = await _webhookManager.InsertAndGetIdWebhookSendAttemptAsync(webhookSenderArgs);
+
+ var request = CreateWebhookRequestMessage(webhookSenderArgs);
+
+ var serializedBody = await _webhookManager.GetSerializedBodyAsync(webhookSenderArgs);
+
+ _webhookManager.SignWebhookRequest(request, serializedBody, webhookSenderArgs.Secret);
+
+ AddAdditionalHeaders(request, webhookSenderArgs);
+
+ var isSucceed = false;
+ HttpStatusCode? statusCode = null;
+ var content = FailedRequestDefaultContent;
+
+ try
+ {
+ var response = await SendHttpRequest(request);
+ isSucceed = response.isSucceed;
+ statusCode = response.statusCode;
+ content = response.content;
+ }
+ catch (TaskCanceledException)
+ {
+ statusCode = HttpStatusCode.RequestTimeout;
+ content = "Request Timeout";
+ }
+ catch (HttpRequestException e)
+ {
+ content = e.Message;
+ }
+ catch (Exception e)
+ {
+ Logger.LogError("An error occured while sending a webhook request", e);
+ }
+ finally
+ {
+ await _webhookManager.StoreResponseOnWebhookSendAttemptAsync(webhookSendAttemptId, webhookSenderArgs.TenantId, statusCode, content);
+ }
+
+ if (!isSucceed)
+ {
+ throw new Exception($"Webhook sending attempt failed. WebhookSendAttempt id: {webhookSendAttemptId}");
+ }
+
+ return webhookSendAttemptId;
+ }
+
+ ///
+ /// You can override this to change request message
+ ///
+ ///
+ protected virtual HttpRequestMessage CreateWebhookRequestMessage(WebhookSenderArgs webhookSenderArgs)
+ {
+ return new HttpRequestMessage(HttpMethod.Post, webhookSenderArgs.WebhookUri);
+ }
+
+ protected virtual void AddAdditionalHeaders(HttpRequestMessage request, WebhookSenderArgs webhookSenderArgs)
+ {
+ foreach (var header in webhookSenderArgs.Headers)
+ {
+ if (request.Headers.TryAddWithoutValidation(header.Key, header.Value))
+ {
+ continue;
+ }
+
+ if (request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value))
+ {
+ continue;
+ }
+
+ throw new Exception($"Invalid Header. SubscriptionId:{webhookSenderArgs.WebhookSubscriptionId},Header: {header.Key}:{header.Value}");
+ }
+ }
+
+ protected virtual async Task<(bool isSucceed, HttpStatusCode statusCode, string content)> SendHttpRequest(HttpRequestMessage request)
+ {
+ var client = _httpClientFactory.CreateClient(AbpWebhooksModule.WebhooksClient);
+
+ var response = await client.SendAsync(request);
+
+ var isSucceed = response.IsSuccessStatusCode;
+ var statusCode = response.StatusCode;
+ var content = await response.Content.ReadAsStringAsync();
+
+ return (isSucceed, statusCode, content);
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/Extensions/WebhookSubscriptionExtensions.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/Extensions/WebhookSubscriptionExtensions.cs
new file mode 100644
index 000000000..8cf48f520
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/Extensions/WebhookSubscriptionExtensions.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.Webhooks.Extensions
+{
+ public static class WebhookSubscriptionExtensions
+ {
+ ///
+ /// checks if subscribed to given webhook
+ ///
+ ///
+ public static bool IsSubscribed(this WebhookSubscriptionInfo webhookSubscription, string webhookName)
+ {
+ if (webhookSubscription.Webhooks.IsNullOrEmpty())
+ {
+ return false;
+ }
+
+ return webhookSubscription.Webhooks.Contains(webhookName);
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionContext.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionContext.cs
new file mode 100644
index 000000000..c170ede3d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionContext.cs
@@ -0,0 +1,16 @@
+using JetBrains.Annotations;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookDefinitionContext
+ {
+ WebhookGroupDefinition AddGroup(
+ [NotNull] string name,
+ ILocalizableString displayName = null);
+
+ WebhookGroupDefinition GetGroupOrNull(string name);
+
+ void RemoveGroup(string name);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionManager.cs
new file mode 100644
index 000000000..a284ab693
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionManager.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookDefinitionManager
+ {
+ ///
+ /// Gets a webhook definition by name.
+ /// Returns null if there is no webhook definition with given name.
+ ///
+ WebhookDefinition GetOrNull(string name);
+
+ ///
+ /// Gets a webhook definition by name.
+ /// Throws exception if there is no webhook definition with given name.
+ ///
+ WebhookDefinition Get(string name);
+
+ ///
+ /// Gets all webhook definitions.
+ ///
+ IReadOnlyList GetAll();
+
+ ///
+ /// Gets all webhook group definitions.
+ ///
+ ///
+ IReadOnlyList GetGroups();
+
+ ///
+ /// Checks if given webhook name is available for given tenant.
+ ///
+ Task IsAvailableAsync(Guid? tenantId, string name);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookEventStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookEventStore.cs
new file mode 100644
index 000000000..1fb8f5c21
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookEventStore.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookEventStore
+ {
+ ///
+ /// Inserts to persistent store
+ ///
+ Task InsertAndGetIdAsync(WebhookEvent webhookEvent);
+
+ ///
+ /// Gets Webhook info by id
+ ///
+ Task GetAsync(Guid? tenantId, Guid id);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookManager.cs
new file mode 100644
index 000000000..1e8e5fd0c
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookManager.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookManager
+ {
+ Task GetWebhookPayloadAsync(WebhookSenderArgs webhookSenderArgs);
+
+ void SignWebhookRequest(HttpRequestMessage request, string serializedBody, string secret);
+
+ Task GetSerializedBodyAsync(WebhookSenderArgs webhookSenderArgs);
+
+ Task InsertAndGetIdWebhookSendAttemptAsync(WebhookSenderArgs webhookSenderArgs);
+
+ Task StoreResponseOnWebhookSendAttemptAsync(
+ Guid webhookSendAttemptId, Guid? tenantId,
+ HttpStatusCode? statusCode, string content);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookPublisher.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookPublisher.cs
new file mode 100644
index 000000000..3b53fec83
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookPublisher.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookPublisher
+ {
+ ///
+ /// Sends webhooks to current tenant subscriptions (). with given data, (Checks permissions)
+ ///
+ ///
+ /// data to send
+ ///
+ /// True: It sends the exact same data as the parameter to clients.
+ ///
+ /// False: It sends data in . It is recommended way.
+ ///
+ ///
+ /// Headers to send. Publisher uses subscription defined webhook by default. You can add additional headers from here. If subscription already has given header, publisher uses the one you give here.
+ Task PublishAsync(string webhookName, object data, bool sendExactSameData = false, WebhookHeader headers = null);
+
+ ///
+ /// Sends webhooks to given tenant's subscriptions
+ ///
+ ///
+ /// data to send
+ ///
+ /// Target tenant id
+ ///
+ ///
+ /// True: It sends the exact same data as the parameter to clients.
+ ///
+ /// False: It sends data in . It is recommended way.
+ ///
+ ///
+ /// Headers to send. Publisher uses subscription defined webhook by default. You can add additional headers from here. If subscription already has given header, publisher uses the one you give here.
+ Task PublishAsync(string webhookName, object data, Guid? tenantId, bool sendExactSameData = false, WebhookHeader headers = null);
+
+ ///
+ /// Sends webhooks to given tenant's subscriptions
+ ///
+ ///
+ /// data to send
+ ///
+ /// Target tenant id(s)
+ ///
+ ///
+ /// True: It sends the exact same data as the parameter to clients.
+ ///
+ /// False: It sends data in . It is recommended way.
+ ///
+ ///
+ /// Headers to send. Publisher uses subscription defined webhook by default. You can add additional headers from here. If subscription already has given header, publisher uses the one you give here.
+ Task PublishAsync(Guid?[] tenantIds, string webhookName, object data, bool sendExactSameData = false, WebhookHeader headers = null);
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSendAttemptStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSendAttemptStore.cs
new file mode 100644
index 000000000..2e32bc63f
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSendAttemptStore.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookSendAttemptStore
+ {
+ Task GetAsync(Guid? tenantId, Guid id);
+
+ ///
+ /// Returns work item count by given web hook id and subscription id, (How many times publisher tried to send web hook)
+ ///
+ Task GetSendAttemptCountAsync(Guid? tenantId, Guid webhookId, Guid webhookSubscriptionId);
+
+ ///
+ /// Checks is there any successful webhook attempt in last items. Should return true if there are not X number items
+ ///
+ Task HasXConsecutiveFailAsync(Guid? tenantId, Guid subscriptionId, int searchCount);
+
+ Task<(int TotalCount, IReadOnlyCollection Webhooks)> GetAllSendAttemptsBySubscriptionAsPagedListAsync(Guid? tenantId, Guid subscriptionId, int maxResultCount, int skipCount);
+
+ Task> GetAllSendAttemptsByWebhookEventIdAsync(Guid? tenantId, Guid webhookEventId);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSender.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSender.cs
new file mode 100644
index 000000000..d30e110e6
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSender.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookSender
+ {
+ ///
+ /// Tries to send webhook with given transactionId and stores process in
+ /// Should throw exception if fails or response status not succeed
+ ///
+ /// arguments
+ /// Webhook send attempt id
+ Task SendWebhookAsync(WebhookSenderArgs webhookSenderArgs);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionManager.cs
new file mode 100644
index 000000000..956b2dbd3
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionManager.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookSubscriptionManager
+ {
+ ///
+ /// Returns subscription for given id.
+ ///
+ /// Unique identifier of
+ Task GetAsync(Guid id);
+
+ ///
+ /// Returns all subscriptions of tenant
+ ///
+ ///
+ /// Target tenant id.
+ ///
+ Task> GetAllSubscriptionsAsync(Guid? tenantId);
+
+ ///
+ /// Returns all subscriptions for given webhook.
+ ///
+ ///
+ ///
+ /// Target tenant id.
+ ///
+ Task> GetAllSubscriptionsIfFeaturesGrantedAsync(Guid? tenantId, string webhookName);
+
+ ///
+ /// Returns all subscriptions of tenant
+ ///
+ ///
+ Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds);
+
+ ///
+ /// Returns all subscriptions for given webhook.
+ ///
+ ///
+ ///
+ /// Target tenant id(s).
+ ///
+ Task> GetAllSubscriptionsOfTenantsIfFeaturesGrantedAsync(Guid?[] tenantIds, string webhookName);
+
+ ///
+ /// Checks if tenant subscribed for a webhook. (Checks if webhook features are granted)
+ ///
+ ///
+ /// Target tenant id(s).
+ ///
+ ///
+ Task IsSubscribedAsync(Guid? tenantId, string webhookName);
+
+ ///
+ /// If id is the default(Guid) adds new subscription, else updates current one. (Checks if webhook features are granted)
+ ///
+ Task AddOrUpdateSubscriptionAsync(WebhookSubscriptionInfo webhookSubscription);
+
+ ///
+ /// Activates/Deactivates given webhook subscription
+ ///
+ /// unique identifier of
+ /// IsActive
+ Task ActivateWebhookSubscriptionAsync(Guid id, bool active);
+
+ ///
+ /// Delete given webhook subscription.
+ ///
+ /// unique identifier of
+ Task DeleteSubscriptionAsync(Guid id);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionsStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionsStore.cs
new file mode 100644
index 000000000..23849ad4e
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionsStore.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ ///
+ /// This interface should be implemented by vendors to make webhooks working.
+ ///
+ public interface IWebhookSubscriptionsStore
+ {
+ ///
+ /// returns subscription
+ ///
+ /// webhook subscription id
+ ///
+ Task GetAsync(Guid id);
+
+ ///
+ /// Saves webhook subscription to a persistent store.
+ ///
+ /// webhook subscription information
+ Task InsertAsync(WebhookSubscriptionInfo webhookSubscription);
+
+ ///
+ /// Updates webhook subscription to a persistent store.
+ ///
+ /// webhook subscription information
+ Task UpdateAsync(WebhookSubscriptionInfo webhookSubscription);
+
+ ///
+ /// Deletes subscription if exists
+ ///
+ /// primary key
+ ///
+ Task DeleteAsync(Guid id);
+
+ ///
+ /// Returns all subscriptions of given tenant including deactivated
+ ///
+ ///
+ /// Target tenant id.
+ ///
+ Task> GetAllSubscriptionsAsync(Guid? tenantId);
+
+ ///
+ /// Returns webhook subscriptions which subscribe to given webhook on tenant(s)
+ ///
+ ///
+ /// Target tenant id.
+ ///
+ ///
+ ///
+ Task> GetAllSubscriptionsAsync(Guid? tenantId, string webhookName);
+
+ ///
+ /// Returns all subscriptions of given tenant including deactivated
+ ///
+ ///
+ /// Target tenant id(s).
+ ///
+ Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds);
+
+ ///
+ /// Returns webhook subscriptions which subscribe to given webhook on tenant(s)
+ ///
+ ///
+ /// Target tenant id(s).
+ ///
+ ///
+ ///
+ Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds, string webhookName);
+
+ ///
+ /// Checks if tenant subscribed for a webhook
+ ///
+ ///
+ /// Target tenant id(s).
+ ///
+ /// Name of the webhook
+ Task IsSubscribedAsync(Guid? tenantId, string webhookName);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookEventStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookEventStore.cs
new file mode 100644
index 000000000..069726e67
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookEventStore.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ ///
+ /// Null pattern implementation of .
+ /// It's used if is not implemented by actual persistent store
+ ///
+ public class NullWebhookEventStore : IWebhookEventStore
+ {
+ public static NullWebhookEventStore Instance { get; } = new NullWebhookEventStore();
+
+ public Task InsertAndGetIdAsync(WebhookEvent webhookEvent)
+ {
+ return Task.FromResult(default);
+ }
+
+ public Task GetAsync(Guid? tenantId, Guid id)
+ {
+ return Task.FromResult(default);
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookSendAttemptStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookSendAttemptStore.cs
new file mode 100644
index 000000000..7254b1ae6
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookSendAttemptStore.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class NullWebhookSendAttemptStore : IWebhookSendAttemptStore
+ {
+ public static NullWebhookSendAttemptStore Instance = new NullWebhookSendAttemptStore();
+
+ public Task InsertAsync(WebhookSendAttempt webhookSendAttempt)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task UpdateAsync(WebhookSendAttempt webhookSendAttempt)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task GetAsync(Guid? tenantId, Guid id)
+ {
+ return Task.FromResult(default);
+ }
+
+ public Task GetSendAttemptCountAsync(Guid? tenantId, Guid webhookId, Guid webhookSubscriptionId)
+ {
+ return Task.FromResult(int.MaxValue);
+ }
+
+ public Task HasXConsecutiveFailAsync(Guid? tenantId, Guid subscriptionId, int searchCount)
+ {
+ return default;
+ }
+
+ public Task<(int TotalCount, IReadOnlyCollection Webhooks)> GetAllSendAttemptsBySubscriptionAsPagedListAsync(Guid? tenantId, Guid subscriptionId, int maxResultCount,
+ int skipCount)
+ {
+ return Task.FromResult(ValueTuple.Create(0, new List() as IReadOnlyCollection));
+ }
+
+ public Task> GetAllSendAttemptsByWebhookEventIdAsync(Guid? tenantId, Guid webhookEventId)
+ {
+ return Task.FromResult(new List());
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookSubscriptionsStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookSubscriptionsStore.cs
new file mode 100644
index 000000000..4214d1d25
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/NullWebhookSubscriptionsStore.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ ///
+ /// Null pattern implementation of .
+ /// It's used if is not implemented by actual persistent store
+ ///
+ public class NullWebhookSubscriptionsStore : IWebhookSubscriptionsStore
+ {
+ public static NullWebhookSubscriptionsStore Instance { get; } = new NullWebhookSubscriptionsStore();
+
+ public Task GetAsync(Guid id)
+ {
+ return Task.FromResult(default);
+ }
+
+ public WebhookSubscriptionInfo Get(Guid id)
+ {
+ return default;
+ }
+
+ public Task InsertAsync(WebhookSubscriptionInfo webhookSubscription)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task UpdateAsync(WebhookSubscriptionInfo webhookSubscription)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task DeleteAsync(Guid id)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task> GetAllSubscriptionsAsync(Guid? tenantId)
+ {
+ return Task.FromResult(new List());
+ }
+
+ public Task> GetAllSubscriptionsAsync(Guid? tenantId, string webhookName)
+ {
+ return Task.FromResult(new List());
+ }
+
+ public Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds)
+ {
+ return Task.FromResult(new List());
+ }
+
+ public Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds, string webhookName)
+ {
+ return Task.FromResult(new List());
+ }
+
+ public Task IsSubscribedAsync(Guid? tenantId, string webhookName)
+ {
+ return Task.FromResult(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinition.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinition.cs
new file mode 100644
index 000000000..1c1461e8a
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinition.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookDefinition
+ {
+ ///
+ /// Unique name of the webhook.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Tries to send webhook only one time without checking to send attempt count
+ ///
+ public bool TryOnce { get; set; }
+
+ ///
+ /// Defined maximum number of sending times
+ ///
+ public int MaxSendAttemptCount { get; set; }
+
+ ///
+ /// Display name of the webhook.
+ /// Optional.
+ ///
+ public ILocalizableString DisplayName { get; set; }
+
+ ///
+ /// Description for the webhook.
+ /// Optional.
+ ///
+ public ILocalizableString Description { get; set; }
+
+ public List RequiredFeatures { get; set; }
+
+ public WebhookDefinition(string name, ILocalizableString displayName = null, ILocalizableString description = null)
+ {
+ if (name.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(name), $"{nameof(name)} can not be null, empty or whitespace!");
+ }
+
+ Name = name.Trim();
+ DisplayName = displayName;
+ Description = description;
+
+ RequiredFeatures = new List();
+ }
+
+ public WebhookDefinition WithFeature(params string[] features)
+ {
+ if (!features.IsNullOrEmpty())
+ {
+ RequiredFeatures.AddRange(features);
+ }
+
+ return this;
+ }
+
+ public override string ToString()
+ {
+ return $"[{nameof(WebhookDefinition)} {Name}]";
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionContext.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionContext.cs
new file mode 100644
index 000000000..5e8987a8b
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionContext.cs
@@ -0,0 +1,55 @@
+using JetBrains.Annotations;
+using System.Collections.Generic;
+using Volo.Abp;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookDefinitionContext : IWebhookDefinitionContext
+ {
+ protected Dictionary Groups { get; }
+
+ public WebhookDefinitionContext(Dictionary webhooks)
+ {
+ Groups = webhooks;
+ }
+
+ public WebhookGroupDefinition AddGroup(
+ [NotNull] string name,
+ ILocalizableString displayName = null)
+ {
+ Check.NotNull(name, nameof(name));
+
+ if (Groups.ContainsKey(name))
+ {
+ throw new AbpException($"There is already an existing webhook group with name: {name}");
+ }
+
+ return Groups[name] = new WebhookGroupDefinition(name, displayName);
+ }
+
+ public WebhookGroupDefinition GetGroupOrNull([NotNull] string name)
+ {
+ Check.NotNull(name, nameof(name));
+
+ if (!Groups.ContainsKey(name))
+ {
+ return null;
+ }
+
+ return Groups[name];
+ }
+
+ public void RemoveGroup(string name)
+ {
+ Check.NotNull(name, nameof(name));
+
+ if (!Groups.ContainsKey(name))
+ {
+ throw new AbpException($"Undefined notification webhook group: '{name}'.");
+ }
+
+ Groups.Remove(name);
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionManager.cs
new file mode 100644
index 000000000..c0f96f20e
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionManager.cs
@@ -0,0 +1,139 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Features;
+using Volo.Abp.MultiTenancy;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ internal class WebhookDefinitionManager : IWebhookDefinitionManager, ISingletonDependency
+ {
+ protected IDictionary WebhookGroupDefinitions => _lazyWebhookGroupDefinitions.Value;
+ private readonly Lazy> _lazyWebhookGroupDefinitions;
+
+ protected IDictionary WebhookDefinitions => _lazyWebhookDefinitions.Value;
+ private readonly Lazy> _lazyWebhookDefinitions;
+
+ private readonly IServiceProvider _serviceProvider;
+ private readonly AbpWebhooksOptions _options;
+
+ public WebhookDefinitionManager(
+ IServiceProvider serviceProvider,
+ IOptions options)
+ {
+ _serviceProvider = serviceProvider;
+ _options = options.Value;
+
+ _lazyWebhookGroupDefinitions = new Lazy>(CreateWebhookGroupDefinitions);
+ _lazyWebhookDefinitions = new Lazy>(CreateWebhookDefinitions);
+ }
+
+ public WebhookDefinition GetOrNull(string name)
+ {
+ if (!WebhookDefinitions.ContainsKey(name))
+ {
+ return null;
+ }
+
+ return WebhookDefinitions[name];
+ }
+
+ public WebhookDefinition Get(string name)
+ {
+ if (!WebhookDefinitions.ContainsKey(name))
+ {
+ throw new KeyNotFoundException($"Webhook definitions does not contain a definition with the key \"{name}\".");
+ }
+
+ return WebhookDefinitions[name];
+ }
+
+ public IReadOnlyList GetAll()
+ {
+ return WebhookDefinitions.Values.ToImmutableList();
+ }
+
+ public IReadOnlyList GetGroups()
+ {
+ return WebhookGroupDefinitions.Values.ToImmutableList();
+ }
+
+ public async Task IsAvailableAsync(Guid? tenantId, string name)
+ {
+ if (tenantId == null) // host allowed to subscribe all webhooks
+ {
+ return true;
+ }
+
+ var webhookDefinition = GetOrNull(name);
+
+ if (webhookDefinition == null)
+ {
+ return false;
+ }
+
+ if (webhookDefinition.RequiredFeatures?.Any() == false)
+ {
+ return true;
+ }
+
+ var currentTenant = _serviceProvider.GetRequiredService();
+ var featureChecker = _serviceProvider.GetRequiredService();
+ using (currentTenant.Change(tenantId))
+ {
+ if (!await featureChecker.IsEnabledAsync(true, webhookDefinition.RequiredFeatures.ToArray()))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ protected virtual Dictionary CreateWebhookDefinitions()
+ {
+ var definitions = new Dictionary();
+
+ foreach (var groupDefinition in WebhookGroupDefinitions.Values)
+ {
+ foreach (var webhook in groupDefinition.Webhooks)
+ {
+ if (definitions.ContainsKey(webhook.Name))
+ {
+ throw new AbpException("Duplicate webhook name: " + webhook.Name);
+ }
+
+ definitions[webhook.Name] = webhook;
+ }
+ }
+
+ return definitions;
+ }
+
+ protected virtual Dictionary CreateWebhookGroupDefinitions()
+ {
+ var definitions = new Dictionary();
+
+ using (var scope = _serviceProvider.CreateScope())
+ {
+ var providers = _options
+ .DefinitionProviders
+ .Select(p => scope.ServiceProvider.GetRequiredService(p) as WebhookDefinitionProvider)
+ .ToList();
+
+ foreach (var provider in providers)
+ {
+ provider.Define(new WebhookDefinitionContext(definitions));
+ }
+ }
+
+ return definitions;
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionProvider.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionProvider.cs
new file mode 100644
index 000000000..ead9ab679
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookDefinitionProvider.cs
@@ -0,0 +1,13 @@
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public abstract class WebhookDefinitionProvider : ITransientDependency
+ {
+ ///
+ /// Used to add/manipulate webhook definitions.
+ ///
+ /// Context,
+ public abstract void Define(IWebhookDefinitionContext context);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookEvent.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookEvent.cs
new file mode 100644
index 000000000..bd32fa76d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookEvent.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ ///
+ /// Store created web hooks. To see who get that webhook check with and you can get
+ ///
+ public class WebhookEvent
+ {
+ public Guid Id { get; set; }
+
+ ///
+ /// Webhook unique name
+ ///
+ public string WebhookName { get; set; }
+
+ ///
+ /// Webhook data as JSON string.
+ ///
+ public string Data { get; set; }
+
+ public DateTime CreationTime { get; set; }
+
+ public Guid? TenantId { get; set; }
+
+ public bool IsDeleted { get; set; }
+
+ public DateTime? DeletionTime { get; set; }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookGroupDefinition.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookGroupDefinition.cs
new file mode 100644
index 000000000..ae58049ed
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookGroupDefinition.cs
@@ -0,0 +1,101 @@
+using JetBrains.Annotations;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Volo.Abp;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Webhooks;
+
+public class WebhookGroupDefinition
+{
+ [NotNull]
+ public string Name { get; set; }
+ public Dictionary Properties { get; }
+
+ private ILocalizableString _displayName;
+ public ILocalizableString DisplayName
+ {
+ get {
+ return _displayName;
+ }
+ set {
+ _displayName = value;
+ }
+ }
+
+ public IReadOnlyList Webhooks => _webhooks.ToImmutableList();
+ private readonly List _webhooks;
+ public object this[string name] {
+ get => Properties.GetOrDefault(name);
+ set => Properties[name] = value;
+ }
+
+ protected internal WebhookGroupDefinition(
+ string name,
+ ILocalizableString displayName = null)
+ {
+ Name = name;
+ DisplayName = displayName ?? new FixedLocalizableString(Name);
+
+ Properties = new Dictionary();
+ _webhooks = new List();
+ }
+
+ public virtual WebhookDefinition AddWebhook(
+ string name,
+ ILocalizableString displayName = null,
+ ILocalizableString description = null)
+ {
+ if (Webhooks.Any(hook => hook.Name.Equals(name)))
+ {
+ throw new AbpException($"There is already an existing webhook with name: {name} in group {Name}");
+ }
+
+ var webhook = new WebhookDefinition(
+ name,
+ displayName,
+ description
+ );
+
+ _webhooks.Add(webhook);
+
+ return webhook;
+ }
+
+ public virtual void AddWebhooks(params WebhookDefinition[] webhooks)
+ {
+ foreach (var webhook in webhooks)
+ {
+ if (Webhooks.Any(hook => hook.Name.Equals(webhook.Name)))
+ {
+ throw new AbpException($"There is already an existing webhook with name: {webhook.Name} in group {Name}");
+ }
+ }
+
+ _webhooks.AddRange(webhooks);
+ }
+
+
+
+ [CanBeNull]
+ public WebhookDefinition GetWebhookOrNull([NotNull] string name)
+ {
+ Check.NotNull(name, nameof(name));
+
+ foreach (var webhook in Webhooks)
+ {
+ if (webhook.Name == name)
+ {
+ return webhook;
+ }
+ }
+
+ return null;
+ }
+
+ public override string ToString()
+ {
+ return $"[{nameof(WebhookGroupDefinition)} {Name}]";
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookHeader.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookHeader.cs
new file mode 100644
index 000000000..91442552f
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookHeader.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookHeader
+ {
+ ///
+ /// If true, webhook will only contain given headers. If false given headers will be added to predefined headers in subscription.
+ /// Default is false
+ ///
+ public bool UseOnlyGivenHeaders { get; set; }
+
+ ///
+ /// That headers will be sent with the webhook.
+ ///
+ public IDictionary Headers { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookManager.cs
new file mode 100644
index 000000000..854795786
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookManager.cs
@@ -0,0 +1,80 @@
+using Newtonsoft.Json;
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public abstract class WebhookManager : IWebhookManager
+ {
+ private const string SignatureHeaderKey = "sha256";
+ private const string SignatureHeaderValueTemplate = SignatureHeaderKey + "={0}";
+ private const string SignatureHeaderName = "abp-webhook-signature";
+ protected IWebhookSendAttemptStore WebhookSendAttemptStore { get; }
+
+ protected WebhookManager(
+ IWebhookSendAttemptStore webhookSendAttemptStore)
+ {
+ WebhookSendAttemptStore = webhookSendAttemptStore;
+ }
+
+ public virtual async Task GetWebhookPayloadAsync(WebhookSenderArgs webhookSenderArgs)
+ {
+ var data = JsonConvert.SerializeObject(webhookSenderArgs.Data);
+
+ var attemptNumber = await WebhookSendAttemptStore.GetSendAttemptCountAsync(
+ webhookSenderArgs.TenantId,
+ webhookSenderArgs.WebhookEventId,
+ webhookSenderArgs.WebhookSubscriptionId);
+
+ return new WebhookPayload(
+ webhookSenderArgs.WebhookEventId.ToString(),
+ webhookSenderArgs.WebhookName,
+ attemptNumber)
+ {
+ Data = data
+ };
+ }
+
+ public virtual void SignWebhookRequest(HttpRequestMessage request, string serializedBody, string secret)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException(nameof(request));
+ }
+
+ if (string.IsNullOrWhiteSpace(serializedBody))
+ {
+ throw new ArgumentNullException(nameof(serializedBody));
+ }
+
+ request.Content = new StringContent(serializedBody, Encoding.UTF8, "application/json");
+
+ var secretBytes = Encoding.UTF8.GetBytes(secret);
+ var headerValue = string.Format(CultureInfo.InvariantCulture, SignatureHeaderValueTemplate, serializedBody.Sha256(secretBytes));
+
+ request.Headers.Add(SignatureHeaderName, headerValue);
+ }
+
+ public virtual async Task GetSerializedBodyAsync(WebhookSenderArgs webhookSenderArgs)
+ {
+ if (webhookSenderArgs.SendExactSameData)
+ {
+ return webhookSenderArgs.Data;
+ }
+
+ var payload = await GetWebhookPayloadAsync(webhookSenderArgs);
+
+ var serializedBody = JsonConvert.SerializeObject(payload);
+
+ return serializedBody;
+ }
+
+ public abstract Task InsertAndGetIdWebhookSendAttemptAsync(WebhookSenderArgs webhookSenderArgs);
+
+ public abstract Task StoreResponseOnWebhookSendAttemptAsync(Guid webhookSendAttemptId, Guid? tenantId, HttpStatusCode? statusCode, string content);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookPayload.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookPayload.cs
new file mode 100644
index 000000000..ff287cbd0
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookPayload.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookPayload
+ {
+ public string Id { get; set; }
+
+ public string WebhookEvent { get; set; }
+
+ public int Attempt { get; set; }
+
+ public dynamic Data { get; set; }
+
+ public DateTime CreationTimeUtc { get; set; }
+
+ public WebhookPayload(string id, string webhookEvent, int attempt)
+ {
+ if (id.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(id));
+ }
+
+ if (webhookEvent.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(webhookEvent));
+ }
+
+ Id = id;
+ WebhookEvent = webhookEvent;
+ Attempt = attempt;
+ CreationTimeUtc = DateTime.UtcNow;
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSendAttempt.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSendAttempt.cs
new file mode 100644
index 000000000..8189f8f72
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSendAttempt.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Net;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ ///
+ /// Table for store webhook work items. Each item stores web hook send attempt of to subscribed tenants
+ ///
+ public class WebhookSendAttempt
+ {
+ public Guid Id { get; set; }
+
+ ///
+ /// foreign id
+ ///
+ public Guid WebhookEventId { get; set; }
+
+ ///
+ /// foreign id
+ ///
+ public Guid WebhookSubscriptionId { get; set; }
+
+ ///
+ /// Webhook response content that webhook endpoint send back
+ ///
+ public string Response { get; set; }
+
+ ///
+ /// Webhook response status code that webhook endpoint send back
+ ///
+ public HttpStatusCode? ResponseStatusCode { get; set; }
+
+ public DateTime CreationTime { get; set; }
+
+ public DateTime? LastModificationTime { get; set; }
+
+ public Guid? TenantId { get; set; }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSenderArgs.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSenderArgs.cs
new file mode 100644
index 000000000..cb30acbf9
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSenderArgs.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookSenderArgs
+ {
+ public Guid? TenantId { get; set; }
+
+ //Webhook information
+
+ ///
+ /// foreign id
+ ///
+ public Guid WebhookEventId { get; set; }
+
+ ///
+ /// Webhook unique name
+ ///
+ public string WebhookName { get; set; }
+
+ ///
+ /// Webhook data as JSON string.
+ ///
+ public string Data { get; set; }
+
+ //Subscription information
+
+ ///
+ /// foreign id
+ ///
+ public Guid WebhookSubscriptionId { get; set; }
+
+ ///
+ /// Subscription webhook endpoint
+ ///
+ public string WebhookUri { get; set; }
+
+ ///
+ /// Webhook secret
+ ///
+ public string Secret { get; set; }
+
+ ///
+ /// Gets a set of additional HTTP headers.That headers will be sent with the webhook.
+ ///
+ public IDictionary Headers { get; set; }
+
+ ///
+ /// True: It sends the exact same data as the parameter to clients.
+ ///
+ /// False: It sends data in . It is recommended way.
+ ///
+ ///
+ public bool SendExactSameData { get; set; }
+
+ public WebhookSenderArgs()
+ {
+ Headers = new Dictionary();
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionInfo.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionInfo.cs
new file mode 100644
index 000000000..9e4e3bf2a
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionInfo.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookSubscriptionInfo
+ {
+ public Guid Id { get; set; }
+ ///
+ /// Subscribed Tenant's id .
+ ///
+ public Guid? TenantId { get; set; }
+
+ ///
+ /// Subscription webhook endpoint
+ ///
+ public string WebhookUri { get; set; }
+
+ ///
+ /// Webhook secret
+ ///
+ public string Secret { get; set; }
+
+ ///
+ /// Is subscription active
+ ///
+ public bool IsActive { get; set; }
+
+ ///
+ /// Subscribed webhook definitions unique names.It contains webhook definitions list as json
+ ///
+ /// Do not change it manually.
+ /// Use ,
+ /// ,
+ /// and
+ /// to change it.
+ ///
+ ///
+ public List Webhooks { get; set; }
+
+ ///
+ /// Gets a set of additional HTTP headers.That headers will be sent with the webhook. It contains webhook header dictionary as json
+ ///
+ /// Do not change it manually.
+ /// Use ,
+ /// ,
+ /// ,
+ /// to change it.
+ ///
+ ///
+ public IDictionary Headers { get; set; }
+
+ public WebhookSubscriptionInfo()
+ {
+ IsActive = true;
+ Headers = new Dictionary();
+ Webhooks = new List();
+ }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionManager.cs
new file mode 100644
index 000000000..fe1a9c697
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionManager.cs
@@ -0,0 +1,172 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.Authorization;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Guids;
+using Volo.Abp.Uow;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookSubscriptionManager : IWebhookSubscriptionManager, ITransientDependency
+ {
+ public IWebhookSubscriptionsStore WebhookSubscriptionsStore { get; set; }
+
+ private readonly IGuidGenerator _guidGenerator;
+ private readonly IUnitOfWorkManager _unitOfWorkManager;
+ private readonly IWebhookDefinitionManager _webhookDefinitionManager;
+
+ private const string WebhookSubscriptionSecretPrefix = "whs_";
+
+ public WebhookSubscriptionManager(
+ IGuidGenerator guidGenerator,
+ IUnitOfWorkManager unitOfWorkManager,
+ IWebhookDefinitionManager webhookDefinitionManager)
+ {
+ _guidGenerator = guidGenerator;
+ _unitOfWorkManager = unitOfWorkManager;
+ _webhookDefinitionManager = webhookDefinitionManager;
+
+ WebhookSubscriptionsStore = NullWebhookSubscriptionsStore.Instance;
+ }
+
+ public virtual async Task GetAsync(Guid id)
+ {
+ return await WebhookSubscriptionsStore.GetAsync(id);
+ }
+
+ public virtual async Task> GetAllSubscriptionsAsync(Guid? tenantId)
+ {
+ return await WebhookSubscriptionsStore.GetAllSubscriptionsAsync(tenantId);
+ }
+
+ public virtual async Task> GetAllSubscriptionsIfFeaturesGrantedAsync(Guid? tenantId, string webhookName)
+ {
+ if (!await _webhookDefinitionManager.IsAvailableAsync(tenantId, webhookName))
+ {
+ return new List();
+ }
+
+ return (await WebhookSubscriptionsStore.GetAllSubscriptionsAsync(tenantId, webhookName)).ToList();
+ }
+
+ public virtual async Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds)
+ {
+ return (await WebhookSubscriptionsStore.GetAllSubscriptionsOfTenantsAsync(tenantIds)).ToList();
+ }
+
+ public virtual async Task> GetAllSubscriptionsOfTenantsIfFeaturesGrantedAsync(Guid?[] tenantIds, string webhookName)
+ {
+ var featureGrantedTenants = new List();
+ foreach (var tenantId in tenantIds)
+ {
+ if (await _webhookDefinitionManager.IsAvailableAsync(tenantId, webhookName))
+ {
+ featureGrantedTenants.Add(tenantId);
+ }
+ }
+
+ return (await WebhookSubscriptionsStore.GetAllSubscriptionsOfTenantsAsync(featureGrantedTenants.ToArray(), webhookName)).ToList();
+ }
+
+ public virtual async Task IsSubscribedAsync(Guid? tenantId, string webhookName)
+ {
+ if (!await _webhookDefinitionManager.IsAvailableAsync(tenantId, webhookName))
+ {
+ return false;
+ }
+
+ return await WebhookSubscriptionsStore.IsSubscribedAsync(tenantId, webhookName);
+ }
+
+ public virtual async Task AddOrUpdateSubscriptionAsync(WebhookSubscriptionInfo webhookSubscription)
+ {
+ using (var uow = _unitOfWorkManager.Begin())
+ {
+ await CheckIfPermissionsGrantedAsync(webhookSubscription);
+
+ if (webhookSubscription.Id == default)
+ {
+ webhookSubscription.Id = _guidGenerator.Create();
+ webhookSubscription.Secret = WebhookSubscriptionSecretPrefix + Guid.NewGuid().ToString("N");
+ await WebhookSubscriptionsStore.InsertAsync(webhookSubscription);
+ }
+ else
+ {
+ await WebhookSubscriptionsStore.UpdateAsync(webhookSubscription);
+ }
+
+ await uow.SaveChangesAsync();
+ }
+ }
+
+ public virtual async Task ActivateWebhookSubscriptionAsync(Guid id, bool active)
+ {
+ using (var uow = _unitOfWorkManager.Begin())
+ {
+ var webhookSubscription = await WebhookSubscriptionsStore.GetAsync(id);
+ webhookSubscription.IsActive = active;
+
+ await uow.SaveChangesAsync();
+ }
+ }
+
+ public virtual async Task DeleteSubscriptionAsync(Guid id)
+ {
+ using (var uow = _unitOfWorkManager.Begin())
+ {
+ await WebhookSubscriptionsStore.DeleteAsync(id);
+
+ await uow.SaveChangesAsync();
+ }
+ }
+
+ public virtual async Task AddWebhookAsync(WebhookSubscriptionInfo subscription, string webhookName)
+ {
+ using (var uow = _unitOfWorkManager.Begin())
+ {
+ await CheckPermissionsAsync(subscription.TenantId, webhookName);
+ webhookName = webhookName.Trim();
+ if (webhookName.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(webhookName), $"{nameof(webhookName)} can not be null, empty or whitespace!");
+ }
+
+ if (!subscription.Webhooks.Contains(webhookName))
+ {
+ subscription.Webhooks.Add(webhookName);
+
+ await WebhookSubscriptionsStore.UpdateAsync(subscription);
+ }
+
+ await uow.SaveChangesAsync();
+ }
+ }
+
+ #region PermissionCheck
+
+ protected virtual async Task CheckIfPermissionsGrantedAsync(WebhookSubscriptionInfo webhookSubscription)
+ {
+ if (webhookSubscription.Webhooks.IsNullOrEmpty())
+ {
+ return;
+ }
+
+ foreach (var webhookDefinition in webhookSubscription.Webhooks)
+ {
+ await CheckPermissionsAsync(webhookSubscription.TenantId, webhookDefinition);
+ }
+ }
+
+ protected virtual async Task CheckPermissionsAsync(Guid? tenantId, string webhookName)
+ {
+ if (!await _webhookDefinitionManager.IsAvailableAsync(tenantId, webhookName))
+ {
+ throw new AbpAuthorizationException($"Tenant \"{tenantId}\" must have necessary feature(s) to use webhook \"{webhookName}\"");
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/System/AbpStringCryptographyExtensions.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/System/AbpStringCryptographyExtensions.cs
new file mode 100644
index 000000000..f4202f6e2
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/System/AbpStringCryptographyExtensions.cs
@@ -0,0 +1,13 @@
+using System.Security.Cryptography;
+
+namespace System;
+
+internal static class AbpStringCryptographyExtensions
+{
+ public static string Sha256(this string planText, byte[] salt)
+ {
+ var data = planText.GetBytes();
+ using var hmacsha256 = new HMACSHA256(salt);
+ return BitConverter.ToString(hmacsha256.ComputeHash(data));
+ }
+}