diff --git a/BTCPayServer/HostedServices/Webhooks/IWebhookProvider.cs b/BTCPayServer/HostedServices/Webhooks/IWebhookProvider.cs index ed795aff1..d4661687b 100644 --- a/BTCPayServer/HostedServices/Webhooks/IWebhookProvider.cs +++ b/BTCPayServer/HostedServices/Webhooks/IWebhookProvider.cs @@ -6,7 +6,7 @@ namespace BTCPayServer.HostedServices.Webhooks; public interface IWebhookProvider { - public Dictionary GetSupportedWebhookTypes(); - - public WebhookEvent CreateTestEvent(string type, params object[] args); + public Dictionary GetSupportedWebhookTypes(); + + public WebhookEvent CreateTestEvent(string type, params object[] args); } diff --git a/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookDeliveryRequest.cs b/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookDeliveryRequest.cs index 6224606ec..b1e7df8c9 100644 --- a/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookDeliveryRequest.cs +++ b/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookDeliveryRequest.cs @@ -4,25 +4,26 @@ using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.Services.Invoices; +using MimeKit; using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData; namespace BTCPayServer.HostedServices.Webhooks; -public class InvoiceWebhookDeliveryRequest : WebhookSender.WebhookDeliveryRequest +public class InvoiceWebhookDeliveryRequest( + InvoiceEntity invoice, + string webhookId, + WebhookEvent webhookEvent, + WebhookDeliveryData delivery, + WebhookBlob webhookBlob) + : WebhookSender.WebhookDeliveryRequest(webhookId, webhookEvent, delivery, webhookBlob) { - public InvoiceEntity Invoice { get; } - - public InvoiceWebhookDeliveryRequest(InvoiceEntity invoice, string webhookId, WebhookEvent webhookEvent, - WebhookDeliveryData delivery, WebhookBlob webhookBlob) : base(webhookId, webhookEvent, delivery, webhookBlob) - { - Invoice = invoice; - } + public InvoiceEntity Invoice { get; } = invoice; public override Task Interpolate(SendEmailRequest req, UIStoresController.StoreEmailRule storeEmailRule) { if (storeEmailRule.CustomerEmail && - MailboxAddressValidator.TryParse(Invoice.Metadata.BuyerEmail, out var bmb)) + MailboxAddressValidator.TryParse(Invoice.Metadata.BuyerEmail, out MailboxAddress bmb)) { req.Email ??= string.Empty; req.Email += $",{bmb}"; @@ -35,7 +36,7 @@ public class InvoiceWebhookDeliveryRequest : WebhookSender.WebhookDeliveryReques private string Interpolate(string str) { - var res = str.Replace("{Invoice.Id}", Invoice.Id) + string res = str.Replace("{Invoice.Id}", Invoice.Id) .Replace("{Invoice.StoreId}", Invoice.StoreId) .Replace("{Invoice.Price}", Invoice.Price.ToString(CultureInfo.InvariantCulture)) .Replace("{Invoice.Currency}", Invoice.Currency) @@ -47,5 +48,4 @@ public class InvoiceWebhookDeliveryRequest : WebhookSender.WebhookDeliveryReques res = InterpolateJsonField(res, "Invoice.Metadata", Invoice.Metadata.ToJObject()); return res; } - } diff --git a/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookProvider.cs b/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookProvider.cs index 107634d9d..62f54d2a9 100644 --- a/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookProvider.cs +++ b/BTCPayServer/HostedServices/Webhooks/InvoiceWebhookProvider.cs @@ -4,70 +4,65 @@ using BTCPayServer.Client.Models; using BTCPayServer.Controllers.Greenfield; using BTCPayServer.Data; using BTCPayServer.Events; -using BTCPayServer.Services.Invoices; using Microsoft.Extensions.Logging; using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData; namespace BTCPayServer.HostedServices.Webhooks; -public class InvoiceWebhookProvider : WebhookProvider +public class InvoiceWebhookProvider( + WebhookSender webhookSender, + EventAggregator eventAggregator, + ILogger logger) + : WebhookProvider(eventAggregator, logger, webhookSender) { - public InvoiceWebhookProvider(WebhookSender webhookSender, EventAggregator eventAggregator, - ILogger logger) : base( - eventAggregator, logger, webhookSender) - { - } - public override Dictionary GetSupportedWebhookTypes() { return new Dictionary { - {WebhookEventType.InvoiceCreated, "Invoice - Created"}, - {WebhookEventType.InvoiceReceivedPayment, "Invoice - Received Payment"}, - {WebhookEventType.InvoicePaymentSettled, "Invoice - Payment Settled"}, - {WebhookEventType.InvoiceProcessing, "Invoice - Is Processing"}, - {WebhookEventType.InvoiceExpired, "Invoice - Expired"}, - {WebhookEventType.InvoiceSettled, "Invoice - Is Settled"}, - {WebhookEventType.InvoiceInvalid, "Invoice - Became Invalid"}, - {WebhookEventType.InvoiceExpiredPaidPartial, "Invoice - Expired Paid Partial"}, - {WebhookEventType.InvoicePaidAfterExpiration, "Invoice - Expired Paid Late"}, + { WebhookEventType.InvoiceCreated, "Invoice - Created" }, + { WebhookEventType.InvoiceReceivedPayment, "Invoice - Received Payment" }, + { WebhookEventType.InvoicePaymentSettled, "Invoice - Payment Settled" }, + { WebhookEventType.InvoiceProcessing, "Invoice - Is Processing" }, + { WebhookEventType.InvoiceExpired, "Invoice - Expired" }, + { WebhookEventType.InvoiceSettled, "Invoice - Is Settled" }, + { WebhookEventType.InvoiceInvalid, "Invoice - Became Invalid" }, + { WebhookEventType.InvoiceExpiredPaidPartial, "Invoice - Expired Paid Partial" }, + { WebhookEventType.InvoicePaidAfterExpiration, "Invoice - Expired Paid Late" } }; } protected override WebhookSender.WebhookDeliveryRequest CreateDeliveryRequest(InvoiceEvent invoiceEvent, WebhookData webhook) { - var webhookEvent = GetWebhookEvent(invoiceEvent)!; - var webhookBlob = webhook?.GetBlob(); + WebhookInvoiceEvent webhookEvent = GetWebhookEvent(invoiceEvent)!; + WebhookBlob webhookBlob = webhook?.GetBlob(); webhookEvent.InvoiceId = invoiceEvent.InvoiceId; webhookEvent.StoreId = invoiceEvent.Invoice.StoreId; webhookEvent.Metadata = invoiceEvent.Invoice.Metadata.ToJObject(); webhookEvent.WebhookId = webhook?.Id; webhookEvent.IsRedelivery = false; - WebhookDeliveryData delivery = webhook is null? null: WebhookExtensions.NewWebhookDelivery(webhook.Id); + WebhookDeliveryData delivery = webhook is null ? null : WebhookExtensions.NewWebhookDelivery(webhook.Id); if (delivery is not null) { webhookEvent.DeliveryId = delivery.Id; webhookEvent.OriginalDeliveryId = delivery.Id; webhookEvent.Timestamp = delivery.Timestamp; } + return new InvoiceWebhookDeliveryRequest(invoiceEvent.Invoice, webhook?.Id, webhookEvent, delivery, webhookBlob); } public override WebhookEvent CreateTestEvent(string type, params object[] args) { - var storeId = args[0].ToString(); - return new WebhookInvoiceEvent(type, storeId) - { - InvoiceId = "__test__" + Guid.NewGuid() + "__test__" - }; + string storeId = args[0].ToString(); + return new WebhookInvoiceEvent(type, storeId) { InvoiceId = "__test__" + Guid.NewGuid() + "__test__" }; } protected override WebhookInvoiceEvent GetWebhookEvent(InvoiceEvent invoiceEvent) { - var eventCode = invoiceEvent.EventCode; - var storeId = invoiceEvent.Invoice.StoreId; + InvoiceEventCode eventCode = invoiceEvent.EventCode; + string storeId = invoiceEvent.Invoice.StoreId; switch (eventCode) { case InvoiceEventCode.Confirmed: @@ -80,21 +75,12 @@ public class InvoiceWebhookProvider : WebhookProvider case InvoiceEventCode.Created: return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated, storeId); case InvoiceEventCode.Expired: - return new WebhookInvoiceExpiredEvent(storeId) - { - PartiallyPaid = invoiceEvent.PaidPartial - }; + return new WebhookInvoiceExpiredEvent(storeId) { PartiallyPaid = invoiceEvent.PaidPartial }; case InvoiceEventCode.FailedToConfirm: case InvoiceEventCode.MarkedInvalid: - return new WebhookInvoiceInvalidEvent(storeId) - { - ManuallyMarked = eventCode == InvoiceEventCode.MarkedInvalid - }; + return new WebhookInvoiceInvalidEvent(storeId) { ManuallyMarked = eventCode == InvoiceEventCode.MarkedInvalid }; case InvoiceEventCode.PaidInFull: - return new WebhookInvoiceProcessingEvent(storeId) - { - OverPaid = invoiceEvent.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver - }; + return new WebhookInvoiceProcessingEvent(storeId) { OverPaid = invoiceEvent.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver }; case InvoiceEventCode.ReceivedPayment: return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoiceReceivedPayment, storeId) { diff --git a/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookDeliveryRequest.cs b/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookDeliveryRequest.cs index 24c7d7fc1..40edbe7ab 100644 --- a/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookDeliveryRequest.cs +++ b/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookDeliveryRequest.cs @@ -5,50 +5,49 @@ using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.Services.PaymentRequests; +using MimeKit; using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData; namespace BTCPayServer.HostedServices.Webhooks; -public class PaymentRequestWebhookDeliveryRequest : WebhookSender.WebhookDeliveryRequest +public class PaymentRequestWebhookDeliveryRequest( + PaymentRequestEvent evt, + string webhookId, + WebhookEvent webhookEvent, + WebhookDeliveryData delivery, + WebhookBlob webhookBlob) + : WebhookSender.WebhookDeliveryRequest(webhookId, webhookEvent, delivery, webhookBlob) { - private readonly PaymentRequestEvent _evt; - - public PaymentRequestWebhookDeliveryRequest(PaymentRequestEvent evt, string webhookId, WebhookEvent webhookEvent, - WebhookDeliveryData delivery, WebhookBlob webhookBlob) : base(webhookId, webhookEvent, delivery, webhookBlob) - { - _evt = evt; - } - public override Task Interpolate(SendEmailRequest req, UIStoresController.StoreEmailRule storeEmailRule) { - var blob = _evt.Data.GetBlob(); + PaymentRequestBlob? blob = evt.Data.GetBlob(); if (storeEmailRule.CustomerEmail && - MailboxAddressValidator.TryParse(blob.Email, out var bmb)) + MailboxAddressValidator.TryParse(blob.Email, out MailboxAddress? bmb)) { req.Email ??= string.Empty; req.Email += $",{bmb}"; } - req.Subject = Interpolate(req.Subject, _evt.Data); - req.Body = Interpolate(req.Body, _evt.Data); + req.Subject = Interpolate(req.Subject, evt.Data); + req.Body = Interpolate(req.Body, evt.Data); return Task.FromResult(req)!; } - private string Interpolate(string str, Data.PaymentRequestData data) + private string Interpolate(string str, PaymentRequestData data) { - var id = data.Id; + string? id = data.Id; string trimmedId = $"{id.Substring(0, 7)}...{id.Substring(id.Length - 7)}"; - - var blob = data.GetBlob(); - var res = str.Replace("{PaymentRequest.Id}", id) + + PaymentRequestBlob? blob = data.GetBlob(); + string res = str.Replace("{PaymentRequest.Id}", id) .Replace("{PaymentRequest.TrimmedId}", trimmedId) .Replace("{PaymentRequest.Amount}", data.Amount.ToString(CultureInfo.InvariantCulture)) .Replace("{PaymentRequest.Currency}", data.Currency) .Replace("{PaymentRequest.Title}", blob.Title) .Replace("{PaymentRequest.Description}", blob.Description) .Replace("{PaymentRequest.ReferenceId}", data.ReferenceId) - .Replace("{PaymentRequest.Status}", _evt.Data.Status.ToString()); + .Replace("{PaymentRequest.Status}", evt.Data.Status.ToString()); res = InterpolateJsonField(res, "PaymentRequest.FormResponse", blob.FormResponse); return res; diff --git a/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookProvider.cs b/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookProvider.cs index 4df461544..f60e3e08f 100644 --- a/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookProvider.cs +++ b/BTCPayServer/HostedServices/Webhooks/PaymentRequestWebhookProvider.cs @@ -8,31 +8,25 @@ using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData; namespace BTCPayServer.HostedServices.Webhooks; -public class PaymentRequestWebhookProvider: WebhookProvider +public class PaymentRequestWebhookProvider(EventAggregator eventAggregator, ILogger logger, WebhookSender webhookSender) + : WebhookProvider(eventAggregator, logger, webhookSender) { - public PaymentRequestWebhookProvider(EventAggregator eventAggregator, ILogger logger, WebhookSender webhookSender) : base(eventAggregator, logger, webhookSender) - { - } - public override Dictionary GetSupportedWebhookTypes() { - return new Dictionary() + return new Dictionary { - {WebhookEventType.PaymentRequestCreated, "Payment Request - Created"}, - {WebhookEventType.PaymentRequestUpdated, "Payment Request - Updated"}, - {WebhookEventType.PaymentRequestArchived, "Payment Request - Archived"}, - {WebhookEventType.PaymentRequestStatusChanged, "Payment Request - Status Changed"}, - {WebhookEventType.PaymentRequestCompleted, "Payment Request - Completed"}, + { WebhookEventType.PaymentRequestCreated, "Payment Request - Created" }, + { WebhookEventType.PaymentRequestUpdated, "Payment Request - Updated" }, + { WebhookEventType.PaymentRequestArchived, "Payment Request - Archived" }, + { WebhookEventType.PaymentRequestStatusChanged, "Payment Request - Status Changed" }, + { WebhookEventType.PaymentRequestCompleted, "Payment Request - Completed" } }; } public override WebhookEvent CreateTestEvent(string type, object[] args) { - var storeId = args[0].ToString(); - return new WebhookPaymentRequestEvent(type, storeId) - { - PaymentRequestId = "__test__" + Guid.NewGuid() + "__test__" - }; + string storeId = args[0].ToString(); + return new WebhookPaymentRequestEvent(type, storeId) { PaymentRequestId = "__test__" + Guid.NewGuid() + "__test__" }; } protected override WebhookPaymentRequestEvent GetWebhookEvent(PaymentRequestEvent evt) @@ -50,20 +44,21 @@ public class PaymentRequestWebhookProvider: WebhookProvider protected override WebhookSender.WebhookDeliveryRequest CreateDeliveryRequest(PaymentRequestEvent paymentRequestEvent, WebhookData webhook) { - var webhookBlob = webhook?.GetBlob(); - var webhookEvent = GetWebhookEvent(paymentRequestEvent)!; + WebhookBlob webhookBlob = webhook?.GetBlob(); + WebhookPaymentRequestEvent webhookEvent = GetWebhookEvent(paymentRequestEvent)!; webhookEvent.StoreId = paymentRequestEvent.Data.StoreDataId; webhookEvent.PaymentRequestId = paymentRequestEvent.Data.Id; webhookEvent.Status = paymentRequestEvent.Data.Status; webhookEvent.WebhookId = webhook?.Id; webhookEvent.IsRedelivery = false; - WebhookDeliveryData delivery = webhook is null? null: WebhookExtensions.NewWebhookDelivery(webhook.Id); + WebhookDeliveryData delivery = webhook is null ? null : WebhookExtensions.NewWebhookDelivery(webhook.Id); if (delivery is not null) { webhookEvent.DeliveryId = delivery.Id; webhookEvent.OriginalDeliveryId = delivery.Id; webhookEvent.Timestamp = delivery.Timestamp; } - return new PaymentRequestWebhookDeliveryRequest(paymentRequestEvent,webhook?.Id, webhookEvent, delivery, webhookBlob ); + + return new PaymentRequestWebhookDeliveryRequest(paymentRequestEvent, webhook?.Id, webhookEvent, delivery, webhookBlob); } } diff --git a/BTCPayServer/HostedServices/Webhooks/PayoutWebhookDeliveryRequest.cs b/BTCPayServer/HostedServices/Webhooks/PayoutWebhookDeliveryRequest.cs index 569dcde1f..4c612daac 100644 --- a/BTCPayServer/HostedServices/Webhooks/PayoutWebhookDeliveryRequest.cs +++ b/BTCPayServer/HostedServices/Webhooks/PayoutWebhookDeliveryRequest.cs @@ -8,9 +8,13 @@ using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData; namespace BTCPayServer.HostedServices.Webhooks; -public class PayoutWebhookDeliveryRequest(PayoutEvent evt, string? webhookId, WebhookEvent webhookEvent, - WebhookDeliveryData? delivery, WebhookBlob? webhookBlob, - BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings) +public class PayoutWebhookDeliveryRequest( + PayoutEvent evt, + string? webhookId, + WebhookEvent webhookEvent, + WebhookDeliveryData? delivery, + WebhookBlob? webhookBlob, + BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings) : WebhookSender.WebhookDeliveryRequest(webhookId!, webhookEvent, delivery!, webhookBlob!) { public override Task Interpolate(SendEmailRequest req, @@ -23,8 +27,8 @@ public class PayoutWebhookDeliveryRequest(PayoutEvent evt, string? webhookId, We private string Interpolate(string str) { - var blob = evt.Payout.GetBlob(btcPayNetworkJsonSerializerSettings); - var res = str.Replace("{Payout.Id}", evt.Payout.Id) + PayoutBlob? blob = evt.Payout.GetBlob(btcPayNetworkJsonSerializerSettings); + string res = str.Replace("{Payout.Id}", evt.Payout.Id) .Replace("{Payout.PullPaymentId}", evt.Payout.PullPaymentDataId) .Replace("{Payout.Destination}", evt.Payout.DedupId ?? blob.Destination) .Replace("{Payout.State}", evt.Payout.State.ToString()); diff --git a/BTCPayServer/HostedServices/Webhooks/PayoutWebhookProvider.cs b/BTCPayServer/HostedServices/Webhooks/PayoutWebhookProvider.cs index 28874fafd..c4f42df86 100644 --- a/BTCPayServer/HostedServices/Webhooks/PayoutWebhookProvider.cs +++ b/BTCPayServer/HostedServices/Webhooks/PayoutWebhookProvider.cs @@ -4,51 +4,53 @@ using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Services; using Microsoft.Extensions.Logging; +using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData; namespace BTCPayServer.HostedServices.Webhooks; -public class PayoutWebhookProvider(EventAggregator eventAggregator, ILogger logger, - WebhookSender webhookSender, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings) +public class PayoutWebhookProvider( + EventAggregator eventAggregator, + ILogger logger, + WebhookSender webhookSender, + BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings) : WebhookProvider(eventAggregator, logger, webhookSender) { protected override WebhookSender.WebhookDeliveryRequest CreateDeliveryRequest(PayoutEvent payoutEvent, WebhookData webhook) { - var webhookBlob = webhook?.GetBlob(); + WebhookBlob webhookBlob = webhook?.GetBlob(); - var webhookEvent = GetWebhookEvent(payoutEvent)!; + WebhookPayoutEvent webhookEvent = GetWebhookEvent(payoutEvent)!; webhookEvent.StoreId = payoutEvent.Payout.StoreDataId; webhookEvent.PayoutId = payoutEvent.Payout.Id; webhookEvent.PayoutState = payoutEvent.Payout.State; webhookEvent.PullPaymentId = payoutEvent.Payout.PullPaymentDataId; webhookEvent.WebhookId = webhook?.Id; webhookEvent.IsRedelivery = false; - Data.WebhookDeliveryData delivery = webhook is null? null: WebhookExtensions.NewWebhookDelivery(webhook.Id); + WebhookDeliveryData delivery = webhook is null ? null : WebhookExtensions.NewWebhookDelivery(webhook.Id); if (delivery is not null) { webhookEvent.DeliveryId = delivery.Id; webhookEvent.OriginalDeliveryId = delivery.Id; webhookEvent.Timestamp = delivery.Timestamp; } - return new PayoutWebhookDeliveryRequest(payoutEvent,webhook?.Id, webhookEvent, delivery, webhookBlob, btcPayNetworkJsonSerializerSettings); + + return new PayoutWebhookDeliveryRequest(payoutEvent, webhook?.Id, webhookEvent, delivery, webhookBlob, btcPayNetworkJsonSerializerSettings); } public override Dictionary GetSupportedWebhookTypes() { - return new Dictionary() + return new Dictionary { - {WebhookEventType.PayoutCreated, "Payout - Created"}, - {WebhookEventType.PayoutApproved, "Payout - Approved"}, - {WebhookEventType.PayoutUpdated, "Payout - Updated"} + { WebhookEventType.PayoutCreated, "Payout - Created" }, + { WebhookEventType.PayoutApproved, "Payout - Approved" }, + { WebhookEventType.PayoutUpdated, "Payout - Updated" } }; } public override WebhookEvent CreateTestEvent(string type, object[] args) { - var storeId = args[0].ToString(); - return new WebhookPayoutEvent(type, storeId) - { - PayoutId = "__test__" + Guid.NewGuid() + "__test__" - }; + string storeId = args[0].ToString(); + return new WebhookPayoutEvent(type, storeId) { PayoutId = "__test__" + Guid.NewGuid() + "__test__" }; } protected override WebhookPayoutEvent GetWebhookEvent(PayoutEvent payoutEvent) diff --git a/BTCPayServer/HostedServices/Webhooks/PendingTransactionDeliveryRequest.cs b/BTCPayServer/HostedServices/Webhooks/PendingTransactionDeliveryRequest.cs index 4038e4bae..57994da95 100644 --- a/BTCPayServer/HostedServices/Webhooks/PendingTransactionDeliveryRequest.cs +++ b/BTCPayServer/HostedServices/Webhooks/PendingTransactionDeliveryRequest.cs @@ -1,11 +1,7 @@ -using System.Collections; -using System.Globalization; -using System.Threading.Tasks; +using System.Threading.Tasks; using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Data; -using BTCPayServer.Services.Invoices; -using BTCPayServer.Services.Wallets; using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData; namespace BTCPayServer.HostedServices.Webhooks; @@ -21,7 +17,7 @@ public class PendingTransactionDeliveryRequest( public override Task Interpolate(SendEmailRequest req, UIStoresController.StoreEmailRule storeEmailRule) { - var blob = evt.Data.GetBlob(); + PendingTransactionBlob blob = evt.Data.GetBlob(); // if (storeEmailRule.CustomerEmail && // MailboxAddressValidator.TryParse(Invoice.Metadata.BuyerEmail, out var bmb)) // { @@ -36,18 +32,17 @@ public class PendingTransactionDeliveryRequest( private string Interpolate(string str, PendingTransactionBlob blob) { - var id = evt.Data.TransactionId; + string id = evt.Data.TransactionId; string trimmedId = $"{id.Substring(0, 7)}...{id.Substring(id.Length - 7)}"; - - var res = str.Replace("{PendingTransaction.Id}", id) + + string res = str.Replace("{PendingTransaction.Id}", id) .Replace("{PendingTransaction.TrimmedId}", trimmedId) .Replace("{PendingTransaction.StoreId}", evt.Data.StoreId) .Replace("{PendingTransaction.SignaturesCollected}", blob.SignaturesCollected?.ToString()) .Replace("{PendingTransaction.SignaturesNeeded}", blob.SignaturesNeeded?.ToString()) .Replace("{PendingTransaction.SignaturesTotal}", blob.SignaturesTotal?.ToString()); - + // res = InterpolateJsonField(res, "Invoice.Metadata", Invoice.Metadata.ToJObject()); return res; } - } diff --git a/BTCPayServer/HostedServices/Webhooks/PendingTransactionWebhookProvider.cs b/BTCPayServer/HostedServices/Webhooks/PendingTransactionWebhookProvider.cs index ece114eef..6585542bd 100644 --- a/BTCPayServer/HostedServices/Webhooks/PendingTransactionWebhookProvider.cs +++ b/BTCPayServer/HostedServices/Webhooks/PendingTransactionWebhookProvider.cs @@ -1,25 +1,19 @@ using System; using System.Collections.Generic; using BTCPayServer.Client.Models; -using BTCPayServer.Controllers.Greenfield; using BTCPayServer.Data; -using BTCPayServer.Events; -using BTCPayServer.Services.Invoices; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData; namespace BTCPayServer.HostedServices.Webhooks; -public class PendingTransactionWebhookProvider : WebhookProvider +public class PendingTransactionWebhookProvider( + WebhookSender webhookSender, + EventAggregator eventAggregator, + ILogger logger) + : WebhookProvider(eventAggregator, logger, webhookSender) { - public PendingTransactionWebhookProvider(WebhookSender webhookSender, EventAggregator eventAggregator, - ILogger logger) : base( - eventAggregator, logger, webhookSender) - { - } - public const string PendingTransactionCreated = nameof(PendingTransactionCreated); public const string PendingTransactionSignatureCollected = nameof(PendingTransactionSignatureCollected); public const string PendingTransactionBroadcast = nameof(PendingTransactionBroadcast); @@ -29,29 +23,30 @@ public class PendingTransactionWebhookProvider : WebhookProvider { - {PendingTransactionCreated, "Pending Transaction - Created"}, - {PendingTransactionSignatureCollected, "Pending Transaction - Signature Collected"}, - {PendingTransactionBroadcast, "Pending Transaction - Broadcast"}, - {PendingTransactionCancelled, "Pending Transaction - Cancelled"} + { PendingTransactionCreated, "Pending Transaction - Created" }, + { PendingTransactionSignatureCollected, "Pending Transaction - Signature Collected" }, + { PendingTransactionBroadcast, "Pending Transaction - Broadcast" }, + { PendingTransactionCancelled, "Pending Transaction - Cancelled" } }; } protected override WebhookSender.WebhookDeliveryRequest CreateDeliveryRequest(PendingTransactionService.PendingTransactionEvent evt, WebhookData webhook) { - var webhookBlob = webhook?.GetBlob(); + WebhookBlob webhookBlob = webhook?.GetBlob(); - var webhookEvent = GetWebhookEvent(evt)!; + WebhookPendingTransactionEvent webhookEvent = GetWebhookEvent(evt)!; webhookEvent.StoreId = evt.Data.StoreId; webhookEvent.WebhookId = webhook?.Id; webhookEvent.IsRedelivery = false; - WebhookDeliveryData delivery = webhook is null? null: WebhookExtensions.NewWebhookDelivery(webhook.Id); + WebhookDeliveryData delivery = webhook is null ? null : WebhookExtensions.NewWebhookDelivery(webhook.Id); if (delivery is not null) { webhookEvent.DeliveryId = delivery.Id; webhookEvent.OriginalDeliveryId = delivery.Id; webhookEvent.Timestamp = delivery.Timestamp; } + return new PendingTransactionDeliveryRequest(evt, webhook?.Id, webhookEvent, delivery, webhookBlob); } @@ -73,14 +68,11 @@ public class PendingTransactionWebhookProvider : WebhookProvider(); services.AddSingleton(o => o.GetRequiredService()); services.AddHostedService(o => o.GetRequiredService()); - + services.AddSingleton(); services.AddSingleton(o => o.GetRequiredService()); services.AddHostedService(o => o.GetRequiredService()); - + services.AddSingleton(); services.AddSingleton(o => o.GetRequiredService()); services.AddHostedService(o => o.GetRequiredService()); diff --git a/BTCPayServer/HostedServices/Webhooks/WebhookProvider.cs b/BTCPayServer/HostedServices/Webhooks/WebhookProvider.cs index 4976cb428..0209fd3fd 100644 --- a/BTCPayServer/HostedServices/Webhooks/WebhookProvider.cs +++ b/BTCPayServer/HostedServices/Webhooks/WebhookProvider.cs @@ -12,17 +12,18 @@ public abstract class WebhookProvider(EventAggregator eventAggregator, ILogge { public abstract Dictionary GetSupportedWebhookTypes(); - protected abstract WebhookSender.WebhookDeliveryRequest CreateDeliveryRequest(T evt, WebhookData webhook); - public abstract WebhookEvent CreateTestEvent(string type, params object[] args); + protected abstract WebhookSender.WebhookDeliveryRequest CreateDeliveryRequest(T evt, WebhookData webhook); + protected abstract StoreWebhookEvent GetWebhookEvent(T evt); - + protected override void SubscribeToEvents() { Subscribe(); base.SubscribeToEvents(); } + protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) { if (evt is T tEvt) @@ -30,16 +31,12 @@ public abstract class WebhookProvider(EventAggregator eventAggregator, ILogge if (GetWebhookEvent(tEvt) is not { } webhookEvent) return; - var webhooks = await webhookSender.GetWebhooks(webhookEvent.StoreId, webhookEvent.Type); - foreach (var webhook in webhooks) - { - webhookSender.EnqueueDelivery(CreateDeliveryRequest(tEvt, webhook)); - } + WebhookData[] webhooks = await webhookSender.GetWebhooks(webhookEvent.StoreId, webhookEvent.Type); + foreach (WebhookData webhook in webhooks) webhookSender.EnqueueDelivery(CreateDeliveryRequest(tEvt, webhook)); EventAggregator.Publish(CreateDeliveryRequest(tEvt, null)); } await base.ProcessEvent(evt, cancellationToken); } - } diff --git a/BTCPayServer/HostedServices/Webhooks/WebhookSender.cs b/BTCPayServer/HostedServices/Webhooks/WebhookSender.cs index 94b6d405d..6c633e0c6 100644 --- a/BTCPayServer/HostedServices/Webhooks/WebhookSender.cs +++ b/BTCPayServer/HostedServices/Webhooks/WebhookSender.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -18,328 +19,310 @@ using Microsoft.Extensions.Logging; using NBitcoin.DataEncoders; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData; -namespace BTCPayServer.HostedServices.Webhooks +namespace BTCPayServer.HostedServices.Webhooks; + +/// +/// This class sends webhook notifications +/// It also makes sure the events sent to a webhook are sent in order to the webhook +/// +public class WebhookSender( + StoreRepository storeRepository, + IHttpClientFactory httpClientFactory, + ApplicationDbContextFactory dbContextFactory, + ILogger logger, + IServiceProvider serviceProvider, + EventAggregator eventAggregator) + : IHostedService { - /// - /// This class sends webhook notifications - /// It also makes sure the events sent to a webhook are sent in order to the webhook - /// - public class WebhookSender : IHostedService + public const string OnionNamedClient = "greenfield-webhook.onion"; + public const string ClearnetNamedClient = "greenfield-webhook.clearnet"; + public const string LoopbackNamedClient = "greenfield-webhook.loopback"; + public static string[] AllClients = new[] { OnionNamedClient, ClearnetNamedClient, LoopbackNamedClient }; + public static readonly JsonSerializerSettings DefaultSerializerSettings; + + + private readonly MultiProcessingQueue _processingQueue = new(); + + private readonly Encoding _utf8 = new UTF8Encoding(false); + + + static WebhookSender() { - public const string OnionNamedClient = "greenfield-webhook.onion"; - public const string ClearnetNamedClient = "greenfield-webhook.clearnet"; - public const string LoopbackNamedClient = "greenfield-webhook.loopback"; - public static string[] AllClients = new[] {OnionNamedClient, ClearnetNamedClient, LoopbackNamedClient}; + DefaultSerializerSettings = WebhookEvent.DefaultSerializerSettings; + } - private readonly EventAggregator _eventAggregator; - private readonly ApplicationDbContextFactory _dbContextFactory; - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; - private readonly Encoding _utf8 = new UTF8Encoding(false); - public static readonly JsonSerializerSettings DefaultSerializerSettings; + private StoreRepository StoreRepository { get; } = storeRepository; + private IHttpClientFactory HttpClientFactory { get; } = httpClientFactory; + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + Task stopping = _processingQueue.Abort(cancellationToken); + await stopping; + } - private readonly MultiProcessingQueue _processingQueue = new(); - private StoreRepository StoreRepository { get; } - private IHttpClientFactory HttpClientFactory { get; } + private HttpClient GetClient(Uri uri) + { + return HttpClientFactory.CreateClient(uri.IsOnion() ? OnionNamedClient : + uri.IsLoopback ? LoopbackNamedClient : ClearnetNamedClient); + } + public async Task Redeliver(string deliveryId) + { + WebhookDeliveryRequest? deliveryRequest = await CreateRedeliveryRequest(deliveryId); + if (deliveryRequest is null) + return null; + EnqueueDelivery(deliveryRequest); + return deliveryRequest.Delivery.Id; + } - static WebhookSender() + private async Task CreateRedeliveryRequest(string deliveryId) + { + await using ApplicationDbContext? ctx = dbContextFactory.CreateContext(); + var webhookDelivery = await ctx.WebhookDeliveries.AsNoTracking() + .Where(o => o.Id == deliveryId) + .Select(o => new { o.Webhook, Delivery = o }) + .FirstOrDefaultAsync(); + if (webhookDelivery is null) + return null; + WebhookDeliveryBlob? oldDeliveryBlob = webhookDelivery.Delivery.GetBlob(); + WebhookDeliveryData? newDelivery = WebhookExtensions.NewWebhookDelivery(webhookDelivery.Webhook.Id); + WebhookDeliveryBlob newDeliveryBlob = new(); + newDeliveryBlob.Request = oldDeliveryBlob.Request; + WebhookEvent? webhookEvent = newDeliveryBlob.ReadRequestAs(); + if (webhookEvent.IsPruned()) + return null; + webhookEvent.DeliveryId = newDelivery.Id; + webhookEvent.WebhookId = webhookDelivery.Webhook.Id; + // if we redelivered a redelivery, we still want the initial delivery here + webhookEvent.OriginalDeliveryId ??= deliveryId; + webhookEvent.IsRedelivery = true; + newDeliveryBlob.Request = ToBytes(webhookEvent); + newDelivery.SetBlob(newDeliveryBlob); + return new WebhookDeliveryRequest(webhookDelivery.Webhook.Id, webhookEvent, newDelivery, + webhookDelivery.Webhook.GetBlob()); + } + + private WebhookEvent GetTestWebHook(string storeId, string webhookId, string webhookEventType, + WebhookDeliveryData delivery) + { + IWebhookProvider? webhookProvider = serviceProvider.GetServices() + .FirstOrDefault(provider => provider.GetSupportedWebhookTypes().ContainsKey(webhookEventType)); + + if (webhookProvider is null) + throw new ArgumentException($"Unknown webhook event type {webhookEventType}", webhookEventType); + + WebhookEvent? webhookEvent = webhookProvider.CreateTestEvent(webhookEventType, storeId); + if (webhookEvent is null) + throw new ArgumentException("Webhook provider does not support tests"); + + webhookEvent.DeliveryId = delivery.Id; + webhookEvent.WebhookId = webhookId; + webhookEvent.OriginalDeliveryId = "__test__" + Guid.NewGuid() + "__test__"; + webhookEvent.IsRedelivery = false; + webhookEvent.Timestamp = delivery.Timestamp; + + return webhookEvent; + } + + public async Task TestWebhook(string storeId, string webhookId, string webhookEventType, + CancellationToken cancellationToken) + { + WebhookDeliveryData? delivery = WebhookExtensions.NewWebhookDelivery(webhookId); + WebhookData? webhook = (await StoreRepository.GetWebhooks(storeId)).FirstOrDefault(w => w.Id == webhookId); + WebhookDeliveryRequest deliveryRequest = new( + webhookId, + GetTestWebHook(storeId, webhookId, webhookEventType, delivery), + delivery, + webhook.GetBlob() + ); + return await SendDelivery(deliveryRequest, cancellationToken); + } + + public void EnqueueDelivery(WebhookDeliveryRequest context) + { + _processingQueue.Enqueue(context.WebhookId, cancellationToken => Process(context, cancellationToken)); + } + + private async Task Process(WebhookDeliveryRequest ctx, CancellationToken cancellationToken) + { + try { - DefaultSerializerSettings = WebhookEvent.DefaultSerializerSettings; - } - - public WebhookSender( - StoreRepository storeRepository, - IHttpClientFactory httpClientFactory, - ApplicationDbContextFactory dbContextFactory, - ILogger logger, - IServiceProvider serviceProvider, - EventAggregator eventAggregator) - { - _dbContextFactory = dbContextFactory; - _logger = logger; - _serviceProvider = serviceProvider; - _eventAggregator = eventAggregator; - StoreRepository = storeRepository; - HttpClientFactory = httpClientFactory; - } - - - private HttpClient GetClient(Uri uri) - { - return HttpClientFactory.CreateClient(uri.IsOnion() ? OnionNamedClient : - uri.IsLoopback ? LoopbackNamedClient : ClearnetNamedClient); - } - - public class WebhookDeliveryRequest(string webhookId, WebhookEvent webhookEvent, - Data.WebhookDeliveryData delivery, WebhookBlob webhookBlob) - { - public WebhookEvent WebhookEvent { get; } = webhookEvent; - public Data.WebhookDeliveryData Delivery { get; } = delivery; - public WebhookBlob WebhookBlob { get; } = webhookBlob; - public string WebhookId { get; } = webhookId; - - public virtual Task Interpolate(SendEmailRequest req, - UIStoresController.StoreEmailRule storeEmailRule) + WebhookBlob? wh = (await StoreRepository.GetWebhook(ctx.WebhookId))?.GetBlob(); + if (wh is null || !wh.ShouldDeliver(ctx.WebhookEvent.Type)) + return; + DeliveryResult result = await SendAndSaveDelivery(ctx, cancellationToken); + if (ctx.WebhookBlob.AutomaticRedelivery && + !result.Success && + result.DeliveryId is not null) { - return Task.FromResult(req)!; - } - - protected static string InterpolateJsonField(string str, string fieldName, JObject obj) - { - fieldName += "."; - //find all instance of {fieldName*} instead str, then run obj.SelectToken(*) on it - while (true) + string? originalDeliveryId = result.DeliveryId; + foreach (TimeSpan 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) + }) { - - var start = str.IndexOf($"{{{fieldName}", StringComparison.InvariantCultureIgnoreCase); - if(start == -1) - break; - start += fieldName.Length + 1; - var end = str.IndexOf("}", start, StringComparison.InvariantCultureIgnoreCase); - if(end == -1) - break; - var jsonpath = str.Substring(start, end - start); - string? result = string.Empty; - try - { - if (string.IsNullOrEmpty(jsonpath)) - { - result = obj.ToString(); - } - else - { - result = obj.SelectToken(jsonpath)?.ToString(); - } - } - catch (Exception) - { - // ignored - } - - str = str.Replace($"{{{fieldName}{jsonpath}}}", result); - } - - return str; - } - } - - public async Task Redeliver(string deliveryId) - { - var deliveryRequest = await CreateRedeliveryRequest(deliveryId); - if (deliveryRequest is null) - return null; - EnqueueDelivery(deliveryRequest); - return deliveryRequest.Delivery.Id; - } - - private async Task CreateRedeliveryRequest(string deliveryId) - { - await using var ctx = _dbContextFactory.CreateContext(); - var webhookDelivery = await ctx.WebhookDeliveries.AsNoTracking() - .Where(o => o.Id == deliveryId) - .Select(o => new {Webhook = o.Webhook, Delivery = o}) - .FirstOrDefaultAsync(); - if (webhookDelivery is null) - return null; - var oldDeliveryBlob = webhookDelivery.Delivery.GetBlob(); - var newDelivery = WebhookExtensions.NewWebhookDelivery(webhookDelivery.Webhook.Id); - var newDeliveryBlob = new WebhookDeliveryBlob(); - newDeliveryBlob.Request = oldDeliveryBlob.Request; - var webhookEvent = newDeliveryBlob.ReadRequestAs(); - if (webhookEvent.IsPruned()) - return null; - webhookEvent.DeliveryId = newDelivery.Id; - webhookEvent.WebhookId = webhookDelivery.Webhook.Id; - // if we redelivered a redelivery, we still want the initial delivery here - webhookEvent.OriginalDeliveryId ??= deliveryId; - webhookEvent.IsRedelivery = true; - newDeliveryBlob.Request = ToBytes(webhookEvent); - newDelivery.SetBlob(newDeliveryBlob); - return new WebhookDeliveryRequest(webhookDelivery.Webhook.Id, webhookEvent, newDelivery, - webhookDelivery.Webhook.GetBlob()); - } - - private WebhookEvent GetTestWebHook(string storeId, string webhookId, string webhookEventType, - Data.WebhookDeliveryData delivery) - { - var webhookProvider = _serviceProvider.GetServices() - .FirstOrDefault(provider => provider.GetSupportedWebhookTypes().ContainsKey(webhookEventType)); - - if (webhookProvider is null) - throw new ArgumentException($"Unknown webhook event type {webhookEventType}", webhookEventType); - - var webhookEvent = webhookProvider.CreateTestEvent(webhookEventType, storeId); - if(webhookEvent is null) - throw new ArgumentException($"Webhook provider does not support tests"); - - webhookEvent.DeliveryId = delivery.Id; - webhookEvent.WebhookId = webhookId; - webhookEvent.OriginalDeliveryId = "__test__" + Guid.NewGuid() + "__test__"; - webhookEvent.IsRedelivery = false; - webhookEvent.Timestamp = delivery.Timestamp; - - return webhookEvent; - } - - public async Task TestWebhook(string storeId, string webhookId, string webhookEventType, - CancellationToken cancellationToken) - { - var delivery = WebhookExtensions.NewWebhookDelivery(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, cancellationToken); - } - - public void EnqueueDelivery(WebhookDeliveryRequest context) - { - _processingQueue.Enqueue(context.WebhookId, (cancellationToken) => Process(context, cancellationToken)); - } - private async Task Process(WebhookDeliveryRequest ctx, CancellationToken cancellationToken) - { - try - { - var wh = (await StoreRepository.GetWebhook(ctx.WebhookId))?.GetBlob(); - if (wh is null || !wh.ShouldDeliver(ctx.WebhookEvent.Type)) - return; - var result = await SendAndSaveDelivery(ctx, cancellationToken); - if (ctx.WebhookBlob.AutomaticRedelivery && - !result.Success && - result.DeliveryId is not null) - { - 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), - }) - { - await Task.Delay(wait, cancellationToken); - ctx = (await CreateRedeliveryRequest(originalDeliveryId))!; - // This may have changed - if (ctx is null || !ctx.WebhookBlob.AutomaticRedelivery || - !ctx.WebhookBlob.ShouldDeliver(ctx.WebhookEvent.Type)) - return; - result = await SendAndSaveDelivery(ctx, cancellationToken); - if (result.Success) - return; - } + await Task.Delay(wait, cancellationToken); + ctx = (await CreateRedeliveryRequest(originalDeliveryId))!; + // This may have changed + if (ctx is null || !ctx.WebhookBlob.AutomaticRedelivery || + !ctx.WebhookBlob.ShouldDeliver(ctx.WebhookEvent.Type)) + return; + result = await SendAndSaveDelivery(ctx, cancellationToken); + if (result.Success) + return; } } - catch when (cancellationToken.IsCancellationRequested) - { - } - catch (Exception ex) - { - _logger.LogError(ex, "Unexpected error when processing a webhook"); - } } - - public class DeliveryResult + catch when (cancellationToken.IsCancellationRequested) { - public string? DeliveryId { get; set; } - public bool Success { get; set; } - public string? ErrorMessage { get; set; } } - - private async Task SendDelivery(WebhookDeliveryRequest ctx, CancellationToken cancellationToken) + catch (Exception ex) { - var uri = new Uri(ctx.WebhookBlob.Url, UriKind.Absolute); - var httpClient = GetClient(uri); - using var request = new HttpRequestMessage(); - request.RequestUri = uri; - request.Method = HttpMethod.Post; - byte[] bytes = ToBytes(ctx.WebhookEvent); - var content = new ByteArrayContent(bytes); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - using var hmac = - new System.Security.Cryptography.HMACSHA256(_utf8.GetBytes(ctx.WebhookBlob.Secret ?? string.Empty)); - var sig = Encoders.Hex.EncodeData(hmac.ComputeHash(bytes)); - content.Headers.Add("BTCPay-Sig", $"sha256={sig}"); - request.Content = content; - var deliveryBlob = ctx.Delivery.GetBlob() ?? new WebhookDeliveryBlob(); - deliveryBlob.Request = bytes; - try - { - using var response = await httpClient.SendAsync(request, cancellationToken); - if (!response.IsSuccessStatusCode) - { - deliveryBlob.Status = WebhookDeliveryStatus.HttpError; - deliveryBlob.ErrorMessage = $"HTTP Error Code {(int)response.StatusCode}"; - } - else - { - deliveryBlob.Status = WebhookDeliveryStatus.HttpSuccess; - } - - deliveryBlob.HttpCode = (int)response.StatusCode; - } - catch (Exception ex) when (!cancellationToken.IsCancellationRequested) - { - deliveryBlob.Status = WebhookDeliveryStatus.Failed; - 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, - CancellationToken cancellationToken) - { - var result = await SendDelivery(ctx, cancellationToken); - await StoreRepository.AddWebhookDelivery(ctx.Delivery); - - return result; - } - - private byte[] ToBytes(WebhookEvent webhookEvent) - { - var str = JsonConvert.SerializeObject(webhookEvent, Formatting.Indented, DefaultSerializerSettings); - var bytes = _utf8.GetBytes(str); - return bytes; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - var stopping = _processingQueue.Abort(cancellationToken); - await stopping; - } - - public async Task GetWebhooks(string invoiceStoreId, string? webhookEventType) - { - return (await StoreRepository.GetWebhooks(invoiceStoreId)).Where(data => webhookEventType is null || data.GetBlob().ShouldDeliver(webhookEventType)).ToArray(); - } - - public async Task GetEmailRules(string storeId, - string type) - { - return ( await StoreRepository.FindStore(storeId))?.GetStoreBlob().EmailRules?.Where(rule => rule.Trigger ==type).ToArray() ?? Array.Empty(); - } - - public Dictionary GetSupportedWebhookTypes() - { - return _serviceProvider.GetServices() - .SelectMany(provider => provider.GetSupportedWebhookTypes()).ToDictionary(pair => pair.Key, pair => pair.Value); + logger.LogError(ex, "Unexpected error when processing a webhook"); } } + + private async Task SendDelivery(WebhookDeliveryRequest ctx, CancellationToken cancellationToken) + { + Uri uri = new(ctx.WebhookBlob.Url, UriKind.Absolute); + HttpClient httpClient = GetClient(uri); + using HttpRequestMessage request = new(); + request.RequestUri = uri; + request.Method = HttpMethod.Post; + byte[] bytes = ToBytes(ctx.WebhookEvent); + ByteArrayContent content = new(bytes); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + using HMACSHA256 hmac = new(_utf8.GetBytes(ctx.WebhookBlob.Secret ?? string.Empty)); + string? sig = Encoders.Hex.EncodeData(hmac.ComputeHash(bytes)); + content.Headers.Add("BTCPay-Sig", $"sha256={sig}"); + request.Content = content; + WebhookDeliveryBlob deliveryBlob = ctx.Delivery.GetBlob() ?? new WebhookDeliveryBlob(); + deliveryBlob.Request = bytes; + try + { + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); + if (!response.IsSuccessStatusCode) + { + deliveryBlob.Status = WebhookDeliveryStatus.HttpError; + deliveryBlob.ErrorMessage = $"HTTP Error Code {(int)response.StatusCode}"; + } + else + { + deliveryBlob.Status = WebhookDeliveryStatus.HttpSuccess; + } + + deliveryBlob.HttpCode = (int)response.StatusCode; + } + catch (Exception ex) when (!cancellationToken.IsCancellationRequested) + { + deliveryBlob.Status = WebhookDeliveryStatus.Failed; + 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, + CancellationToken cancellationToken) + { + DeliveryResult result = await SendDelivery(ctx, cancellationToken); + await StoreRepository.AddWebhookDelivery(ctx.Delivery); + + return result; + } + + private byte[] ToBytes(WebhookEvent webhookEvent) + { + string str = JsonConvert.SerializeObject(webhookEvent, Formatting.Indented, DefaultSerializerSettings); + byte[] bytes = _utf8.GetBytes(str); + return bytes; + } + + public async Task GetWebhooks(string invoiceStoreId, string? webhookEventType) + { + return (await StoreRepository.GetWebhooks(invoiceStoreId)).Where(data => webhookEventType is null || data.GetBlob().ShouldDeliver(webhookEventType)) + .ToArray(); + } + + public async Task GetEmailRules(string storeId, + string type) + { + return (await StoreRepository.FindStore(storeId))?.GetStoreBlob().EmailRules?.Where(rule => rule.Trigger == type).ToArray() ?? + Array.Empty(); + } + + public Dictionary GetSupportedWebhookTypes() + { + return serviceProvider.GetServices() + .SelectMany(provider => provider.GetSupportedWebhookTypes()).ToDictionary(pair => pair.Key, pair => pair.Value); + } + + public class WebhookDeliveryRequest( + string webhookId, + WebhookEvent webhookEvent, + WebhookDeliveryData delivery, + WebhookBlob webhookBlob) + { + public WebhookEvent WebhookEvent { get; } = webhookEvent; + public WebhookDeliveryData Delivery { get; } = delivery; + public WebhookBlob WebhookBlob { get; } = webhookBlob; + public string WebhookId { get; } = webhookId; + + public virtual Task Interpolate(SendEmailRequest req, + UIStoresController.StoreEmailRule storeEmailRule) + { + return Task.FromResult(req)!; + } + + protected static string InterpolateJsonField(string str, string fieldName, JObject obj) + { + fieldName += "."; + //find all instance of {fieldName*} instead str, then run obj.SelectToken(*) on it + while (true) + { + int start = str.IndexOf($"{{{fieldName}", StringComparison.InvariantCultureIgnoreCase); + if (start == -1) + break; + start += fieldName.Length + 1; + int end = str.IndexOf("}", start, StringComparison.InvariantCultureIgnoreCase); + if (end == -1) + break; + string jsonpath = str.Substring(start, end - start); + string? result = string.Empty; + try + { + if (string.IsNullOrEmpty(jsonpath)) + result = obj.ToString(); + else + result = obj.SelectToken(jsonpath)?.ToString(); + } + catch (Exception) + { + // ignored + } + + str = str.Replace($"{{{fieldName}{jsonpath}}}", result); + } + + return str; + } + } + + public class DeliveryResult + { + public string? DeliveryId { get; set; } + public bool Success { get; set; } + public string? ErrorMessage { get; set; } + } }