2025-04-13 09:26:21 +00:00
|
|
|
|
using System;
|
2023-07-04 20:44:35 +00:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Net;
|
|
|
|
|
|
using System.Net.Http;
|
2023-07-04 21:37:31 +00:00
|
|
|
|
using System.Net.Http.Json;
|
2025-04-13 08:44:14 +00:00
|
|
|
|
using System.Net.Sockets;
|
2024-01-14 05:49:08 +00:00
|
|
|
|
using System.Text.Json;
|
|
|
|
|
|
using System.Text.Json.Serialization;
|
2023-07-04 20:44:35 +00:00
|
|
|
|
using System.Threading;
|
|
|
|
|
|
using System.Threading.Tasks;
|
2025-04-13 09:26:21 +00:00
|
|
|
|
using Flow.Launcher.Infrastructure.Http;
|
|
|
|
|
|
using Flow.Launcher.Plugin;
|
2023-07-04 20:44:35 +00:00
|
|
|
|
|
|
|
|
|
|
namespace Flow.Launcher.Core.ExternalPlugins
|
|
|
|
|
|
{
|
|
|
|
|
|
public record CommunityPluginSource(string ManifestFileUrl)
|
|
|
|
|
|
{
|
2025-04-17 23:17:33 +00:00
|
|
|
|
private static readonly string ClassName = nameof(CommunityPluginSource);
|
|
|
|
|
|
|
2023-07-04 20:44:35 +00:00
|
|
|
|
private string latestEtag = "";
|
|
|
|
|
|
|
2025-09-23 09:40:54 +00:00
|
|
|
|
private List<UserPlugin> plugins = [];
|
2023-07-04 20:44:35 +00:00
|
|
|
|
|
2025-04-13 08:36:34 +00:00
|
|
|
|
private static readonly JsonSerializerOptions PluginStoreItemSerializationOption = new()
|
2024-01-14 05:49:08 +00:00
|
|
|
|
{
|
|
|
|
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2023-07-04 20:44:35 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Fetch and deserialize the contents of a plugins.json file found at <see cref="ManifestFileUrl"/>.
|
|
|
|
|
|
/// We use conditional http requests to keep repeat requests fast.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// This method will only return plugin details when the underlying http request is successful (200 or 304).
|
|
|
|
|
|
/// In any other case, an exception is raised
|
|
|
|
|
|
/// </remarks>
|
|
|
|
|
|
public async Task<List<UserPlugin>> FetchAsync(CancellationToken token)
|
|
|
|
|
|
{
|
2025-09-23 09:40:54 +00:00
|
|
|
|
PublicApi.Instance.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}");
|
2023-07-04 20:44:35 +00:00
|
|
|
|
|
|
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Get, ManifestFileUrl);
|
|
|
|
|
|
|
|
|
|
|
|
request.Headers.Add("If-None-Match", latestEtag);
|
|
|
|
|
|
|
2025-04-13 08:44:14 +00:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token)
|
2024-01-14 05:49:08 +00:00
|
|
|
|
.ConfigureAwait(false);
|
2023-07-04 20:44:35 +00:00
|
|
|
|
|
2025-04-13 08:44:14 +00:00
|
|
|
|
if (response.StatusCode == HttpStatusCode.OK)
|
|
|
|
|
|
{
|
|
|
|
|
|
plugins = await response.Content
|
|
|
|
|
|
.ReadFromJsonAsync<List<UserPlugin>>(PluginStoreItemSerializationOption, cancellationToken: token)
|
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
latestEtag = response.Headers.ETag?.Tag;
|
2023-07-04 20:44:35 +00:00
|
|
|
|
|
2025-09-23 09:40:54 +00:00
|
|
|
|
PublicApi.Instance.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}");
|
2025-04-13 08:44:14 +00:00
|
|
|
|
return plugins;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (response.StatusCode == HttpStatusCode.NotModified)
|
|
|
|
|
|
{
|
2025-09-23 09:40:54 +00:00
|
|
|
|
PublicApi.Instance.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified.");
|
2025-04-13 08:44:14 +00:00
|
|
|
|
return plugins;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-09-23 09:40:54 +00:00
|
|
|
|
PublicApi.Instance.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
2025-04-17 23:17:33 +00:00
|
|
|
|
return null;
|
2025-04-13 08:44:14 +00:00
|
|
|
|
}
|
2023-07-04 20:44:35 +00:00
|
|
|
|
}
|
2025-09-13 16:04:32 +00:00
|
|
|
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
2025-09-13 15:30:13 +00:00
|
|
|
|
{
|
2025-09-23 09:40:54 +00:00
|
|
|
|
PublicApi.Instance.LogDebug(ClassName, $"Fetching from {ManifestFileUrl} was cancelled by caller.");
|
2025-09-13 16:04:32 +00:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (TaskCanceledException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Likely an HttpClient timeout or external cancellation not requested by our token
|
2025-09-23 09:40:54 +00:00
|
|
|
|
PublicApi.Instance.LogWarn(ClassName, $"Fetching from {ManifestFileUrl} timed out.");
|
2025-09-13 15:30:13 +00:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2025-04-13 08:44:14 +00:00
|
|
|
|
catch (Exception e)
|
2023-07-04 20:44:35 +00:00
|
|
|
|
{
|
2025-04-13 08:44:14 +00:00
|
|
|
|
if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
|
|
|
|
|
|
{
|
2025-09-23 09:40:54 +00:00
|
|
|
|
PublicApi.Instance.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e);
|
2025-04-13 08:44:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-09-23 09:40:54 +00:00
|
|
|
|
PublicApi.Instance.LogException(ClassName, "Error Occurred", e);
|
2025-04-13 08:44:14 +00:00
|
|
|
|
}
|
2025-04-17 23:17:33 +00:00
|
|
|
|
return null;
|
2023-07-04 20:44:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|