mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-19 06:54:19 +01:00
Cleaning up Webhooks code
This commit is contained in:
@@ -6,7 +6,7 @@ namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public interface IWebhookProvider
|
||||
{
|
||||
public Dictionary<string,string> GetSupportedWebhookTypes();
|
||||
|
||||
public WebhookEvent CreateTestEvent(string type, params object[] args);
|
||||
public Dictionary<string, string> GetSupportedWebhookTypes();
|
||||
|
||||
public WebhookEvent CreateTestEvent(string type, params object[] args);
|
||||
}
|
||||
|
||||
@@ -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<SendEmailRequest> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<InvoiceEvent>
|
||||
public class InvoiceWebhookProvider(
|
||||
WebhookSender webhookSender,
|
||||
EventAggregator eventAggregator,
|
||||
ILogger<InvoiceWebhookProvider> logger)
|
||||
: WebhookProvider<InvoiceEvent>(eventAggregator, logger, webhookSender)
|
||||
{
|
||||
public InvoiceWebhookProvider(WebhookSender webhookSender, EventAggregator eventAggregator,
|
||||
ILogger<InvoiceWebhookProvider> logger) : base(
|
||||
eventAggregator, logger, webhookSender)
|
||||
{
|
||||
}
|
||||
|
||||
public override Dictionary<string, string> GetSupportedWebhookTypes()
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{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<InvoiceEvent>
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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<SendEmailRequest?> 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;
|
||||
|
||||
@@ -8,31 +8,25 @@ using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
||||
|
||||
namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public class PaymentRequestWebhookProvider: WebhookProvider<PaymentRequestEvent>
|
||||
public class PaymentRequestWebhookProvider(EventAggregator eventAggregator, ILogger<PaymentRequestWebhookProvider> logger, WebhookSender webhookSender)
|
||||
: WebhookProvider<PaymentRequestEvent>(eventAggregator, logger, webhookSender)
|
||||
{
|
||||
public PaymentRequestWebhookProvider(EventAggregator eventAggregator, ILogger<PaymentRequestWebhookProvider> logger, WebhookSender webhookSender) : base(eventAggregator, logger, webhookSender)
|
||||
{
|
||||
}
|
||||
|
||||
public override Dictionary<string, string> GetSupportedWebhookTypes()
|
||||
{
|
||||
return new Dictionary<string, string>()
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{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<PaymentRequestEvent>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SendEmailRequest?> 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());
|
||||
|
||||
@@ -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<PayoutWebhookProvider> logger,
|
||||
WebhookSender webhookSender, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
|
||||
public class PayoutWebhookProvider(
|
||||
EventAggregator eventAggregator,
|
||||
ILogger<PayoutWebhookProvider> logger,
|
||||
WebhookSender webhookSender,
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
|
||||
: WebhookProvider<PayoutEvent>(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<string, string> GetSupportedWebhookTypes()
|
||||
{
|
||||
return new Dictionary<string, string>()
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{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)
|
||||
|
||||
@@ -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<SendEmailRequest> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<PendingTransactionService.PendingTransactionEvent>
|
||||
public class PendingTransactionWebhookProvider(
|
||||
WebhookSender webhookSender,
|
||||
EventAggregator eventAggregator,
|
||||
ILogger<InvoiceWebhookProvider> logger)
|
||||
: WebhookProvider<PendingTransactionService.PendingTransactionEvent>(eventAggregator, logger, webhookSender)
|
||||
{
|
||||
public PendingTransactionWebhookProvider(WebhookSender webhookSender, EventAggregator eventAggregator,
|
||||
ILogger<InvoiceWebhookProvider> 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<PendingTransact
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{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<PendingTransact
|
||||
|
||||
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__" };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public class WebhookPendingTransactionEvent : StoreWebhookEvent
|
||||
{
|
||||
public WebhookPendingTransactionEvent(string type, string storeId)
|
||||
|
||||
@@ -11,14 +11,9 @@ namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public static class WebhookExtensions
|
||||
{
|
||||
public static Data.WebhookDeliveryData NewWebhookDelivery(string webhookId)
|
||||
public static WebhookDeliveryData NewWebhookDelivery(string webhookId)
|
||||
{
|
||||
return new Data.WebhookDeliveryData
|
||||
{
|
||||
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)),
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
WebhookId = webhookId
|
||||
};
|
||||
return new WebhookDeliveryData { Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)), Timestamp = DateTimeOffset.UtcNow, WebhookId = webhookId };
|
||||
}
|
||||
|
||||
public static bool ShouldDeliver(this WebhookBlob wh, string type)
|
||||
@@ -35,11 +30,11 @@ public static class WebhookExtensions
|
||||
services.AddSingleton<PayoutWebhookProvider>();
|
||||
services.AddSingleton<IWebhookProvider>(o => o.GetRequiredService<PayoutWebhookProvider>());
|
||||
services.AddHostedService(o => o.GetRequiredService<PayoutWebhookProvider>());
|
||||
|
||||
|
||||
services.AddSingleton<PaymentRequestWebhookProvider>();
|
||||
services.AddSingleton<IWebhookProvider>(o => o.GetRequiredService<PaymentRequestWebhookProvider>());
|
||||
services.AddHostedService(o => o.GetRequiredService<PaymentRequestWebhookProvider>());
|
||||
|
||||
|
||||
services.AddSingleton<PendingTransactionWebhookProvider>();
|
||||
services.AddSingleton<IWebhookProvider>(o => o.GetRequiredService<PendingTransactionWebhookProvider>());
|
||||
services.AddHostedService(o => o.GetRequiredService<PendingTransactionWebhookProvider>());
|
||||
|
||||
@@ -12,17 +12,18 @@ public abstract class WebhookProvider<T>(EventAggregator eventAggregator, ILogge
|
||||
{
|
||||
public abstract Dictionary<string, string> 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<T>();
|
||||
base.SubscribeToEvents();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is T tEvt)
|
||||
@@ -30,16 +31,12 @@ public abstract class WebhookProvider<T>(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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This class sends webhook notifications
|
||||
/// It also makes sure the events sent to a webhook are sent in order to the webhook
|
||||
/// </summary>
|
||||
public class WebhookSender(
|
||||
StoreRepository storeRepository,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
ILogger<WebhookSender> logger,
|
||||
IServiceProvider serviceProvider,
|
||||
EventAggregator eventAggregator)
|
||||
: IHostedService
|
||||
{
|
||||
/// <summary>
|
||||
/// This class sends webhook notifications
|
||||
/// It also makes sure the events sent to a webhook are sent in order to the webhook
|
||||
/// </summary>
|
||||
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<WebhookSender> _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<string?> 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<WebhookDeliveryRequest?> 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<WebhookEvent>();
|
||||
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<IWebhookProvider>()
|
||||
.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<DeliveryResult> 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<WebhookSender> 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<SendEmailRequest?> 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<string?> Redeliver(string deliveryId)
|
||||
{
|
||||
var deliveryRequest = await CreateRedeliveryRequest(deliveryId);
|
||||
if (deliveryRequest is null)
|
||||
return null;
|
||||
EnqueueDelivery(deliveryRequest);
|
||||
return deliveryRequest.Delivery.Id;
|
||||
}
|
||||
|
||||
private async Task<WebhookDeliveryRequest?> 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<WebhookEvent>();
|
||||
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<IWebhookProvider>()
|
||||
.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<DeliveryResult> 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<DeliveryResult> 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<DeliveryResult> 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<WebhookData[]> GetWebhooks(string invoiceStoreId, string? webhookEventType)
|
||||
{
|
||||
return (await StoreRepository.GetWebhooks(invoiceStoreId)).Where(data => webhookEventType is null || data.GetBlob().ShouldDeliver(webhookEventType)).ToArray();
|
||||
}
|
||||
|
||||
public async Task<UIStoresController.StoreEmailRule[]> GetEmailRules(string storeId,
|
||||
string type)
|
||||
{
|
||||
return ( await StoreRepository.FindStore(storeId))?.GetStoreBlob().EmailRules?.Where(rule => rule.Trigger ==type).ToArray() ?? Array.Empty<UIStoresController.StoreEmailRule>();
|
||||
}
|
||||
|
||||
public Dictionary<string, string> GetSupportedWebhookTypes()
|
||||
{
|
||||
return _serviceProvider.GetServices<IWebhookProvider>()
|
||||
.SelectMany(provider => provider.GetSupportedWebhookTypes()).ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||
logger.LogError(ex, "Unexpected error when processing a webhook");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<DeliveryResult> 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<DeliveryResult> 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<WebhookData[]> GetWebhooks(string invoiceStoreId, string? webhookEventType)
|
||||
{
|
||||
return (await StoreRepository.GetWebhooks(invoiceStoreId)).Where(data => webhookEventType is null || data.GetBlob().ShouldDeliver(webhookEventType))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public async Task<UIStoresController.StoreEmailRule[]> GetEmailRules(string storeId,
|
||||
string type)
|
||||
{
|
||||
return (await StoreRepository.FindStore(storeId))?.GetStoreBlob().EmailRules?.Where(rule => rule.Trigger == type).ToArray() ??
|
||||
Array.Empty<UIStoresController.StoreEmailRule>();
|
||||
}
|
||||
|
||||
public Dictionary<string, string> GetSupportedWebhookTypes()
|
||||
{
|
||||
return serviceProvider.GetServices<IWebhookProvider>()
|
||||
.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<SendEmailRequest?> 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; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user