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

View File

@@ -14,16 +14,19 @@ namespace BTCPayServer
{
void Unsubscribe();
}
public class EventAggregator : IDisposable
{
public EventAggregator(Logs logs)
{
Logs = logs;
}
class Subscription : IEventAggregatorSubscription
{
private readonly EventAggregator aggregator;
readonly Type t;
public Subscription(EventAggregator aggregator, Type t)
{
this.aggregator = aggregator;
@@ -31,21 +34,24 @@ namespace BTCPayServer
}
public Action<Object> Act { get; set; }
public bool Any { get; set; }
bool _Disposed;
public void Dispose()
{
if (_Disposed)
return;
_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.Count == 0)
this.aggregator._Subscriptions.Remove(t);
dict.Remove(t);
}
}
}
@@ -56,18 +62,25 @@ namespace BTCPayServer
Dispose();
}
}
public Task<T> WaitNext<T>(CancellationToken cancellation = default(CancellationToken))
{
return WaitNext<T>(o => true, cancellation);
}
public async Task<T> WaitNext<T>(Func<T, bool> predicate, CancellationToken cancellation = default(CancellationToken))
{
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
using var subscription = Subscribe<T>((a, b) => { if (predicate(b)) { tcs.TrySetResult(b); a.Unsubscribe(); } });
using (cancellation.Register(() => { tcs.TrySetCanceled(); }))
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
using var subscription = Subscribe<T>((a, b) =>
{
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
@@ -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))
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)
{
@@ -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)
{
var eventType = typeof(T);
@@ -111,6 +139,20 @@ namespace BTCPayServer
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)
{
var s = new Subscription(this, eventType);
@@ -120,20 +162,26 @@ namespace BTCPayServer
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>>();
_Subscriptions.Add(eventType, actions);
subscriptions.Add(eventType, actions);
}
actions.Add(subscription, subscription.Act);
}
return subscription;
}
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 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)));
}
class ChannelSubscription<T> : IEventAggregatorSubscription
{
private Channel<T> _evts;
@@ -187,23 +236,35 @@ namespace BTCPayServer
_evts.Writer.TryComplete();
}
}
public IEventAggregatorSubscription SubscribeAsync<T>(Func<T, Task> subscription)
{
Channel<T> evts = Channel.CreateUnbounded<T>();
var innerSubscription = Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => evts.Writer.TryWrite(t)));
return new ChannelSubscription<T>(evts, innerSubscription, subscription, Logs);
}
public IEventAggregatorSubscription Subscribe<T>(Action<T> subscription)
{
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()
{
lock (_Subscriptions)
{
_Subscriptions.Clear();
}
lock (_SubscriptionsAny)
{
_SubscriptionsAny.Clear();
}
}
}
}

View File

@@ -1,6 +1,6 @@
using System;
using BTCPayServer;
using BTCPayServer.Abstractions;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
@@ -83,7 +83,32 @@ namespace Microsoft.AspNetCore.Mvc
values: new { invoiceId },
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)
{
WalletId.TryParse(walletIdOrStoreId, out var wallet);

View File

@@ -36,14 +36,36 @@ namespace BTCPayServer.HostedServices
readonly Channel<object> _Events = Channel.CreateUnbounded<object>();
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
{
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);
}
}
catch when (cancellationToken.IsCancellationRequested)
{
throw;
@@ -71,12 +93,24 @@ namespace BTCPayServer.HostedServices
{
_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)
{
_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)
{
SubscribeToEvents();
@@ -87,6 +121,7 @@ namespace BTCPayServer.HostedServices
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
_Events.Writer.TryComplete();
_Subscriptions.ForEach(subscription => subscription.Dispose());
_Cts.Cancel();
try

View File

@@ -168,6 +168,12 @@ namespace BTCPayServer.Hosting
// /Components/{View Component Name}/{View Name}.cshtml
o.ViewLocationFormats.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()
.AddPlugins(services, Configuration, LoggerFactory, bootstrapServiceProvider)