Send notification when a new plugin version is available (#5450)

This commit is contained in:
Andrew Camilleri
2023-11-30 10:12:44 +01:00
committed by GitHub
parent b31f1812d2
commit b0554bbf17
4 changed files with 221 additions and 96 deletions

View File

@@ -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>();

View File

@@ -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;
}
}
}

View 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);
}
}
}

View File

@@ -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>();