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(Guid repoId, 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 { RepositoryId = repoId, TargetUrl = targetUrl, EventType = eventType, Secret = secret, IsActive = true }; await context.Webhooks.AddAsync(webhook); await context.SaveChangesAsync(); } public async Task ToggleWebhookAsync(Guid repoId, int webhookId, bool activated) { var hook = await context.Webhooks.FindAsync(webhookId); if (hook == null || hook.RepositoryId != repoId) return; if (hook.IsActive == activated) return; hook.IsActive = activated; context.Webhooks.Update(hook); await context.SaveChangesAsync(); } public async Task DeleteWebhookAsync(Guid repoId, int webhookId) { var hook = await context.Webhooks.FindAsync(webhookId); if (hook == null || hook.RepositoryId != repoId) return; context.Webhooks.Remove(hook); await context.SaveChangesAsync(); } public async Task> GetWebhooksAsync(Guid repoId) { return await context.Webhooks .Where(w => w.RepositoryId == repoId) .ToListAsync(); } public async Task TriggerWebhooksAsync(Guid repoId, WebhookEventType eventType, object payload) { if (!settings.UseWebHook) return; var hooks = await context.Webhooks .Where(w => w.RepositoryId == repoId && 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(); } }