38 changed files with 0 additions and 2045 deletions
@ -1,16 +0,0 @@ |
|||||
<Project Sdk="Microsoft.NET.Sdk"> |
|
||||
|
|
||||
<Import Project="..\..\..\configureawait.props" /> |
|
||||
<Import Project="..\..\..\common.props" /> |
|
||||
|
|
||||
<PropertyGroup> |
|
||||
<TargetFramework>netstandard2.0</TargetFramework> |
|
||||
<RootNamespace /> |
|
||||
</PropertyGroup> |
|
||||
|
|
||||
<ItemGroup> |
|
||||
<ProjectReference Include="..\LINGYUN.Abp.WebhooksManagement.HttpApi.Client\LINGYUN.Abp.WebhooksManagement.HttpApi.Client.csproj" /> |
|
||||
<ProjectReference Include="..\LINGYUN.Abp.Webhooks\LINGYUN.Abp.Webhooks.csproj" /> |
|
||||
</ItemGroup> |
|
||||
|
|
||||
</Project> |
|
||||
@ -1,10 +0,0 @@ |
|||||
using LINGYUN.Abp.WebhooksManagement; |
|
||||
using Volo.Abp.Modularity; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks.ClientProxies; |
|
||||
|
|
||||
[DependsOn(typeof(AbpWebhooksModule))] |
|
||||
[DependsOn(typeof(WebhooksManagementHttpApiClientModule))] |
|
||||
public class AbpWebHooksClientProxiesModule : AbpModule |
|
||||
{ |
|
||||
} |
|
||||
@ -1,91 +0,0 @@ |
|||||
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<Guid?> |
|
||||
{ |
|
||||
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); |
|
||||
} |
|
||||
} |
|
||||
@ -1,3 +0,0 @@ |
|||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|
||||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|
||||
</Weavers> |
|
||||
@ -1,30 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|
||||
<xs:element name="Weavers"> |
|
||||
<xs:complexType> |
|
||||
<xs:all> |
|
||||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|
||||
<xs:complexType> |
|
||||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|
||||
</xs:complexType> |
|
||||
</xs:element> |
|
||||
</xs:all> |
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|
||||
<xs:annotation> |
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|
||||
</xs:annotation> |
|
||||
</xs:attribute> |
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|
||||
<xs:annotation> |
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|
||||
</xs:annotation> |
|
||||
</xs:attribute> |
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|
||||
<xs:annotation> |
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|
||||
</xs:annotation> |
|
||||
</xs:attribute> |
|
||||
</xs:complexType> |
|
||||
</xs:element> |
|
||||
</xs:schema> |
|
||||
@ -1,18 +0,0 @@ |
|||||
<Project Sdk="Microsoft.NET.Sdk"> |
|
||||
|
|
||||
<Import Project="..\..\..\configureawait.props" /> |
|
||||
<Import Project="..\..\..\common.props" /> |
|
||||
|
|
||||
<PropertyGroup> |
|
||||
<TargetFramework>netstandard2.0</TargetFramework> |
|
||||
<RootNamespace /> |
|
||||
</PropertyGroup> |
|
||||
|
|
||||
<ItemGroup> |
|
||||
<PackageReference Include="Volo.Abp.BackgroundJobs" Version="$(VoloAbpPackageVersion)" /> |
|
||||
<PackageReference Include="Volo.Abp.Features" Version="$(VoloAbpPackageVersion)" /> |
|
||||
<PackageReference Include="Volo.Abp.Guids" Version="$(VoloAbpPackageVersion)" /> |
|
||||
<PackageReference Include="Volo.Abp.Http.Client" Version="$(VoloAbpPackageVersion)" /> |
|
||||
</ItemGroup> |
|
||||
|
|
||||
</Project> |
|
||||
@ -1,54 +0,0 @@ |
|||||
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<AbpWebhooksOptions>(); |
|
||||
|
|
||||
context.Services.AddHttpClient(WebhooksClient, client => |
|
||||
{ |
|
||||
client.Timeout = options.TimeoutDuration; |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private static void AutoAddDefinitionProviders(IServiceCollection services) |
|
||||
{ |
|
||||
var definitionProviders = new List<Type>(); |
|
||||
|
|
||||
services.OnRegistred(context => |
|
||||
{ |
|
||||
if (typeof(WebhookDefinitionProvider).IsAssignableFrom(context.ImplementationType)) |
|
||||
{ |
|
||||
definitionProviders.Add(context.ImplementationType); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
services.Configure<AbpWebhooksOptions>(options => |
|
||||
{ |
|
||||
options.DefinitionProviders.AddIfNotContains(definitionProviders); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
@ -1,35 +0,0 @@ |
|||||
using System; |
|
||||
using Volo.Abp.Collections; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks; |
|
||||
|
|
||||
public class AbpWebhooksOptions |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// 默认超时时间
|
|
||||
/// </summary>
|
|
||||
public TimeSpan TimeoutDuration { get; set; } |
|
||||
/// <summary>
|
|
||||
/// 默认最大发送次数
|
|
||||
/// </summary>
|
|
||||
public int MaxSendAttemptCount { get; set; } |
|
||||
/// <summary>
|
|
||||
/// 是否达到最大连续失败次数时自动取消订阅
|
|
||||
/// </summary>
|
|
||||
public bool IsAutomaticSubscriptionDeactivationEnabled { get; set; } |
|
||||
/// <summary>
|
|
||||
/// 取消订阅前最大连续失败次数
|
|
||||
/// </summary>
|
|
||||
public int MaxConsecutiveFailCountBeforeDeactivateSubscription { get; set; } |
|
||||
|
|
||||
public ITypeList<WebhookDefinitionProvider> DefinitionProviders { get; } |
|
||||
|
|
||||
public AbpWebhooksOptions() |
|
||||
{ |
|
||||
TimeoutDuration = TimeSpan.FromSeconds(60); |
|
||||
MaxSendAttemptCount = 5; |
|
||||
MaxConsecutiveFailCountBeforeDeactivateSubscription = MaxSendAttemptCount * 3; |
|
||||
|
|
||||
DefinitionProviders = new TypeList<WebhookDefinitionProvider>(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,131 +0,0 @@ |
|||||
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<WebhookSenderArgs>, 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<AbpWebhooksOptions> 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<bool> 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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,140 +0,0 @@ |
|||||
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<WebhookSubscriptionInfo> 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<WebhookEvent> 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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,129 +0,0 @@ |
|||||
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<DefaultWebhookSender> 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<DefaultWebhookSender>.Instance; |
|
||||
} |
|
||||
|
|
||||
public async Task<Guid> 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; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// You can override this to change request message
|
|
||||
/// </summary>
|
|
||||
/// <returns></returns>
|
|
||||
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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,21 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks.Extensions |
|
||||
{ |
|
||||
public static class WebhookSubscriptionExtensions |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// checks if subscribed to given webhook
|
|
||||
/// </summary>
|
|
||||
/// <returns></returns>
|
|
||||
public static bool IsSubscribed(this WebhookSubscriptionInfo webhookSubscription, string webhookName) |
|
||||
{ |
|
||||
if (webhookSubscription.Webhooks.IsNullOrEmpty()) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
return webhookSubscription.Webhooks.Contains(webhookName); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,16 +0,0 @@ |
|||||
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); |
|
||||
} |
|
||||
} |
|
||||
@ -1,37 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public interface IWebhookDefinitionManager |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Gets a webhook definition by name.
|
|
||||
/// Returns null if there is no webhook definition with given name.
|
|
||||
/// </summary>
|
|
||||
WebhookDefinition GetOrNull(string name); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a webhook definition by name.
|
|
||||
/// Throws exception if there is no webhook definition with given name.
|
|
||||
/// </summary>
|
|
||||
WebhookDefinition Get(string name); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets all webhook definitions.
|
|
||||
/// </summary>
|
|
||||
IReadOnlyList<WebhookDefinition> GetAll(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets all webhook group definitions.
|
|
||||
/// </summary>
|
|
||||
/// <returns></returns>
|
|
||||
IReadOnlyList<WebhookGroupDefinition> GetGroups(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Checks if given webhook name is available for given tenant.
|
|
||||
/// </summary>
|
|
||||
Task<bool> IsAvailableAsync(Guid? tenantId, string name); |
|
||||
} |
|
||||
} |
|
||||
@ -1,18 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public interface IWebhookEventStore |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Inserts to persistent store
|
|
||||
/// </summary>
|
|
||||
Task<Guid> InsertAndGetIdAsync(WebhookEvent webhookEvent); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets Webhook info by id
|
|
||||
/// </summary>
|
|
||||
Task<WebhookEvent> GetAsync(Guid? tenantId, Guid id); |
|
||||
} |
|
||||
} |
|
||||
@ -1,22 +0,0 @@ |
|||||
using System; |
|
||||
using System.Net; |
|
||||
using System.Net.Http; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public interface IWebhookManager |
|
||||
{ |
|
||||
Task<WebhookPayload> GetWebhookPayloadAsync(WebhookSenderArgs webhookSenderArgs); |
|
||||
|
|
||||
void SignWebhookRequest(HttpRequestMessage request, string serializedBody, string secret); |
|
||||
|
|
||||
Task<string> GetSerializedBodyAsync(WebhookSenderArgs webhookSenderArgs); |
|
||||
|
|
||||
Task<Guid> InsertAndGetIdWebhookSendAttemptAsync(WebhookSenderArgs webhookSenderArgs); |
|
||||
|
|
||||
Task StoreResponseOnWebhookSendAttemptAsync( |
|
||||
Guid webhookSendAttemptId, Guid? tenantId, |
|
||||
HttpStatusCode? statusCode, string content); |
|
||||
} |
|
||||
} |
|
||||
@ -1,56 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public interface IWebhookPublisher |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Sends webhooks to current tenant subscriptions (<see cref="IAbpSession.TenantId"/>). with given data, (Checks permissions)
|
|
||||
/// </summary>
|
|
||||
/// <param name="webhookName"><see cref="WebhookDefinition.Name"/></param>
|
|
||||
/// <param name="data">data to send</param>
|
|
||||
/// <param name="sendExactSameData">
|
|
||||
/// True: It sends the exact same data as the parameter to clients.
|
|
||||
/// <para>
|
|
||||
/// False: It sends data in <see cref="WebhookPayload"/>. It is recommended way.
|
|
||||
/// </para>
|
|
||||
/// </param>
|
|
||||
/// <param name="headers">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.</param>
|
|
||||
Task PublishAsync(string webhookName, object data, bool sendExactSameData = false, WebhookHeader headers = null); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Sends webhooks to given tenant's subscriptions
|
|
||||
/// </summary>
|
|
||||
/// <param name="webhookName"><see cref="WebhookDefinition.Name"/></param>
|
|
||||
/// <param name="data">data to send</param>
|
|
||||
/// <param name="tenantId">
|
|
||||
/// Target tenant id
|
|
||||
/// </param>
|
|
||||
/// <param name="sendExactSameData">
|
|
||||
/// True: It sends the exact same data as the parameter to clients.
|
|
||||
/// <para>
|
|
||||
/// False: It sends data in <see cref="WebhookPayload"/>. It is recommended way.
|
|
||||
/// </para>
|
|
||||
/// </param>
|
|
||||
/// <param name="headers">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.</param>
|
|
||||
Task PublishAsync(string webhookName, object data, Guid? tenantId, bool sendExactSameData = false, WebhookHeader headers = null); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Sends webhooks to given tenant's subscriptions
|
|
||||
/// </summary>
|
|
||||
/// <param name="webhookName"><see cref="WebhookDefinition.Name"/></param>
|
|
||||
/// <param name="data">data to send</param>
|
|
||||
/// <param name="tenantIds">
|
|
||||
/// Target tenant id(s)
|
|
||||
/// </param>
|
|
||||
/// <param name="sendExactSameData">
|
|
||||
/// True: It sends the exact same data as the parameter to clients.
|
|
||||
/// <para>
|
|
||||
/// False: It sends data in <see cref="WebhookPayload"/>. It is recommended way.
|
|
||||
/// </para>
|
|
||||
/// </param>
|
|
||||
/// <param name="headers">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.</param>
|
|
||||
Task PublishAsync(Guid?[] tenantIds, string webhookName, object data, bool sendExactSameData = false, WebhookHeader headers = null); |
|
||||
} |
|
||||
} |
|
||||
@ -1,25 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public interface IWebhookSendAttemptStore |
|
||||
{ |
|
||||
Task<WebhookSendAttempt> GetAsync(Guid? tenantId, Guid id); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Returns work item count by given web hook id and subscription id, (How many times publisher tried to send web hook)
|
|
||||
/// </summary>
|
|
||||
Task<int> GetSendAttemptCountAsync(Guid? tenantId, Guid webhookId, Guid webhookSubscriptionId); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Checks is there any successful webhook attempt in last <paramref name="searchCount"/> items. Should return true if there are not X number items
|
|
||||
/// </summary>
|
|
||||
Task<bool> HasXConsecutiveFailAsync(Guid? tenantId, Guid subscriptionId, int searchCount); |
|
||||
|
|
||||
Task<(int TotalCount, IReadOnlyCollection<WebhookSendAttempt> Webhooks)> GetAllSendAttemptsBySubscriptionAsPagedListAsync(Guid? tenantId, Guid subscriptionId, int maxResultCount, int skipCount); |
|
||||
|
|
||||
Task<List<WebhookSendAttempt>> GetAllSendAttemptsByWebhookEventIdAsync(Guid? tenantId, Guid webhookEventId); |
|
||||
} |
|
||||
} |
|
||||
@ -1,16 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public interface IWebhookSender |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Tries to send webhook with given transactionId and stores process in <see cref="WebhookSendAttempt"/>
|
|
||||
/// Should throw exception if fails or response status not succeed
|
|
||||
/// </summary>
|
|
||||
/// <param name="webhookSenderArgs">arguments</param>
|
|
||||
/// <returns>Webhook send attempt id</returns>
|
|
||||
Task<Guid> SendWebhookAsync(WebhookSenderArgs webhookSenderArgs); |
|
||||
} |
|
||||
} |
|
||||
@ -1,74 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public interface IWebhookSubscriptionManager |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Returns subscription for given id.
|
|
||||
/// </summary>
|
|
||||
/// <param name="id">Unique identifier of <see cref="WebhookSubscriptionInfo"/></param>
|
|
||||
Task<WebhookSubscriptionInfo> GetAsync(Guid id); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Returns all subscriptions of tenant
|
|
||||
/// </summary>
|
|
||||
/// <param name="tenantId">
|
|
||||
/// Target tenant id.
|
|
||||
/// </param>
|
|
||||
Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsAsync(Guid? tenantId); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Returns all subscriptions for given webhook.
|
|
||||
/// </summary>
|
|
||||
/// <param name="webhookName"><see cref="WebhookDefinition.Name"/></param>
|
|
||||
/// <param name="tenantId">
|
|
||||
/// Target tenant id.
|
|
||||
/// </param>
|
|
||||
Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsIfFeaturesGrantedAsync(Guid? tenantId, string webhookName); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Returns all subscriptions of tenant
|
|
||||
/// </summary>
|
|
||||
/// <returns></returns>
|
|
||||
Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Returns all subscriptions for given webhook.
|
|
||||
/// </summary>
|
|
||||
/// <param name="webhookName"><see cref="WebhookDefinition.Name"/></param>
|
|
||||
/// <param name="tenantIds">
|
|
||||
/// Target tenant id(s).
|
|
||||
/// </param>
|
|
||||
Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsOfTenantsIfFeaturesGrantedAsync(Guid?[] tenantIds, string webhookName); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Checks if tenant subscribed for a webhook. (Checks if webhook features are granted)
|
|
||||
/// </summary>
|
|
||||
/// <param name="tenantId">
|
|
||||
/// Target tenant id(s).
|
|
||||
/// </param>
|
|
||||
/// <param name="webhookName"><see cref="WebhookDefinition.Name"/></param>
|
|
||||
Task<bool> IsSubscribedAsync(Guid? tenantId, string webhookName); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// If id is the default(Guid) adds new subscription, else updates current one. (Checks if webhook features are granted)
|
|
||||
/// </summary>
|
|
||||
Task AddOrUpdateSubscriptionAsync(WebhookSubscriptionInfo webhookSubscription); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Activates/Deactivates given webhook subscription
|
|
||||
/// </summary>
|
|
||||
/// <param name="id">unique identifier of <see cref="WebhookSubscriptionInfo"/></param>
|
|
||||
/// <param name="active">IsActive</param>
|
|
||||
Task ActivateWebhookSubscriptionAsync(Guid id, bool active); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Delete given webhook subscription.
|
|
||||
/// </summary>
|
|
||||
/// <param name="id">unique identifier of <see cref="WebhookSubscriptionInfo"/></param>
|
|
||||
Task DeleteSubscriptionAsync(Guid id); |
|
||||
} |
|
||||
} |
|
||||
@ -1,83 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// This interface should be implemented by vendors to make webhooks working.
|
|
||||
/// </summary>
|
|
||||
public interface IWebhookSubscriptionsStore |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// returns subscription
|
|
||||
/// </summary>
|
|
||||
/// <param name="id">webhook subscription id</param>
|
|
||||
/// <returns></returns>
|
|
||||
Task<WebhookSubscriptionInfo> GetAsync(Guid id); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Saves webhook subscription to a persistent store.
|
|
||||
/// </summary>
|
|
||||
/// <param name="webhookSubscription">webhook subscription information</param>
|
|
||||
Task InsertAsync(WebhookSubscriptionInfo webhookSubscription); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Updates webhook subscription to a persistent store.
|
|
||||
/// </summary>
|
|
||||
/// <param name="webhookSubscription">webhook subscription information</param>
|
|
||||
Task UpdateAsync(WebhookSubscriptionInfo webhookSubscription); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Deletes subscription if exists
|
|
||||
/// </summary>
|
|
||||
/// <param name="id"><see cref="WebhookSubscriptionInfo"/> primary key</param>
|
|
||||
/// <returns></returns>
|
|
||||
Task DeleteAsync(Guid id); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Returns all subscriptions of given tenant including deactivated
|
|
||||
/// </summary>
|
|
||||
/// <param name="tenantId">
|
|
||||
/// Target tenant id.
|
|
||||
/// </param>
|
|
||||
Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsAsync(Guid? tenantId); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Returns webhook subscriptions which subscribe to given webhook on tenant(s)
|
|
||||
/// </summary>
|
|
||||
/// <param name="tenantId">
|
|
||||
/// Target tenant id.
|
|
||||
/// </param>
|
|
||||
/// <param name="webhookName"><see cref="WebhookDefinition.Name"/></param>
|
|
||||
/// <returns></returns>
|
|
||||
Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsAsync(Guid? tenantId, string webhookName); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Returns all subscriptions of given tenant including deactivated
|
|
||||
/// </summary>
|
|
||||
/// <param name="tenantIds">
|
|
||||
/// Target tenant id(s).
|
|
||||
/// </param>
|
|
||||
Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Returns webhook subscriptions which subscribe to given webhook on tenant(s)
|
|
||||
/// </summary>
|
|
||||
/// <param name="tenantIds">
|
|
||||
/// Target tenant id(s).
|
|
||||
/// </param>
|
|
||||
/// <param name="webhookName"><see cref="WebhookDefinition.Name"/></param>
|
|
||||
/// <returns></returns>
|
|
||||
Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds, string webhookName); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Checks if tenant subscribed for a webhook
|
|
||||
/// </summary>
|
|
||||
/// <param name="tenantId">
|
|
||||
/// Target tenant id(s).
|
|
||||
/// </param>
|
|
||||
/// <param name="webhookName">Name of the webhook</param>
|
|
||||
Task<bool> IsSubscribedAsync(Guid? tenantId, string webhookName); |
|
||||
} |
|
||||
} |
|
||||
@ -1,24 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Null pattern implementation of <see cref="IWebhookSubscriptionsStore"/>.
|
|
||||
/// It's used if <see cref="IWebhookSubscriptionsStore"/> is not implemented by actual persistent store
|
|
||||
/// </summary>
|
|
||||
public class NullWebhookEventStore : IWebhookEventStore |
|
||||
{ |
|
||||
public static NullWebhookEventStore Instance { get; } = new NullWebhookEventStore(); |
|
||||
|
|
||||
public Task<Guid> InsertAndGetIdAsync(WebhookEvent webhookEvent) |
|
||||
{ |
|
||||
return Task.FromResult<Guid>(default); |
|
||||
} |
|
||||
|
|
||||
public Task<WebhookEvent> GetAsync(Guid? tenantId, Guid id) |
|
||||
{ |
|
||||
return Task.FromResult<WebhookEvent>(default); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,47 +0,0 @@ |
|||||
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<WebhookSendAttempt> GetAsync(Guid? tenantId, Guid id) |
|
||||
{ |
|
||||
return Task.FromResult<WebhookSendAttempt>(default); |
|
||||
} |
|
||||
|
|
||||
public Task<int> GetSendAttemptCountAsync(Guid? tenantId, Guid webhookId, Guid webhookSubscriptionId) |
|
||||
{ |
|
||||
return Task.FromResult(int.MaxValue); |
|
||||
} |
|
||||
|
|
||||
public Task<bool> HasXConsecutiveFailAsync(Guid? tenantId, Guid subscriptionId, int searchCount) |
|
||||
{ |
|
||||
return default; |
|
||||
} |
|
||||
|
|
||||
public Task<(int TotalCount, IReadOnlyCollection<WebhookSendAttempt> Webhooks)> GetAllSendAttemptsBySubscriptionAsPagedListAsync(Guid? tenantId, Guid subscriptionId, int maxResultCount, |
|
||||
int skipCount) |
|
||||
{ |
|
||||
return Task.FromResult(ValueTuple.Create(0, new List<WebhookSendAttempt>() as IReadOnlyCollection<WebhookSendAttempt>)); |
|
||||
} |
|
||||
|
|
||||
public Task<List<WebhookSendAttempt>> GetAllSendAttemptsByWebhookEventIdAsync(Guid? tenantId, Guid webhookEventId) |
|
||||
{ |
|
||||
return Task.FromResult(new List<WebhookSendAttempt>()); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,65 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Null pattern implementation of <see cref="IWebhookSubscriptionsStore"/>.
|
|
||||
/// It's used if <see cref="IWebhookSubscriptionsStore"/> is not implemented by actual persistent store
|
|
||||
/// </summary>
|
|
||||
public class NullWebhookSubscriptionsStore : IWebhookSubscriptionsStore |
|
||||
{ |
|
||||
public static NullWebhookSubscriptionsStore Instance { get; } = new NullWebhookSubscriptionsStore(); |
|
||||
|
|
||||
public Task<WebhookSubscriptionInfo> GetAsync(Guid id) |
|
||||
{ |
|
||||
return Task.FromResult<WebhookSubscriptionInfo>(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<List<WebhookSubscriptionInfo>> GetAllSubscriptionsAsync(Guid? tenantId) |
|
||||
{ |
|
||||
return Task.FromResult(new List<WebhookSubscriptionInfo>()); |
|
||||
} |
|
||||
|
|
||||
public Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsAsync(Guid? tenantId, string webhookName) |
|
||||
{ |
|
||||
return Task.FromResult(new List<WebhookSubscriptionInfo>()); |
|
||||
} |
|
||||
|
|
||||
public Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds) |
|
||||
{ |
|
||||
return Task.FromResult(new List<WebhookSubscriptionInfo>()); |
|
||||
} |
|
||||
|
|
||||
public Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds, string webhookName) |
|
||||
{ |
|
||||
return Task.FromResult(new List<WebhookSubscriptionInfo>()); |
|
||||
} |
|
||||
|
|
||||
public Task<bool> IsSubscribedAsync(Guid? tenantId, string webhookName) |
|
||||
{ |
|
||||
return Task.FromResult(false); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,67 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Volo.Abp.Localization; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public class WebhookDefinition |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Unique name of the webhook.
|
|
||||
/// </summary>
|
|
||||
public string Name { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Tries to send webhook only one time without checking to send attempt count
|
|
||||
/// </summary>
|
|
||||
public bool TryOnce { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Defined maximum number of sending times
|
|
||||
/// </summary>
|
|
||||
public int MaxSendAttemptCount { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Display name of the webhook.
|
|
||||
/// Optional.
|
|
||||
/// </summary>
|
|
||||
public ILocalizableString DisplayName { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Description for the webhook.
|
|
||||
/// Optional.
|
|
||||
/// </summary>
|
|
||||
public ILocalizableString Description { get; set; } |
|
||||
|
|
||||
public List<string> 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<string>(); |
|
||||
} |
|
||||
|
|
||||
public WebhookDefinition WithFeature(params string[] features) |
|
||||
{ |
|
||||
if (!features.IsNullOrEmpty()) |
|
||||
{ |
|
||||
RequiredFeatures.AddRange(features); |
|
||||
} |
|
||||
|
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
public override string ToString() |
|
||||
{ |
|
||||
return $"[{nameof(WebhookDefinition)} {Name}]"; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,55 +0,0 @@ |
|||||
using JetBrains.Annotations; |
|
||||
using System.Collections.Generic; |
|
||||
using Volo.Abp; |
|
||||
using Volo.Abp.Localization; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public class WebhookDefinitionContext : IWebhookDefinitionContext |
|
||||
{ |
|
||||
protected Dictionary<string, WebhookGroupDefinition> Groups { get; } |
|
||||
|
|
||||
public WebhookDefinitionContext(Dictionary<string, WebhookGroupDefinition> 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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,139 +0,0 @@ |
|||||
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<string, WebhookGroupDefinition> WebhookGroupDefinitions => _lazyWebhookGroupDefinitions.Value; |
|
||||
private readonly Lazy<Dictionary<string, WebhookGroupDefinition>> _lazyWebhookGroupDefinitions; |
|
||||
|
|
||||
protected IDictionary<string, WebhookDefinition> WebhookDefinitions => _lazyWebhookDefinitions.Value; |
|
||||
private readonly Lazy<Dictionary<string, WebhookDefinition>> _lazyWebhookDefinitions; |
|
||||
|
|
||||
private readonly IServiceProvider _serviceProvider; |
|
||||
private readonly AbpWebhooksOptions _options; |
|
||||
|
|
||||
public WebhookDefinitionManager( |
|
||||
IServiceProvider serviceProvider, |
|
||||
IOptions<AbpWebhooksOptions> options) |
|
||||
{ |
|
||||
_serviceProvider = serviceProvider; |
|
||||
_options = options.Value; |
|
||||
|
|
||||
_lazyWebhookGroupDefinitions = new Lazy<Dictionary<string, WebhookGroupDefinition>>(CreateWebhookGroupDefinitions); |
|
||||
_lazyWebhookDefinitions = new Lazy<Dictionary<string, WebhookDefinition>>(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<WebhookDefinition> GetAll() |
|
||||
{ |
|
||||
return WebhookDefinitions.Values.ToImmutableList(); |
|
||||
} |
|
||||
|
|
||||
public IReadOnlyList<WebhookGroupDefinition> GetGroups() |
|
||||
{ |
|
||||
return WebhookGroupDefinitions.Values.ToImmutableList(); |
|
||||
} |
|
||||
|
|
||||
public async Task<bool> 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<ICurrentTenant>(); |
|
||||
var featureChecker = _serviceProvider.GetRequiredService<IFeatureChecker>(); |
|
||||
using (currentTenant.Change(tenantId)) |
|
||||
{ |
|
||||
if (!await featureChecker.IsEnabledAsync(true, webhookDefinition.RequiredFeatures.ToArray())) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
protected virtual Dictionary<string, WebhookDefinition> CreateWebhookDefinitions() |
|
||||
{ |
|
||||
var definitions = new Dictionary<string, WebhookDefinition>(); |
|
||||
|
|
||||
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<string, WebhookGroupDefinition> CreateWebhookGroupDefinitions() |
|
||||
{ |
|
||||
var definitions = new Dictionary<string, WebhookGroupDefinition>(); |
|
||||
|
|
||||
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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,13 +0,0 @@ |
|||||
using Volo.Abp.DependencyInjection; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public abstract class WebhookDefinitionProvider : ITransientDependency |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Used to add/manipulate webhook definitions.
|
|
||||
/// </summary>
|
|
||||
/// <param name="context">Context</param>,
|
|
||||
public abstract void Define(IWebhookDefinitionContext context); |
|
||||
} |
|
||||
} |
|
||||
@ -1,30 +0,0 @@ |
|||||
using System; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Store created web hooks. To see who get that webhook check with <see cref="WebhookSendAttempt.WebhookEventId"/> and you can get <see cref="WebhookSendAttempt.WebhookSubscriptionId"/>
|
|
||||
/// </summary>
|
|
||||
public class WebhookEvent |
|
||||
{ |
|
||||
public Guid Id { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Webhook unique name <see cref="WebhookDefinition.Name"/>
|
|
||||
/// </summary>
|
|
||||
public string WebhookName { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Webhook data as JSON string.
|
|
||||
/// </summary>
|
|
||||
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; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,101 +0,0 @@ |
|||||
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<string, object> Properties { get; } |
|
||||
|
|
||||
private ILocalizableString _displayName; |
|
||||
public ILocalizableString DisplayName |
|
||||
{ |
|
||||
get { |
|
||||
return _displayName; |
|
||||
} |
|
||||
set { |
|
||||
_displayName = value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public IReadOnlyList<WebhookDefinition> Webhooks => _webhooks.ToImmutableList(); |
|
||||
private readonly List<WebhookDefinition> _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<string, object>(); |
|
||||
_webhooks = new List<WebhookDefinition>(); |
|
||||
} |
|
||||
|
|
||||
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}]"; |
|
||||
} |
|
||||
} |
|
||||
@ -1,18 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public class WebhookHeader |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// If true, webhook will only contain given headers. If false given headers will be added to predefined headers in subscription.
|
|
||||
/// Default is false
|
|
||||
/// </summary>
|
|
||||
public bool UseOnlyGivenHeaders { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// That headers will be sent with the webhook.
|
|
||||
/// </summary>
|
|
||||
public IDictionary<string, string> Headers { get; set; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,80 +0,0 @@ |
|||||
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<WebhookPayload> 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<string> GetSerializedBodyAsync(WebhookSenderArgs webhookSenderArgs) |
|
||||
{ |
|
||||
if (webhookSenderArgs.SendExactSameData) |
|
||||
{ |
|
||||
return webhookSenderArgs.Data; |
|
||||
} |
|
||||
|
|
||||
var payload = await GetWebhookPayloadAsync(webhookSenderArgs); |
|
||||
|
|
||||
var serializedBody = JsonConvert.SerializeObject(payload); |
|
||||
|
|
||||
return serializedBody; |
|
||||
} |
|
||||
|
|
||||
public abstract Task<Guid> InsertAndGetIdWebhookSendAttemptAsync(WebhookSenderArgs webhookSenderArgs); |
|
||||
|
|
||||
public abstract Task StoreResponseOnWebhookSendAttemptAsync(Guid webhookSendAttemptId, Guid? tenantId, HttpStatusCode? statusCode, string content); |
|
||||
} |
|
||||
} |
|
||||
@ -1,35 +0,0 @@ |
|||||
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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,39 +0,0 @@ |
|||||
using System; |
|
||||
using System.Net; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Table for store webhook work items. Each item stores web hook send attempt of <see cref="WebhookEvent"/> to subscribed tenants
|
|
||||
/// </summary>
|
|
||||
public class WebhookSendAttempt |
|
||||
{ |
|
||||
public Guid Id { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// <see cref="WebhookEvent"/> foreign id
|
|
||||
/// </summary>
|
|
||||
public Guid WebhookEventId { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// <see cref="WebhookSubscription"/> foreign id
|
|
||||
/// </summary>
|
|
||||
public Guid WebhookSubscriptionId { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Webhook response content that webhook endpoint send back
|
|
||||
/// </summary>
|
|
||||
public string Response { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Webhook response status code that webhook endpoint send back
|
|
||||
/// </summary>
|
|
||||
public HttpStatusCode? ResponseStatusCode { get; set; } |
|
||||
|
|
||||
public DateTime CreationTime { get; set; } |
|
||||
|
|
||||
public DateTime? LastModificationTime { get; set; } |
|
||||
|
|
||||
public Guid? TenantId { get; set; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,62 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public class WebhookSenderArgs |
|
||||
{ |
|
||||
public Guid? TenantId { get; set; } |
|
||||
|
|
||||
//Webhook information
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// <see cref="WebhookEvent"/> foreign id
|
|
||||
/// </summary>
|
|
||||
public Guid WebhookEventId { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Webhook unique name
|
|
||||
/// </summary>
|
|
||||
public string WebhookName { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Webhook data as JSON string.
|
|
||||
/// </summary>
|
|
||||
public string Data { get; set; } |
|
||||
|
|
||||
//Subscription information
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// <see cref="WebhookSubscription"/> foreign id
|
|
||||
/// </summary>
|
|
||||
public Guid WebhookSubscriptionId { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Subscription webhook endpoint
|
|
||||
/// </summary>
|
|
||||
public string WebhookUri { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Webhook secret
|
|
||||
/// </summary>
|
|
||||
public string Secret { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a set of additional HTTP headers.That headers will be sent with the webhook.
|
|
||||
/// </summary>
|
|
||||
public IDictionary<string, string> Headers { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// True: It sends the exact same data as the parameter to clients.
|
|
||||
/// <para>
|
|
||||
/// False: It sends data in <see cref="WebhookPayload"/>. It is recommended way.
|
|
||||
/// </para>
|
|
||||
/// </summary>
|
|
||||
public bool SendExactSameData { get; set; } |
|
||||
|
|
||||
public WebhookSenderArgs() |
|
||||
{ |
|
||||
Headers = new Dictionary<string, string>(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,60 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Webhooks |
|
||||
{ |
|
||||
public class WebhookSubscriptionInfo |
|
||||
{ |
|
||||
public Guid Id { get; set; } |
|
||||
/// <summary>
|
|
||||
/// Subscribed Tenant's id .
|
|
||||
/// </summary>
|
|
||||
public Guid? TenantId { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Subscription webhook endpoint
|
|
||||
/// </summary>
|
|
||||
public string WebhookUri { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Webhook secret
|
|
||||
/// </summary>
|
|
||||
public string Secret { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Is subscription active
|
|
||||
/// </summary>
|
|
||||
public bool IsActive { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Subscribed webhook definitions unique names.It contains webhook definitions list as json
|
|
||||
/// <para>
|
|
||||
/// Do not change it manually.
|
|
||||
/// Use <see cref=" WebhookSubscriptionInfoExtensions.GetSubscribedWebhooks"/>,
|
|
||||
/// <see cref=" WebhookSubscriptionInfoExtensions.SubscribeWebhook"/>,
|
|
||||
/// <see cref="WebhookSubscriptionInfoExtensions.UnsubscribeWebhook"/> and
|
|
||||
/// <see cref="WebhookSubscriptionInfoExtensions.RemoveAllSubscribedWebhooks"/> to change it.
|
|
||||
/// </para>
|
|
||||
/// </summary>
|
|
||||
public List<string> Webhooks { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a set of additional HTTP headers.That headers will be sent with the webhook. It contains webhook header dictionary as json
|
|
||||
/// <para>
|
|
||||
/// Do not change it manually.
|
|
||||
/// Use <see cref=" WebhookSubscriptionInfoExtensions.GetWebhookHeaders"/>,
|
|
||||
/// <see cref="WebhookSubscriptionInfoExtensions.AddWebhookHeader"/>,
|
|
||||
/// <see cref="WebhookSubscriptionInfoExtensions.RemoveWebhookHeader"/>,
|
|
||||
/// <see cref="WebhookSubscriptionInfoExtensions.RemoveAllWebhookHeaders"/> to change it.
|
|
||||
/// </para>
|
|
||||
/// </summary>
|
|
||||
public IDictionary<string, string> Headers { get; set; } |
|
||||
|
|
||||
public WebhookSubscriptionInfo() |
|
||||
{ |
|
||||
IsActive = true; |
|
||||
Headers = new Dictionary<string, string>(); |
|
||||
Webhooks = new List<string>(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,172 +0,0 @@ |
|||||
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<WebhookSubscriptionInfo> GetAsync(Guid id) |
|
||||
{ |
|
||||
return await WebhookSubscriptionsStore.GetAsync(id); |
|
||||
} |
|
||||
|
|
||||
public virtual async Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsAsync(Guid? tenantId) |
|
||||
{ |
|
||||
return await WebhookSubscriptionsStore.GetAllSubscriptionsAsync(tenantId); |
|
||||
} |
|
||||
|
|
||||
public virtual async Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsIfFeaturesGrantedAsync(Guid? tenantId, string webhookName) |
|
||||
{ |
|
||||
if (!await _webhookDefinitionManager.IsAvailableAsync(tenantId, webhookName)) |
|
||||
{ |
|
||||
return new List<WebhookSubscriptionInfo>(); |
|
||||
} |
|
||||
|
|
||||
return (await WebhookSubscriptionsStore.GetAllSubscriptionsAsync(tenantId, webhookName)).ToList(); |
|
||||
} |
|
||||
|
|
||||
public virtual async Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds) |
|
||||
{ |
|
||||
return (await WebhookSubscriptionsStore.GetAllSubscriptionsOfTenantsAsync(tenantIds)).ToList(); |
|
||||
} |
|
||||
|
|
||||
public virtual async Task<List<WebhookSubscriptionInfo>> GetAllSubscriptionsOfTenantsIfFeaturesGrantedAsync(Guid?[] tenantIds, string webhookName) |
|
||||
{ |
|
||||
var featureGrantedTenants = new List<Guid?>(); |
|
||||
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<bool> 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
|
|
||||
} |
|
||||
} |
|
||||
@ -1,13 +0,0 @@ |
|||||
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)); |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue