diff --git a/BTCPayServer/Controllers/StoresController.Integrations.cs b/BTCPayServer/Controllers/StoresController.Integrations.cs index 57fd55e06..b21addc63 100644 --- a/BTCPayServer/Controllers/StoresController.Integrations.cs +++ b/BTCPayServer/Controllers/StoresController.Integrations.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Linq; using System.Threading.Tasks; using BTCPayServer.Data; @@ -14,7 +15,7 @@ namespace BTCPayServer.Controllers [HttpGet("{storeId}/integrations")] public IActionResult Integrations() { - return View("Integrations",new IntegrationsViewModel()); + return View("Integrations", new IntegrationsViewModel()); } [HttpGet("{storeId}/webhooks")] @@ -109,6 +110,30 @@ namespace BTCPayServer.Controllers return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id }); } + [HttpGet("{storeId}/webhooks/{webhookId}/test")] + public async Task TestWebhook(string webhookId) + { + var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId); + if (webhook is null) + return NotFound(); + + return View(nameof(TestWebhook)); + } + + [HttpPost("{storeId}/webhooks/{webhookId}/test")] + public async Task TestWebhook(string webhookId, TestWebhookViewModel viewModel) + { + var result = await WebhookNotificationManager.TestWebhook(CurrentStore.Id, webhookId, viewModel.Type); + + if (result.Success) { + TempData[WellKnownTempData.SuccessMessage] = $"{viewModel.Type.ToString()} event delivered successfully! Delivery ID is {result.DeliveryId}"; + } else { + TempData[WellKnownTempData.ErrorMessage] = $"{viewModel.Type.ToString()} event could not be delivered. Error message received: {(result.ErrorMessage ?? "unknown")}"; + } + + return View(nameof(TestWebhook)); + } + [HttpPost("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")] public async Task RedeliverWebhook(string webhookId, string deliveryId) { diff --git a/BTCPayServer/HostedServices/WebhookNotificationManager.cs b/BTCPayServer/HostedServices/WebhookNotificationManager.cs index ee4d90f2a..a73c96c2d 100644 --- a/BTCPayServer/HostedServices/WebhookNotificationManager.cs +++ b/BTCPayServer/HostedServices/WebhookNotificationManager.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Linq; @@ -70,7 +71,7 @@ namespace BTCPayServer.HostedServices Subscribe(); } - public async Task Redeliver(string deliveryId) + public async Task Redeliver(string deliveryId) { var deliveryRequest = await CreateRedeliveryRequest(deliveryId); if (deliveryRequest is null) @@ -79,7 +80,7 @@ namespace BTCPayServer.HostedServices return deliveryRequest.Delivery.Id; } - private async Task CreateRedeliveryRequest(string deliveryId) + private async Task CreateRedeliveryRequest(string deliveryId) { using var ctx = StoreRepository.CreateDbContext(); var webhookDelivery = await ctx.WebhookDeliveries.AsNoTracking() @@ -93,8 +94,7 @@ namespace BTCPayServer.HostedServices if (webhookDelivery is null) return null; var oldDeliveryBlob = webhookDelivery.Delivery.GetBlob(); - var newDelivery = NewDelivery(); - newDelivery.WebhookId = webhookDelivery.Webhook.Id; + var newDelivery = NewDelivery(webhookDelivery.Webhook.Id); var newDeliveryBlob = new WebhookDeliveryBlob(); newDeliveryBlob.Request = oldDeliveryBlob.Request; var webhookEvent = newDeliveryBlob.ReadRequestAs(); @@ -107,6 +107,35 @@ namespace BTCPayServer.HostedServices newDelivery.SetBlob(newDeliveryBlob); return new WebhookDeliveryRequest(webhookDelivery.Webhook.Id, webhookEvent, newDelivery, webhookDelivery.Webhook.GetBlob()); } + + private WebhookEvent GetTestWebHook(string storeId, string webhookId, WebhookEventType webhookEventType, Data.WebhookDeliveryData delivery) + { + var webhookEvent = GetWebhookEvent(webhookEventType); + webhookEvent.InvoiceId = "__test__" + Guid.NewGuid().ToString() + "__test__"; + webhookEvent.StoreId = storeId; + webhookEvent.DeliveryId = delivery.Id; + webhookEvent.WebhookId = webhookId; + webhookEvent.OriginalDeliveryId = "__test__" + Guid.NewGuid().ToString() + "__test__"; + webhookEvent.IsRedelivery = false; + webhookEvent.Timestamp = delivery.Timestamp; + + return webhookEvent; + } + + public async Task TestWebhook(string storeId, string webhookId, WebhookEventType webhookEventType) + { + var delivery = NewDelivery(webhookId); + var webhook = (await StoreRepository.GetWebhooks(storeId)).FirstOrDefault(w => w.Id == webhookId); + var deliveryRequest = new WebhookDeliveryRequest( + webhookId, + GetTestWebHook(storeId, webhookId, webhookEventType, delivery), + delivery, + webhook.GetBlob() + ); + + return await SendDelivery(deliveryRequest); + } + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) { if (evt is InvoiceEvent invoiceEvent) @@ -119,8 +148,7 @@ namespace BTCPayServer.HostedServices continue; if (!ShouldDeliver(webhookEvent.Type, webhookBlob)) continue; - Data.WebhookDeliveryData delivery = NewDelivery(); - delivery.WebhookId = webhook.Id; + Data.WebhookDeliveryData delivery = NewDelivery(webhook.Id); webhookEvent.InvoiceId = invoiceEvent.InvoiceId; webhookEvent.StoreId = invoiceEvent.Invoice.StoreId; webhookEvent.DeliveryId = delivery.Id; @@ -147,7 +175,28 @@ namespace BTCPayServer.HostedServices _ = Process(context.WebhookId, channel); } - private WebhookInvoiceEvent GetWebhookEvent(InvoiceEvent invoiceEvent) + private WebhookInvoiceEvent GetWebhookEvent(WebhookEventType webhookEventType) + { + switch (webhookEventType) + { + case WebhookEventType.InvoiceCreated: + return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated); + case WebhookEventType.InvoiceReceivedPayment: + return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoiceReceivedPayment); + case WebhookEventType.InvoiceProcessing: + return new WebhookInvoiceProcessingEvent(WebhookEventType.InvoiceProcessing); + case WebhookEventType.InvoiceExpired: + return new WebhookInvoiceExpiredEvent(WebhookEventType.InvoiceExpired); + case WebhookEventType.InvoiceSettled: + return new WebhookInvoiceSettledEvent(WebhookEventType.InvoiceSettled); + case WebhookEventType.InvoiceInvalid: + return new WebhookInvoiceInvalidEvent(WebhookEventType.InvoiceInvalid); + default: + return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated); + } + } + + private WebhookInvoiceEvent? GetWebhookEvent(InvoiceEvent invoiceEvent) { var eventCode = invoiceEvent.EventCode; switch (eventCode) @@ -200,7 +249,7 @@ namespace BTCPayServer.HostedServices var wh = (await StoreRepository.GetWebhook(ctx.WebhookId))?.GetBlob(); if (wh is null || !ShouldDeliver(ctx.WebhookEvent.Type, wh)) continue; - var result = await SendDelivery(ctx); + var result = await SendAndSaveDelivery(ctx); if (ctx.WebhookBlob.AutomaticRedelivery && !result.Success && result.DeliveryId is string) @@ -208,15 +257,15 @@ namespace BTCPayServer.HostedServices var originalDeliveryId = result.DeliveryId; foreach (var wait in new[] { - TimeSpan.FromSeconds(10), - TimeSpan.FromMinutes(1), - TimeSpan.FromMinutes(10), - TimeSpan.FromMinutes(10), - TimeSpan.FromMinutes(10), - TimeSpan.FromMinutes(10), - TimeSpan.FromMinutes(10), - TimeSpan.FromMinutes(10), - }) + TimeSpan.FromSeconds(10), + TimeSpan.FromMinutes(1), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(10), + }) { await Task.Delay(wait, CancellationToken); ctx = await CreateRedeliveryRequest(originalDeliveryId); @@ -224,7 +273,7 @@ namespace BTCPayServer.HostedServices if (!ctx.WebhookBlob.AutomaticRedelivery || !ShouldDeliver(ctx.WebhookEvent.Type, ctx.WebhookBlob)) break; - result = await SendDelivery(ctx); + result = await SendAndSaveDelivery(ctx); if (result.Success) break; } @@ -246,11 +295,13 @@ namespace BTCPayServer.HostedServices return wh.Active && wh.AuthorizedEvents.Match(type); } - class DeliveryResult + public class DeliveryResult { - public string DeliveryId { get; set; } + public string? DeliveryId { get; set; } public bool Success { get; set; } + public string? ErrorMessage { get; set; } } + private async Task SendDelivery(WebhookDeliveryRequest ctx) { var uri = new Uri(ctx.WebhookBlob.Url, UriKind.Absolute); @@ -287,8 +338,22 @@ namespace BTCPayServer.HostedServices deliveryBlob.ErrorMessage = ex.Message; } ctx.Delivery.SetBlob(deliveryBlob); + + return new DeliveryResult() + { + Success = deliveryBlob.ErrorMessage is null, + DeliveryId = ctx.Delivery.Id, + ErrorMessage = deliveryBlob.ErrorMessage + }; + } + + + private async Task SendAndSaveDelivery(WebhookDeliveryRequest ctx) + { + var result = await SendDelivery(ctx); await StoreRepository.AddWebhookDelivery(ctx.Delivery); - return new DeliveryResult() { Success = deliveryBlob.ErrorMessage is null, DeliveryId = ctx.Delivery.Id }; + + return result; } private byte[] ToBytes(WebhookEvent webhookEvent) @@ -298,12 +363,14 @@ namespace BTCPayServer.HostedServices return bytes; } - private static Data.WebhookDeliveryData NewDelivery() + private static Data.WebhookDeliveryData NewDelivery(string webhookId) { - var delivery = new Data.WebhookDeliveryData(); - delivery.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); - delivery.Timestamp = DateTimeOffset.UtcNow; - return delivery; + return new Data.WebhookDeliveryData + { + Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)), + Timestamp = DateTimeOffset.UtcNow, + WebhookId = webhookId + }; } } } diff --git a/BTCPayServer/Models/StoreViewModels/TestWebhookViewModel.cs b/BTCPayServer/Models/StoreViewModels/TestWebhookViewModel.cs new file mode 100644 index 000000000..d5fe1c284 --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/TestWebhookViewModel.cs @@ -0,0 +1,9 @@ +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Models.StoreViewModels +{ + public class TestWebhookViewModel + { + public WebhookEventType Type { get; set; } + } +} diff --git a/BTCPayServer/Views/Stores/TestWebhook.cshtml b/BTCPayServer/Views/Stores/TestWebhook.cshtml new file mode 100644 index 000000000..aee8540b2 --- /dev/null +++ b/BTCPayServer/Views/Stores/TestWebhook.cshtml @@ -0,0 +1,26 @@ +@model EditWebhookViewModel +@using BTCPayServer.Client.Models; +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData.SetActivePageAndTitle(StoreNavPages.Webhooks, "Send a test event to a webhook endpoint", Context.GetStoreData().StoreName); +} + +
+
+
+

@ViewData["PageTitle"]

+ +
+ + +
+ + +
+
+
diff --git a/BTCPayServer/Views/Stores/Webhooks.cshtml b/BTCPayServer/Views/Stores/Webhooks.cshtml index dbc290ffb..2bafad1c1 100644 --- a/BTCPayServer/Views/Stores/Webhooks.cshtml +++ b/BTCPayServer/Views/Stores/Webhooks.cshtml @@ -30,7 +30,9 @@ @wh.Url - Modify - Delete + Test - + Modify - + Delete }