mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-02-12 01:34:34 +01:00
Send notification when a new plugin version is available (#5450)
This commit is contained in:
@@ -15,6 +15,7 @@ using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
@@ -46,6 +47,7 @@ using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Storage.Models;
|
||||
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
|
||||
@@ -57,6 +59,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Payment;
|
||||
@@ -2258,13 +2261,17 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
|
||||
class MockVersionFetcher : IVersionFetcher
|
||||
class MockVersionFetcher : GithubVersionFetcher
|
||||
{
|
||||
public const string MOCK_NEW_VERSION = "9.9.9.9";
|
||||
public Task<string> Fetch(CancellationToken cancellation)
|
||||
public override Task<string> Fetch(CancellationToken cancellation)
|
||||
{
|
||||
return Task.FromResult(MOCK_NEW_VERSION);
|
||||
}
|
||||
|
||||
public MockVersionFetcher(IHttpClientFactory httpClientFactory, BTCPayServerOptions options, ILogger<GithubVersionFetcher> logger, SettingsRepository settingsRepository, BTCPayServerEnvironment environment, NotificationSender notificationSender) : base(httpClientFactory, options, logger, settingsRepository, environment, notificationSender)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
@@ -2283,8 +2290,13 @@ namespace BTCPayServer.Tests
|
||||
var mockEnv = tester.PayTester.GetService<BTCPayServerEnvironment>();
|
||||
var mockSender = tester.PayTester.GetService<Services.Notifications.NotificationSender>();
|
||||
|
||||
var svc = new NewVersionCheckerHostedService(settings, mockEnv, mockSender, new MockVersionFetcher(), BTCPayLogs);
|
||||
await svc.ProcessVersionCheck();
|
||||
var svc = new MockVersionFetcher(tester.PayTester.GetService<IHttpClientFactory>(),
|
||||
tester.PayTester.GetService<BTCPayServerOptions>(),
|
||||
tester.PayTester.GetService<ILogger<GithubVersionFetcher>>(),
|
||||
settings,
|
||||
mockEnv,
|
||||
mockSender);
|
||||
await svc.Do(CancellationToken.None);
|
||||
|
||||
// since last version present in database was null, it should've been updated with version mock returned
|
||||
var lastVersion = await settings.GetSettingAsync<NewVersionCheckerDataHolder>();
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
@@ -13,49 +12,83 @@ using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class NewVersionCheckerHostedService : BaseAsyncService
|
||||
public class NewVersionCheckerDataHolder
|
||||
{
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly NotificationSender _notificationSender;
|
||||
private readonly IVersionFetcher _versionFetcher;
|
||||
public string LastVersion { get; set; }
|
||||
}
|
||||
|
||||
public NewVersionCheckerHostedService(SettingsRepository settingsRepository, BTCPayServerEnvironment env,
|
||||
NotificationSender notificationSender, IVersionFetcher versionFetcher, Logs logs) : base(logs)
|
||||
public interface IVersionFetcher
|
||||
{
|
||||
Task<string> Fetch(CancellationToken cancellation);
|
||||
}
|
||||
|
||||
public class GithubVersionFetcher : IPeriodicTask, IVersionFetcher
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly Uri _updateurl;
|
||||
|
||||
public GithubVersionFetcher(IHttpClientFactory httpClientFactory,
|
||||
BTCPayServerOptions options, ILogger<GithubVersionFetcher> logger, SettingsRepository settingsRepository,
|
||||
BTCPayServerEnvironment environment, NotificationSender notificationSender)
|
||||
{
|
||||
_logger = logger;
|
||||
_settingsRepository = settingsRepository;
|
||||
_env = env;
|
||||
_environment = environment;
|
||||
_notificationSender = notificationSender;
|
||||
_versionFetcher = versionFetcher;
|
||||
_httpClient = httpClientFactory.CreateClient(nameof(GithubVersionFetcher));
|
||||
_httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
|
||||
_httpClient.DefaultRequestHeaders.Add("User-Agent", "BTCPayServer/NewVersionChecker");
|
||||
|
||||
_updateurl = options.UpdateUrl;
|
||||
}
|
||||
|
||||
internal override Task[] InitializeTasks()
|
||||
private static readonly Regex _releaseVersionTag = new Regex("^(v[1-9]+(\\.[0-9]+)*(-[0-9]+)?)$");
|
||||
private readonly ILogger<GithubVersionFetcher> _logger;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _environment;
|
||||
private readonly NotificationSender _notificationSender;
|
||||
|
||||
public async Task<string> Fetch(CancellationToken cancellation)
|
||||
{
|
||||
return new Task[] { CreateLoopTask(LoopVersionCheck) };
|
||||
if (_updateurl == null)
|
||||
return null;
|
||||
|
||||
using var resp = await _httpClient.GetAsync(_updateurl, cancellation);
|
||||
var strResp = await resp.Content.ReadAsStringAsync(cancellation);
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
var jobj = JObject.Parse(strResp);
|
||||
var tag = jobj["tag_name"].ToString();
|
||||
|
||||
var isReleaseVersionTag = _releaseVersionTag.IsMatch(tag);
|
||||
if (isReleaseVersionTag)
|
||||
{
|
||||
return tag.TrimStart('v');
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Unsuccessful status code returned during new version check. " +
|
||||
$"Url: {_updateurl}, HTTP Code: {resp.StatusCode}, Response Body: {strResp}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async Task LoopVersionCheck()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessVersionCheck();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Events.LogError(ex, "Error while performing new version check");
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromDays(1), CancellationToken);
|
||||
}
|
||||
|
||||
public async Task ProcessVersionCheck()
|
||||
public async Task Do(CancellationToken cancellationToken)
|
||||
{
|
||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.CheckForNewVersions)
|
||||
{
|
||||
var tag = await _versionFetcher.Fetch(CancellationToken);
|
||||
if (tag != null && tag != _env.Version)
|
||||
var tag = await Fetch(cancellationToken);
|
||||
if (tag != null && tag != _environment.Version)
|
||||
{
|
||||
var dh = await _settingsRepository.GetSettingAsync<NewVersionCheckerDataHolder>() ?? new NewVersionCheckerDataHolder();
|
||||
var dh = await _settingsRepository.GetSettingAsync<NewVersionCheckerDataHolder>() ??
|
||||
new NewVersionCheckerDataHolder();
|
||||
if (dh.LastVersion != tag)
|
||||
{
|
||||
await _notificationSender.SendNotification(new AdminScope(), new NewVersionNotification(tag));
|
||||
@@ -67,65 +100,4 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class NewVersionCheckerDataHolder
|
||||
{
|
||||
public string LastVersion { get; set; }
|
||||
}
|
||||
|
||||
public interface IVersionFetcher
|
||||
{
|
||||
Task<string> Fetch(CancellationToken cancellation);
|
||||
}
|
||||
|
||||
public class GithubVersionFetcher : IVersionFetcher
|
||||
{
|
||||
public Logs Logs { get; }
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly Uri _updateurl;
|
||||
public GithubVersionFetcher(IHttpClientFactory httpClientFactory, BTCPayServerOptions options, Logs logs)
|
||||
{
|
||||
Logs = logs;
|
||||
_httpClient = httpClientFactory.CreateClient(nameof(GithubVersionFetcher));
|
||||
_httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
|
||||
_httpClient.DefaultRequestHeaders.Add("User-Agent", "BTCPayServer/NewVersionChecker");
|
||||
|
||||
_updateurl = options.UpdateUrl;
|
||||
}
|
||||
|
||||
private static readonly Regex _releaseVersionTag = new Regex("^(v[1-9]+(\\.[0-9]+)*(-[0-9]+)?)$");
|
||||
public async Task<string> Fetch(CancellationToken cancellation)
|
||||
{
|
||||
if (_updateurl == null)
|
||||
return null;
|
||||
|
||||
using (var resp = await _httpClient.GetAsync(_updateurl, cancellation))
|
||||
{
|
||||
var strResp = await resp.Content.ReadAsStringAsync();
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
var jobj = JObject.Parse(strResp);
|
||||
var tag = jobj["tag_name"].ToString();
|
||||
|
||||
var isReleaseVersionTag = _releaseVersionTag.IsMatch(tag);
|
||||
if (isReleaseVersionTag)
|
||||
{
|
||||
return tag.TrimStart('v');
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logs.Events.LogWarning($"Unsuccessful status code returned during new version check. " +
|
||||
$"Url: {_updateurl}, HTTP Code: {resp.StatusCode}, Response Body: {strResp}");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
140
BTCPayServer/HostedServices/PluginUpdateFetcher.cs
Normal file
140
BTCPayServer/HostedServices/PluginUpdateFetcher.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Plugins;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
internal class PluginUpdateNotification : BaseNotification
|
||||
{
|
||||
private const string TYPE = "pluginupdate";
|
||||
|
||||
internal class Handler : NotificationHandler<PluginUpdateNotification>
|
||||
{
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly BTCPayServerOptions _options;
|
||||
|
||||
public Handler(LinkGenerator linkGenerator, BTCPayServerOptions options)
|
||||
{
|
||||
_linkGenerator = linkGenerator;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public override string NotificationType => TYPE;
|
||||
|
||||
public override (string identifier, string name)[] Meta
|
||||
{
|
||||
get
|
||||
{
|
||||
return new (string identifier, string name)[] {(TYPE, "Plugin update")};
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FillViewModel(PluginUpdateNotification notification, NotificationViewModel vm)
|
||||
{
|
||||
vm.Identifier = notification.Identifier;
|
||||
vm.Type = notification.NotificationType;
|
||||
vm.Body = $"New {notification.Name} plugin version {notification.Version} released!";
|
||||
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIServerController.ListPlugins),
|
||||
"UIServer",
|
||||
new {plugin = notification.PluginIdentifier}, _options.RootPath);
|
||||
}
|
||||
}
|
||||
|
||||
public PluginUpdateNotification()
|
||||
{
|
||||
}
|
||||
|
||||
public PluginUpdateNotification(PluginService.AvailablePlugin plugin)
|
||||
{
|
||||
Name = plugin.Name;
|
||||
PluginIdentifier = plugin.Identifier;
|
||||
Version = plugin.Version.ToString();
|
||||
}
|
||||
|
||||
public string PluginIdentifier { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Version { get; set; }
|
||||
public override string Identifier => TYPE;
|
||||
public override string NotificationType => TYPE;
|
||||
}
|
||||
|
||||
public class PluginVersionCheckerDataHolder
|
||||
{
|
||||
public Dictionary<string, Version> LastVersions { get; set; }
|
||||
}
|
||||
|
||||
public class PluginUpdateFetcher : IPeriodicTask
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly Uri _updateurl;
|
||||
|
||||
public PluginUpdateFetcher(
|
||||
SettingsRepository settingsRepository,
|
||||
ILogger<PluginUpdateFetcher> logger, NotificationSender notificationSender, PluginService pluginService)
|
||||
{
|
||||
_settingsRepository = settingsRepository;
|
||||
_logger = logger;
|
||||
_notificationSender = notificationSender;
|
||||
_pluginService = pluginService;
|
||||
}
|
||||
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly ILogger<PluginUpdateFetcher> _logger;
|
||||
private readonly NotificationSender _notificationSender;
|
||||
private readonly PluginService _pluginService;
|
||||
|
||||
|
||||
public async Task Do(CancellationToken cancellationToken)
|
||||
{
|
||||
var dh = await _settingsRepository.GetSettingAsync<PluginVersionCheckerDataHolder>() ??
|
||||
new PluginVersionCheckerDataHolder();
|
||||
dh.LastVersions ??= new Dictionary<string, Version>();
|
||||
var disabledPlugins = _pluginService.GetDisabledPlugins();
|
||||
|
||||
var installedPlugins =
|
||||
_pluginService.LoadedPlugins.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
|
||||
var remotePlugins = await _pluginService.GetRemotePlugins();
|
||||
var remotePluginsList = remotePlugins
|
||||
.Where(pair => installedPlugins.ContainsKey(pair.Identifier) || disabledPlugins.Contains(pair.Name))
|
||||
.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
|
||||
var notify = new HashSet<string>();
|
||||
foreach (var pair in remotePluginsList)
|
||||
{
|
||||
if (dh.LastVersions.TryGetValue(pair.Key, out var lastVersion) && lastVersion >= pair.Value)
|
||||
continue;
|
||||
if (installedPlugins.TryGetValue(pair.Key, out var installedVersion) && installedVersion < pair.Value)
|
||||
notify.Add(pair.Key);
|
||||
if (disabledPlugins.Contains(pair.Key))
|
||||
{
|
||||
notify.Add(pair.Key);
|
||||
}
|
||||
}
|
||||
|
||||
dh.LastVersions = remotePluginsList;
|
||||
|
||||
foreach (string pluginUpdate in notify)
|
||||
{
|
||||
var plugin = remotePlugins.First(p => p.Identifier == pluginUpdate);
|
||||
await _notificationSender.SendNotification(new AdminScope(), new PluginUpdateNotification(plugin));
|
||||
}
|
||||
|
||||
await _settingsRepository.UpdateSetting(dh);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,6 +379,8 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, StoreEmailRuleProcessorSender>();
|
||||
services.AddSingleton<IHostedService, PeriodicTaskLauncherHostedService>();
|
||||
services.AddScheduledTask<CleanupWebhookDeliveriesTask>(TimeSpan.FromHours(6.0));
|
||||
services.AddScheduledTask<GithubVersionFetcher>(TimeSpan.FromDays(1));
|
||||
services.AddScheduledTask<PluginUpdateFetcher>(TimeSpan.FromDays(1));
|
||||
|
||||
services.AddReportProvider<PaymentsReportProvider>();
|
||||
services.AddReportProvider<OnChainWalletReportProvider>();
|
||||
@@ -439,9 +441,8 @@ namespace BTCPayServer.Hosting
|
||||
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>();
|
||||
|
||||
services.AddSingleton<IVersionFetcher, GithubVersionFetcher>();
|
||||
services.AddSingleton<IHostedService, NewVersionCheckerHostedService>();
|
||||
services.AddSingleton<INotificationHandler, NewVersionNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, PluginUpdateNotification.Handler>();
|
||||
|
||||
services.AddSingleton<INotificationHandler, InvoiceEventNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, PayoutNotification.Handler>();
|
||||
|
||||
Reference in New Issue
Block a user