119 lines
4.0 KiB
C#
119 lines
4.0 KiB
C#
using System.Net;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using Flawless.Communication.Shared;
|
||
using Flawless.Server.Models;
|
||
using Microsoft.EntityFrameworkCore;
|
||
|
||
namespace Flawless.Server.Services;
|
||
|
||
public class WebhookService(
|
||
SettingFacade settings,
|
||
AppDbContext context,
|
||
IHttpClientFactory httpFactory,
|
||
ILogger<WebhookService> logger)
|
||
{
|
||
public async Task AddWebhookAsync(Repository repo, string targetUrl, WebhookEventType eventType, string? secret)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(targetUrl) || !Uri.TryCreate(targetUrl, UriKind.Absolute, out _))
|
||
throw new ArgumentException("No valid target URL provided");
|
||
|
||
var webhook = new Webhook {
|
||
Repository = repo,
|
||
TargetUrl = targetUrl,
|
||
EventType = eventType,
|
||
Secret = secret,
|
||
IsActive = true
|
||
};
|
||
|
||
await context.Webhooks.AddAsync(webhook);
|
||
await context.SaveChangesAsync();
|
||
}
|
||
|
||
public async Task ToggleWebhookAsync(Repository repo, int webhookId, bool activated)
|
||
{
|
||
var hook = await context.Webhooks
|
||
.Include(x => x.Repository)
|
||
.FirstOrDefaultAsync(x => x.Id == webhookId);
|
||
|
||
if (hook == null || hook.Repository != repo) return;
|
||
|
||
if (hook.IsActive == activated) return;
|
||
|
||
hook.IsActive = activated;
|
||
context.Webhooks.Update(hook);
|
||
await context.SaveChangesAsync();
|
||
}
|
||
|
||
public async Task DeleteWebhookAsync(Repository repo, int webhookId)
|
||
{
|
||
var hook = await context.Webhooks
|
||
.Include(x => x.Repository)
|
||
.FirstOrDefaultAsync(x => x.Id == webhookId);
|
||
|
||
if (hook == null || hook.Repository != repo) return;
|
||
|
||
context.Webhooks.Remove(hook);
|
||
await context.SaveChangesAsync();
|
||
}
|
||
|
||
public async Task<IEnumerable<Webhook>> GetWebhooksAsync(Repository repo)
|
||
{
|
||
return await context.Webhooks
|
||
.Where(w => w.Repository == repo)
|
||
.ToListAsync();
|
||
}
|
||
|
||
public async Task TriggerWebhooksAsync(Repository repo, WebhookEventType eventType, object payload)
|
||
{
|
||
if (!settings.UseWebHook) return;
|
||
|
||
var hooks = await context.Webhooks
|
||
.Where(w => w.Repository == repo && w.EventType == eventType && w.IsActive)
|
||
.ToListAsync();
|
||
|
||
using var client = httpFactory.CreateClient();
|
||
client.Timeout = TimeSpan.FromSeconds(settings.WebhookTimeout);
|
||
|
||
foreach (var hook in hooks)
|
||
{
|
||
for (var retry = 0; retry < settings.WebhookMaxRetries; retry++)
|
||
{
|
||
try
|
||
{
|
||
var content = new StringContent(JsonSerializer.Serialize(payload),
|
||
Encoding.UTF8, "application/json");
|
||
|
||
if (hook.Secret != null)
|
||
{
|
||
var signature = HMACSHA256.HashData(
|
||
Encoding.UTF8.GetBytes(hook.Secret),
|
||
await content.ReadAsByteArrayAsync());
|
||
|
||
content.Headers.Add("X-Signature", $"sha256={Convert.ToHexString(signature)}");
|
||
}
|
||
|
||
var response = await client.PostAsync(hook.TargetUrl, content);
|
||
if (response.IsSuccessStatusCode) break;
|
||
|
||
logger.LogWarning($"Webhook {hook.Id} Failed:{response.StatusCode}");
|
||
await Task.Delay(1000 * (int)Math.Pow(2, retry)); // 指数退避
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.LogError(ex, $"Webhook {hook.Id} Failed for {retry + 1} times.");
|
||
if (retry == settings.WebhookMaxRetries - 1)
|
||
await DeactivateFailedWebhook(hook);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task DeactivateFailedWebhook(Webhook hook)
|
||
{
|
||
hook.IsActive = false;
|
||
context.Webhooks.Update(hook);
|
||
await context.SaveChangesAsync();
|
||
}
|
||
} |