From b0554bbf170b100606a2b665bfc99601f6f25c8e Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 30 Nov 2023 10:12:44 +0100 Subject: [PATCH] Send notification when a new plugin version is available (#5450) --- BTCPayServer.Tests/UnitTest1.cs | 20 ++- ...stedService.cs => GithubVersionFetcher.cs} | 152 +++++++----------- .../HostedServices/PluginUpdateFetcher.cs | 140 ++++++++++++++++ BTCPayServer/Hosting/BTCPayServerServices.cs | 5 +- 4 files changed, 221 insertions(+), 96 deletions(-) rename BTCPayServer/HostedServices/{NewVersionCheckerHostedService.cs => GithubVersionFetcher.cs} (51%) create mode 100644 BTCPayServer/HostedServices/PluginUpdateFetcher.cs diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index b72524c37..f5786d496 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -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 Fetch(CancellationToken cancellation) + public override Task Fetch(CancellationToken cancellation) { return Task.FromResult(MOCK_NEW_VERSION); } + + public MockVersionFetcher(IHttpClientFactory httpClientFactory, BTCPayServerOptions options, ILogger 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(); var mockSender = tester.PayTester.GetService(); - var svc = new NewVersionCheckerHostedService(settings, mockEnv, mockSender, new MockVersionFetcher(), BTCPayLogs); - await svc.ProcessVersionCheck(); + var svc = new MockVersionFetcher(tester.PayTester.GetService(), + tester.PayTester.GetService(), + tester.PayTester.GetService>(), + 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(); diff --git a/BTCPayServer/HostedServices/NewVersionCheckerHostedService.cs b/BTCPayServer/HostedServices/GithubVersionFetcher.cs similarity index 51% rename from BTCPayServer/HostedServices/NewVersionCheckerHostedService.cs rename to BTCPayServer/HostedServices/GithubVersionFetcher.cs index 875d4203a..5f08df9e6 100644 --- a/BTCPayServer/HostedServices/NewVersionCheckerHostedService.cs +++ b/BTCPayServer/HostedServices/GithubVersionFetcher.cs @@ -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 Fetch(CancellationToken cancellation); + } + + public class GithubVersionFetcher : IPeriodicTask, IVersionFetcher + { + private readonly HttpClient _httpClient; + private readonly Uri _updateurl; + + public GithubVersionFetcher(IHttpClientFactory httpClientFactory, + BTCPayServerOptions options, ILogger 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 _logger; + private readonly SettingsRepository _settingsRepository; + private readonly BTCPayServerEnvironment _environment; + private readonly NotificationSender _notificationSender; + + public async Task 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() ?? 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() ?? new NewVersionCheckerDataHolder(); + var dh = await _settingsRepository.GetSettingAsync() ?? + 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 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 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; - } - } } diff --git a/BTCPayServer/HostedServices/PluginUpdateFetcher.cs b/BTCPayServer/HostedServices/PluginUpdateFetcher.cs new file mode 100644 index 000000000..840afc8ea --- /dev/null +++ b/BTCPayServer/HostedServices/PluginUpdateFetcher.cs @@ -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 + { + 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 LastVersions { get; set; } + } + + public class PluginUpdateFetcher : IPeriodicTask + { + private readonly HttpClient _httpClient; + private readonly Uri _updateurl; + + public PluginUpdateFetcher( + SettingsRepository settingsRepository, + ILogger logger, NotificationSender notificationSender, PluginService pluginService) + { + _settingsRepository = settingsRepository; + _logger = logger; + _notificationSender = notificationSender; + _pluginService = pluginService; + } + + private readonly SettingsRepository _settingsRepository; + private readonly ILogger _logger; + private readonly NotificationSender _notificationSender; + private readonly PluginService _pluginService; + + + public async Task Do(CancellationToken cancellationToken) + { + var dh = await _settingsRepository.GetSettingAsync() ?? + new PluginVersionCheckerDataHolder(); + dh.LastVersions ??= new Dictionary(); + 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(); + 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); + } + } +} diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 9252d4baf..a9def9b71 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -379,6 +379,8 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(); services.AddScheduledTask(TimeSpan.FromHours(6.0)); + services.AddScheduledTask(TimeSpan.FromDays(1)); + services.AddScheduledTask(TimeSpan.FromDays(1)); services.AddReportProvider(); services.AddReportProvider(); @@ -439,9 +441,8 @@ namespace BTCPayServer.Hosting services.AddScoped(); services.AddScoped(); - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton();