Merge pull request #6896 from btcpayserver/fix/unlisted-plugins-updates

fix: ensure unlisted installed plugins appear as updatable
This commit is contained in:
rockstardev
2025-09-05 11:33:04 -05:00
committed by GitHub
3 changed files with 101 additions and 26 deletions

View File

@@ -26,12 +26,12 @@ namespace BTCPayServer.Controllers
{
availablePlugins = await pluginService.GetRemotePlugins(search);
}
catch (Exception)
catch (Exception ex)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = StringLocalizer["Remote plugins lookup failed. Try again later."].Value
Message = StringLocalizer["Remote plugins lookup failed. Try again later. Error: {0}", ex.Message].Value
});
availablePlugins = Array.Empty<PluginService.AvailablePlugin>();
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
@@ -46,32 +47,35 @@ namespace BTCPayServer.Plugins
public JObject ManifestInfo { get; set; }
public string Documentation { get; set; }
}
public record InstalledPluginRequest(string Identifier, string Version);
public class PluginBuilderClient
{
HttpClient httpClient;
public HttpClient HttpClient => httpClient;
private readonly HttpClient _httpClient;
public HttpClient HttpClient => _httpClient;
public PluginBuilderClient(HttpClient httpClient)
{
this.httpClient = httpClient;
_httpClient = httpClient;
}
static JsonSerializerSettings serializerSettings = new() { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() };
public async Task<PublishedVersion[]> GetPublishedVersions(string btcpayVersion, bool includePreRelease, string searchPluginName = null, bool? includeAllVersions = null)
{
var queryString = $"?includePreRelease={includePreRelease}";
if (btcpayVersion is not null)
queryString += $"&btcpayVersion={btcpayVersion}";
queryString += $"&btcpayVersion={Uri.EscapeDataString(btcpayVersion)}";
if (searchPluginName is not null)
queryString += $"&searchPluginName={searchPluginName}";
queryString += $"&searchPluginName={Uri.EscapeDataString(searchPluginName)}";
if (includeAllVersions is not null)
queryString += $"&includeAllVersions={includeAllVersions}";
var result = await httpClient.GetStringAsync($"api/v1/plugins{queryString}");
var result = await _httpClient.GetStringAsync($"api/v1/plugins{queryString}");
return JsonConvert.DeserializeObject<PublishedVersion[]>(result, serializerSettings) ?? throw new InvalidOperationException();
}
public async Task<PublishedVersion> GetPlugin(string pluginSlug, string version)
{
try
{
var result = await httpClient.GetStringAsync($"api/v1/plugins/{pluginSlug}/versions/{version}");
var result = await _httpClient.GetStringAsync($"api/v1/plugins/{pluginSlug}/versions/{version}");
return JsonConvert.DeserializeObject<PublishedVersion>(result, serializerSettings);
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
@@ -85,9 +89,33 @@ namespace BTCPayServer.Plugins
var queryString = $"?btcpayVersion={btcpayVersion}&includePreRelease={includePreRelease}&includeAllVersions={includeAllVersions}";
var url = $"api/v1/plugins/{identifier}{queryString}";
var result = await httpClient.GetStringAsync(url);
var result = await _httpClient.GetStringAsync(url);
return JsonConvert.DeserializeObject<PublishedVersion[]>(result, serializerSettings)
?? throw new InvalidOperationException();
}
public async Task<PublishedVersion[]> GetInstalledPluginsUpdates(
string btcpayVersion,
bool includePreRelease,
IEnumerable<InstalledPluginRequest> plugins)
{
var queryString = $"?includePreRelease={includePreRelease}";
if (!string.IsNullOrWhiteSpace(btcpayVersion))
queryString += $"&btcpayVersion={Uri.EscapeDataString(btcpayVersion)}";
var json = JsonConvert.SerializeObject(plugins, serializerSettings);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
using var resp = await _httpClient.PostAsync($"api/v1/plugins/updates{queryString}", content);
resp.EnsureSuccessStatusCode();
var body = await resp.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<PublishedVersion[]>(body, serializerSettings);
if (result is null)
throw new JsonException("Plugin updates response deserialized to null.");
return result;
}
}
}

View File

@@ -20,12 +20,14 @@ namespace BTCPayServer.Plugins
private readonly IOptions<DataDirectories> _dataDirectories;
private readonly PoliciesSettings _policiesSettings;
private readonly PluginBuilderClient _pluginBuilderClient;
public PluginService(
IEnumerable<IBTCPayServerPlugin> btcPayServerPlugins,
PluginBuilderClient pluginBuilderClient,
IOptions<DataDirectories> dataDirectories,
PoliciesSettings policiesSettings,
BTCPayServerEnvironment env)
BTCPayServerEnvironment env
)
{
LoadedPlugins = btcPayServerPlugins;
Installed = btcPayServerPlugins.ToDictionary(p => p.Identifier, p => p.Version, StringComparer.OrdinalIgnoreCase);
@@ -49,32 +51,77 @@ namespace BTCPayServer.Plugins
return pluginManifest.Version;
}
private string GetShortBtcpayVersion() => Env.Version.TrimStart('v').Split('+')[0];
public async Task<AvailablePlugin[]> GetRemotePlugins(string searchPluginName)
{
string btcpayVersion = Env.Version.TrimStart('v').Split('+')[0];
string btcpayVersion = GetShortBtcpayVersion();
var versions = await _pluginBuilderClient.GetPublishedVersions(
btcpayVersion, _policiesSettings.PluginPreReleases, searchPluginName);
return versions.Select(v =>
var plugins = versions
.Select(MapToAvailablePlugin)
.Where(p => p is not null)
.Select(p => p!)
.ToList();
var listedIds = new HashSet<string>(
plugins.Select(p => p.Identifier),
StringComparer.OrdinalIgnoreCase);
var loadedToCheck = LoadedPlugins
.Where(p => !p.SystemPlugin && !listedIds.Contains(p.Identifier))
.Select(p => new InstalledPluginRequest(p.Identifier, p.Version.ToString()))
.ToList();
if (loadedToCheck.Count <= 0) return plugins.ToArray();
var updates = await _pluginBuilderClient.GetInstalledPluginsUpdates(
btcpayVersion,
_policiesSettings.PluginPreReleases,
loadedToCheck);
if (updates is { Length: > 0 })
{
var p = v.ManifestInfo.ToObject<AvailablePlugin>();
p.Documentation = v.Documentation;
var github = v.BuildInfo.GetGithubRepository();
if (github != null)
{
p.Source = github.GetSourceUrl(v.BuildInfo.gitCommit, v.BuildInfo.pluginDir);
p.Author = github.Owner;
p.AuthorLink = $"https://github.com/{github.Owner}";
}
p.SystemPlugin = false;
return p;
}).ToArray();
plugins.AddRange(
updates.Select(MapToAvailablePlugin)
.Where(p => p is not null)
.Select(p => p!)
);
}
return plugins.ToArray();
}
#nullable enable
private AvailablePlugin? MapToAvailablePlugin(PublishedVersion publishedVersion)
{
if (publishedVersion.ManifestInfo is null)
return null;
var availablePlugin = publishedVersion.ManifestInfo.ToObject<AvailablePlugin>();
if (availablePlugin is null)
throw new InvalidDataException($"Manifest deserialized to null BuildId: {publishedVersion.BuildId} PluginSlug: {publishedVersion.ProjectSlug}");
availablePlugin.Documentation = publishedVersion.Documentation;
var buildInfo = publishedVersion.BuildInfo;
var github = buildInfo?.GetGithubRepository();
if (buildInfo is not null && github is not null)
{
availablePlugin.Source = github.GetSourceUrl(buildInfo.gitCommit, buildInfo.pluginDir);
availablePlugin.Author = github.Owner;
availablePlugin.AuthorLink = $"https://github.com/{github.Owner}";
}
availablePlugin.SystemPlugin = false;
return availablePlugin;
}
#nullable restore
public async Task<AvailablePlugin> DownloadRemotePlugin(string pluginIdentifier, string version, VersionCondition condition = null)
{
if (version is null)
{
string btcpayVersion = Env.Version.TrimStart('v').Split('+')[0];
string btcpayVersion = GetShortBtcpayVersion();
var versions = await _pluginBuilderClient.GetPluginVersionsForDownload(pluginIdentifier,
btcpayVersion, _policiesSettings.PluginPreReleases, includeAllVersions: true);
var potentialVersions = versions