Merge pull request #3289 from NicolasDorier/oiufew

Do not send email twice for bitpay API, log whether IPN is an ExtendedNotification. (Fix #968)
This commit is contained in:
Nicolas Dorier 2022-01-12 00:42:44 +09:00 committed by GitHub
commit 96259dfb49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 31 additions and 74 deletions

View file

@ -2,16 +2,17 @@ namespace BTCPayServer.Events
{
public class InvoiceIPNEvent : IHasInvoiceId
{
public InvoiceIPNEvent(string invoiceId, int? eventCode, string name)
public InvoiceIPNEvent(string invoiceId, int? eventCode, string name, bool extendedNotification)
{
InvoiceId = invoiceId;
EventCode = eventCode;
Name = name;
ExtendedNotification = extendedNotification;
}
public int? EventCode { get; set; }
public string Name { get; set; }
public bool ExtendedNotification { get; }
public string InvoiceId { get; set; }
public string Error { get; set; }
@ -20,7 +21,17 @@ namespace BTCPayServer.Events
string ipnType = "IPN";
if (EventCode.HasValue)
{
ipnType = $"IPN ({EventCode.Value} {Name})";
string suffix = string.Empty;
if (ExtendedNotification)
suffix = " ExtendedNotification";
ipnType = $"IPN ({EventCode.Value} {Name}{suffix})";
}
else
{
string suffix = string.Empty;
if (ExtendedNotification)
suffix = " (ExtendedNotification)";
ipnType += suffix;
}
if (Error == null)
return $"{ipnType} sent for invoice {InvoiceId}";

View file

@ -62,7 +62,7 @@ namespace BTCPayServer.HostedServices
_StoreRepository = storeRepository;
}
async Task Notify(InvoiceEntity invoice, InvoiceEvent invoiceEvent, bool extendedNotification)
async Task Notify(InvoiceEntity invoice, InvoiceEvent invoiceEvent, bool extendedNotification, bool sendMail)
{
var dto = invoice.EntityToDTO();
var notification = new InvoicePaymentNotificationEventWrapper()
@ -122,7 +122,7 @@ namespace BTCPayServer.HostedServices
#pragma warning restore CS0618
}
if ((invoice.ExtendedNotifications || invoiceEvent.Name != InvoiceEvent.Expired) && !String.IsNullOrEmpty(invoice.NotificationEmail))
if (sendMail && !String.IsNullOrEmpty(invoice.NotificationEmail))
{
var json = NBitcoin.JsonConverters.Serializer.ToString(notification);
var store = await _StoreRepository.FindStore(invoice.StoreId);
@ -148,10 +148,10 @@ namespace BTCPayServer.HostedServices
public async Task NotifyHttp(ScheduledJob job, CancellationToken cancellationToken)
{
bool reschedule = false;
var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name);
var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name, job.Notification.ExtendedNotification);
try
{
HttpResponseMessage response = await SendNotification(job.Notification, cancellationToken);
using HttpResponseMessage response = await SendNotification(job.Notification, cancellationToken);
reschedule = !response.IsSuccessStatusCode;
aggregatorEvent.Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null;
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
@ -232,75 +232,15 @@ namespace BTCPayServer.HostedServices
request.RequestUri = new Uri(notification.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(notificationString, UTF8, "application/json");
var response = await Enqueue(notification.Data.Id, async () =>
{
using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromMinutes(1.0));
return await _Client.SendAsync(request, cts.Token);
}
});
var response = await _Client.SendAsync(request, cts.Token);
return response;
}
readonly Dictionary<string, Task> _SendingRequestsByInvoiceId = new Dictionary<string, Task>();
/// <summary>
/// Will make sure only one callback is called at once on the same invoiceId
/// </summary>
/// <param name="id"></param>
/// <param name="sendRequest"></param>
/// <returns></returns>
private async Task<T> Enqueue<T>(string id, Func<Task<T>> sendRequest)
{
Task<T> sending = null;
lock (_SendingRequestsByInvoiceId)
{
if (_SendingRequestsByInvoiceId.TryGetValue(id, out var executing))
{
var completion = new TaskCompletionSource<T>();
sending = completion.Task;
_SendingRequestsByInvoiceId.Remove(id);
_SendingRequestsByInvoiceId.Add(id, sending);
executing.ContinueWith(_ =>
{
sendRequest()
.ContinueWith(t =>
{
if (t.Status == TaskStatus.RanToCompletion)
{
completion.TrySetResult(t.Result);
}
if (t.Status == TaskStatus.Faulted)
{
completion.TrySetException(t.Exception);
}
if (t.Status == TaskStatus.Canceled)
{
completion.TrySetCanceled();
}
}, TaskScheduler.Default);
}, TaskScheduler.Default);
}
else
{
sending = sendRequest();
_SendingRequestsByInvoiceId.Add(id, sending);
}
sending.ContinueWith(o =>
{
lock (_SendingRequestsByInvoiceId)
{
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
if (executing2 == sending)
_SendingRequestsByInvoiceId.Remove(id);
}
}, TaskScheduler.Default);
}
return await sending;
}
readonly int MaxTry = 6;
readonly CompositeDisposable leases = new CompositeDisposable();
@ -316,6 +256,7 @@ namespace BTCPayServer.HostedServices
var invoice = await _InvoiceRepository.GetInvoice(e.Invoice.Id);
if (invoice == null)
return;
bool sendMail = true;
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
if (invoice.FullNotifications)
{
@ -328,17 +269,22 @@ namespace BTCPayServer.HostedServices
e.Name == InvoiceEvent.Completed ||
e.Name == InvoiceEvent.ExpiredPaidPartial
)
_ = Notify(invoice, e, false);
{
_ = Notify(invoice, e, false, sendMail);
sendMail = false;
}
}
if (e.Name == InvoiceEvent.Confirmed)
{
_ = Notify(invoice, e, false);
_ = Notify(invoice, e, false, sendMail);
sendMail = false;
}
if (invoice.ExtendedNotifications)
{
_ = Notify(invoice, e, true);
_ = Notify(invoice, e, true, sendMail);
sendMail = false;
}
}));
return Task.CompletedTask;