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 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> 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(); } }