Add helpers methods (#6941)

This commit is contained in:
Nicolas Dorier
2025-10-07 21:08:23 +09:00
committed by GitHub
parent e170ed1f91
commit 6b727dd192
5 changed files with 145 additions and 24 deletions

View File

@@ -95,7 +95,7 @@ namespace BTCPayServer.Controllers
} }
[HttpGet("invoices/{invoiceId}")] [HttpGet("invoices/{invoiceId}")]
[HttpGet("/stores/{storeId}/invoices/${invoiceId}")] [HttpGet("/stores/{storeId}/invoices/{invoiceId}")]
[Authorize(Policy = Policies.CanViewInvoices, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanViewInvoices, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Invoice(string invoiceId) public async Task<IActionResult> Invoice(string invoiceId)
{ {
@@ -837,13 +837,7 @@ namespace BTCPayServer.Controllers
lang ??= storeBlob.DefaultLang; lang ??= storeBlob.DefaultLang;
var receiptEnabled = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, invoice.ReceiptOptions).Enabled is true; var receiptEnabled = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, invoice.ReceiptOptions).Enabled is true;
var receiptUrl = receiptEnabled ? _linkGenerator.GetUriByAction( var receiptUrl = receiptEnabled ? _linkGenerator.ReceiptLink(invoiceId, Request.GetRequestBaseUrl()) : null;
nameof(InvoiceReceipt),
"UIInvoice",
new { invoiceId },
Request.Scheme,
Request.Host,
Request.PathBase) : null;
var orderId = invoice.Metadata.OrderId; var orderId = invoice.Metadata.OrderId;
var supportUrl = !string.IsNullOrEmpty(storeBlob.StoreSupportUrl) var supportUrl = !string.IsNullOrEmpty(storeBlob.StoreSupportUrl)

View File

@@ -14,16 +14,19 @@ namespace BTCPayServer
{ {
void Unsubscribe(); void Unsubscribe();
} }
public class EventAggregator : IDisposable public class EventAggregator : IDisposable
{ {
public EventAggregator(Logs logs) public EventAggregator(Logs logs)
{ {
Logs = logs; Logs = logs;
} }
class Subscription : IEventAggregatorSubscription class Subscription : IEventAggregatorSubscription
{ {
private readonly EventAggregator aggregator; private readonly EventAggregator aggregator;
readonly Type t; readonly Type t;
public Subscription(EventAggregator aggregator, Type t) public Subscription(EventAggregator aggregator, Type t)
{ {
this.aggregator = aggregator; this.aggregator = aggregator;
@@ -31,21 +34,24 @@ namespace BTCPayServer
} }
public Action<Object> Act { get; set; } public Action<Object> Act { get; set; }
public bool Any { get; set; }
bool _Disposed; bool _Disposed;
public void Dispose() public void Dispose()
{ {
if (_Disposed) if (_Disposed)
return; return;
_Disposed = true; _Disposed = true;
lock (this.aggregator._Subscriptions) var dict = Any ? aggregator._SubscriptionsAny : aggregator._Subscriptions;
lock (dict)
{ {
if (this.aggregator._Subscriptions.TryGetValue(t, out Dictionary<Subscription, Action<object>> actions)) if (dict.TryGetValue(t, out Dictionary<Subscription, Action<object>> actions))
{ {
if (actions.Remove(this)) if (actions.Remove(this))
{ {
if (actions.Count == 0) if (actions.Count == 0)
this.aggregator._Subscriptions.Remove(t); dict.Remove(t);
} }
} }
} }
@@ -56,18 +62,25 @@ namespace BTCPayServer
Dispose(); Dispose();
} }
} }
public Task<T> WaitNext<T>(CancellationToken cancellation = default(CancellationToken)) public Task<T> WaitNext<T>(CancellationToken cancellation = default(CancellationToken))
{ {
return WaitNext<T>(o => true, cancellation); return WaitNext<T>(o => true, cancellation);
} }
public async Task<T> WaitNext<T>(Func<T, bool> predicate, CancellationToken cancellation = default(CancellationToken)) public async Task<T> WaitNext<T>(Func<T, bool> predicate, CancellationToken cancellation = default(CancellationToken))
{ {
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(); TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
using var subscription = Subscribe<T>((a, b) => { if (predicate(b)) { tcs.TrySetResult(b); a.Unsubscribe(); } }); using var subscription = Subscribe<T>((a, b) =>
using (cancellation.Register(() => { tcs.TrySetCanceled(); }))
{ {
return await tcs.Task.ConfigureAwait(false); if (predicate(b))
{
tcs.TrySetResult(b);
a.Unsubscribe();
} }
});
await using var reg = cancellation.Register(() => tcs.TrySetCanceled(cancellation));
return await tcs.Task.ConfigureAwait(false);
} }
public void Publish<T>(T evt) where T : class public void Publish<T>(T evt) where T : class
@@ -87,8 +100,17 @@ namespace BTCPayServer
} }
} }
lock (_SubscriptionsAny)
{
foreach (var kv in _SubscriptionsAny)
{
if (kv.Key.IsAssignableFrom(evtType))
actionList.AddRange(kv.Value.Values);
}
}
if (Logs.Events.IsEnabled(LogLevel.Information)) if (Logs.Events.IsEnabled(LogLevel.Information))
Logs.Events.LogInformation("Event published {0}", string.IsNullOrEmpty(evt?.ToString()) ? evtType.GetType().Name : evt.ToString()); Logs.Events.LogInformation("{0}", string.IsNullOrEmpty(evt?.ToString()) ? evtType.Name : evt.ToString());
foreach (var sub in actionList) foreach (var sub in actionList)
{ {
@@ -103,6 +125,12 @@ namespace BTCPayServer
} }
} }
/// <summary>
/// Subscribe to any event of exactly type T
/// </summary>
/// <param name="subscription"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public IEventAggregatorSubscription Subscribe<T>(Action<IEventAggregatorSubscription, T> subscription) public IEventAggregatorSubscription Subscribe<T>(Action<IEventAggregatorSubscription, T> subscription)
{ {
var eventType = typeof(T); var eventType = typeof(T);
@@ -111,6 +139,20 @@ namespace BTCPayServer
return Subscribe(eventType, s); return Subscribe(eventType, s);
} }
/// <summary>
/// Subscribe to any event of type T or any of its derived type
/// </summary>
/// <param name="subscription"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public IEventAggregatorSubscription SubscribeAny<T>(Action<IEventAggregatorSubscription, T> subscription)
{
var eventType = typeof(T);
var s = new Subscription(this, eventType) { Any = true };
s.Act = (o) => subscription(s, (T)o);
return Subscribe(eventType, s);
}
public IEventAggregatorSubscription Subscribe(Type eventType, Action<IEventAggregatorSubscription, object> subscription) public IEventAggregatorSubscription Subscribe(Type eventType, Action<IEventAggregatorSubscription, object> subscription)
{ {
var s = new Subscription(this, eventType); var s = new Subscription(this, eventType);
@@ -120,20 +162,26 @@ namespace BTCPayServer
private IEventAggregatorSubscription Subscribe(Type eventType, Subscription subscription) private IEventAggregatorSubscription Subscribe(Type eventType, Subscription subscription)
{ {
lock (_Subscriptions) var subscriptions = subscription.Any ? _SubscriptionsAny : _Subscriptions;
lock (subscriptions)
{ {
if (!_Subscriptions.TryGetValue(eventType, out Dictionary<Subscription, Action<object>> actions)) if (!subscriptions.TryGetValue(eventType, out Dictionary<Subscription, Action<object>> actions))
{ {
actions = new Dictionary<Subscription, Action<object>>(); actions = new Dictionary<Subscription, Action<object>>();
_Subscriptions.Add(eventType, actions); subscriptions.Add(eventType, actions);
} }
actions.Add(subscription, subscription.Act); actions.Add(subscription, subscription.Act);
} }
return subscription; return subscription;
} }
readonly Dictionary<Type, Dictionary<Subscription, Action<object>>> _Subscriptions = new Dictionary<Type, Dictionary<Subscription, Action<object>>>(); readonly Dictionary<Type, Dictionary<Subscription, Action<object>>> _Subscriptions = new Dictionary<Type, Dictionary<Subscription, Action<object>>>();
readonly Dictionary<Type, Dictionary<Subscription, Action<object>>>
_SubscriptionsAny = new Dictionary<Type, Dictionary<Subscription, Action<object>>>();
public Logs Logs { get; } public Logs Logs { get; }
public IEventAggregatorSubscription Subscribe<T, TReturn>(Func<T, TReturn> subscription) public IEventAggregatorSubscription Subscribe<T, TReturn>(Func<T, TReturn> subscription)
@@ -145,6 +193,7 @@ namespace BTCPayServer
{ {
return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(sub, t))); return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(sub, t)));
} }
class ChannelSubscription<T> : IEventAggregatorSubscription class ChannelSubscription<T> : IEventAggregatorSubscription
{ {
private Channel<T> _evts; private Channel<T> _evts;
@@ -187,23 +236,35 @@ namespace BTCPayServer
_evts.Writer.TryComplete(); _evts.Writer.TryComplete();
} }
} }
public IEventAggregatorSubscription SubscribeAsync<T>(Func<T, Task> subscription) public IEventAggregatorSubscription SubscribeAsync<T>(Func<T, Task> subscription)
{ {
Channel<T> evts = Channel.CreateUnbounded<T>(); Channel<T> evts = Channel.CreateUnbounded<T>();
var innerSubscription = Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => evts.Writer.TryWrite(t))); var innerSubscription = Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => evts.Writer.TryWrite(t)));
return new ChannelSubscription<T>(evts, innerSubscription, subscription, Logs); return new ChannelSubscription<T>(evts, innerSubscription, subscription, Logs);
} }
public IEventAggregatorSubscription Subscribe<T>(Action<T> subscription) public IEventAggregatorSubscription Subscribe<T>(Action<T> subscription)
{ {
return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(t))); return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(t)));
} }
public IEventAggregatorSubscription SubscribeAny<T>(Action<T> subscription)
{
return SubscribeAny(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(t)));
}
public void Dispose() public void Dispose()
{ {
lock (_Subscriptions) lock (_Subscriptions)
{ {
_Subscriptions.Clear(); _Subscriptions.Clear();
} }
lock (_SubscriptionsAny)
{
_SubscriptionsAny.Clear();
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
using System; using System;
using BTCPayServer; using BTCPayServer;
using BTCPayServer.Abstractions;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
@@ -83,7 +83,32 @@ namespace Microsoft.AspNetCore.Mvc
values: new { invoiceId }, values: new { invoiceId },
scheme, host, pathbase); scheme, host, pathbase);
} }
#nullable enable
public static string ReceiptLink(this LinkGenerator urlHelper, string invoiceId, RequestBaseUrl baseUrl)
=> urlHelper.GetUriByAction(
action: nameof(UIInvoiceController.InvoiceReceipt),
controller: "UIInvoice",
values: new { invoiceId },
baseUrl);
public static string InvoiceCheckoutLink(this LinkGenerator urlHelper, string invoiceId, RequestBaseUrl baseUrl)
=> urlHelper.GetUriByAction(
action: nameof(UIInvoiceController.Checkout),
controller: "UIInvoice",
values: new { invoiceId },
baseUrl
);
public static string GetUriByAction(
this LinkGenerator generator,
string action,
string controller,
object? values,
RequestBaseUrl requestBaseUrl,
FragmentString fragment = default,
LinkOptions? options = null) => generator.GetUriByAction(action, controller, values, requestBaseUrl.Scheme, requestBaseUrl.Host, requestBaseUrl.PathBase, fragment, options) ?? throw new InvalidOperationException($"Bug, unable to generate link for {controller}.{action}");
#nullable restore
public static string PayoutLink(this LinkGenerator urlHelper, string walletIdOrStoreId, string pullPaymentId, PayoutState payoutState, string scheme, HostString host, string pathbase) public static string PayoutLink(this LinkGenerator urlHelper, string walletIdOrStoreId, string pullPaymentId, PayoutState payoutState, string scheme, HostString host, string pathbase)
{ {
WalletId.TryParse(walletIdOrStoreId, out var wallet); WalletId.TryParse(walletIdOrStoreId, out var wallet);

View File

@@ -36,14 +36,36 @@ namespace BTCPayServer.HostedServices
readonly Channel<object> _Events = Channel.CreateUnbounded<object>(); readonly Channel<object> _Events = Channel.CreateUnbounded<object>();
public async Task ProcessEvents(CancellationToken cancellationToken) public async Task ProcessEvents(CancellationToken cancellationToken)
{ {
while (await _Events.Reader.WaitToReadAsync(cancellationToken)) // We want current job to finish before exiting
// ReSharper disable once MethodSupportsCancellation
while (await _Events.Reader.WaitToReadAsync())
{ {
if (_Events.Reader.TryRead(out var evt)) while (_Events.Reader.TryRead(out var evt))
{ {
try try
{
if (evt is ExecutingEvent e)
{
try
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, e.CancellationToken);
await ProcessEvent(e.Event, linkedCts.Token);
e.Tcs.TrySetResult();
}
catch (OperationCanceledException pce) when (e.CancellationToken.IsCancellationRequested || cancellationToken.IsCancellationRequested)
{
e.Tcs.TrySetCanceled(pce.CancellationToken);
}
catch (Exception ex)
{
e.Tcs.TrySetException(ex);
}
}
else
{ {
await ProcessEvent(evt, cancellationToken); await ProcessEvent(evt, cancellationToken);
} }
}
catch when (cancellationToken.IsCancellationRequested) catch when (cancellationToken.IsCancellationRequested)
{ {
throw; throw;
@@ -71,12 +93,24 @@ namespace BTCPayServer.HostedServices
{ {
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e!))); _Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e!)));
} }
protected void SubscribeAny<T>()
{
_Subscriptions.Add(_EventAggregator.SubscribeAny<T>(e => _Events.Writer.TryWrite(e!)));
}
protected void PushEvent(object obj) protected void PushEvent(object obj)
{ {
_Events.Writer.TryWrite(obj); _Events.Writer.TryWrite(obj);
} }
record ExecutingEvent(object Event, TaskCompletionSource Tcs, CancellationToken CancellationToken);
protected Task RunEvent(object obj, CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_Events.Writer.TryWrite(new ExecutingEvent(obj, tcs, cancellationToken));
return tcs.Task;
}
public virtual Task StartAsync(CancellationToken cancellationToken) public virtual Task StartAsync(CancellationToken cancellationToken)
{ {
SubscribeToEvents(); SubscribeToEvents();
@@ -87,6 +121,7 @@ namespace BTCPayServer.HostedServices
public virtual async Task StopAsync(CancellationToken cancellationToken) public virtual async Task StopAsync(CancellationToken cancellationToken)
{ {
_Events.Writer.TryComplete();
_Subscriptions.ForEach(subscription => subscription.Dispose()); _Subscriptions.ForEach(subscription => subscription.Dispose());
_Cts.Cancel(); _Cts.Cancel();
try try

View File

@@ -168,6 +168,12 @@ namespace BTCPayServer.Hosting
// /Components/{View Component Name}/{View Name}.cshtml // /Components/{View Component Name}/{View Name}.cshtml
o.ViewLocationFormats.Add("/{0}.cshtml"); o.ViewLocationFormats.Add("/{0}.cshtml");
o.PageViewLocationFormats.Add("/{0}.cshtml"); o.PageViewLocationFormats.Add("/{0}.cshtml");
// Allows the use of Area for plugins
o.AreaViewLocationFormats.Add("/Plugins/{2}/Views/{1}/{0}.cshtml");
o.AreaViewLocationFormats.Add("/Plugins/{2}/Views/{0}.cshtml");
o.AreaViewLocationFormats.Add("/Plugins/{2}/Views/Shared/{0}.cshtml");
o.AreaViewLocationFormats.Add("/{0}.cshtml");
}) })
.AddNewtonsoftJson() .AddNewtonsoftJson()
.AddPlugins(services, Configuration, LoggerFactory, bootstrapServiceProvider) .AddPlugins(services, Configuration, LoggerFactory, bootstrapServiceProvider)