mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2026-01-16 08:25:28 +00:00
Implement plugin updates with IPluginUpdates interface (#3151)
* Initial implementation of plugin updates
* Update PluginsCore.cs
* Update IPluginUpdates.cs
* Update PluginsCore.cs
* Make it work
* Misc
* Revert "Misc"
This reverts commit bccd1bb2b8.
* Proper fix
* Make plugin updates independent of GitHub
* Final touches
* Misc
* Allow plugin creators for more flexibility in picking from GitHub releases
* Misc rename
* Make changelog internal again
This is ASF implementation detail, make body available instead and let people implement changelogs themselves
* Misc
* Add missing localization
* Add a way to disable plugin updates
* Update PluginsCore.cs
* Update PluginsCore.cs
* Misc
* Update IGitHubPluginUpdates.cs
* Update IGitHubPluginUpdates.cs
* Update IGitHubPluginUpdates.cs
* Update IGitHubPluginUpdates.cs
* Make zip selection ignore case
* Update ArchiSteamFarm/Core/Utilities.cs
Co-authored-by: Vita Chumakova <me@ezhevita.dev>
* Misc error notify
* Add commands and finally call it a day
* Misc progress percentages text
* Misc
* Flip DefaultPluginsUpdateMode as per the voting
* Misc
---------
Co-authored-by: Vita Chumakova <me@ezhevita.dev>
This commit is contained in:
committed by
GitHub
parent
c874779c0d
commit
aedede3ba4
49
ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs
Normal file
49
ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ArchiSteamFarm.Web.GitHub.Data;
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
public sealed class ReleaseAsset {
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("browser_download_url")]
|
||||
[JsonRequired]
|
||||
public Uri DownloadURL { get; private init; } = null!;
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("name")]
|
||||
[JsonRequired]
|
||||
public string Name { get; private init; } = "";
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("size")]
|
||||
[JsonRequired]
|
||||
public uint Size { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
private ReleaseAsset() { }
|
||||
}
|
||||
152
ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs
Normal file
152
ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Core;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace ArchiSteamFarm.Web.GitHub.Data;
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
public sealed class ReleaseResponse {
|
||||
internal string? ChangelogHTML {
|
||||
get {
|
||||
if (BackingChangelogHTML != null) {
|
||||
return BackingChangelogHTML;
|
||||
}
|
||||
|
||||
if (Changelog == null) {
|
||||
ASF.ArchiLogger.LogNullError(Changelog);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
using StringWriter writer = new();
|
||||
|
||||
HtmlRenderer renderer = new(writer);
|
||||
|
||||
renderer.Render(Changelog);
|
||||
writer.Flush();
|
||||
|
||||
return BackingChangelogHTML = writer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
internal string? ChangelogPlainText {
|
||||
get {
|
||||
if (BackingChangelogPlainText != null) {
|
||||
return BackingChangelogPlainText;
|
||||
}
|
||||
|
||||
if (Changelog == null) {
|
||||
ASF.ArchiLogger.LogNullError(Changelog);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
using StringWriter writer = new();
|
||||
|
||||
HtmlRenderer renderer = new(writer) {
|
||||
EnableHtmlForBlock = false,
|
||||
EnableHtmlForInline = false,
|
||||
EnableHtmlEscape = false
|
||||
};
|
||||
|
||||
renderer.Render(Changelog);
|
||||
writer.Flush();
|
||||
|
||||
return BackingChangelogPlainText = writer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private MarkdownDocument? Changelog {
|
||||
get {
|
||||
if (BackingChangelog != null) {
|
||||
return BackingChangelog;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(MarkdownBody)) {
|
||||
ASF.ArchiLogger.LogNullError(MarkdownBody);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return BackingChangelog = ExtractChangelogFromBody(MarkdownBody);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("assets")]
|
||||
[JsonRequired]
|
||||
public ImmutableHashSet<ReleaseAsset> Assets { get; private init; } = ImmutableHashSet<ReleaseAsset>.Empty;
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("prerelease")]
|
||||
[JsonRequired]
|
||||
public bool IsPreRelease { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("body")]
|
||||
[JsonRequired]
|
||||
public string MarkdownBody { get; private init; } = "";
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("published_at")]
|
||||
[JsonRequired]
|
||||
public DateTime PublishedAt { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("tag_name")]
|
||||
[JsonRequired]
|
||||
public string Tag { get; private init; } = "";
|
||||
|
||||
private MarkdownDocument? BackingChangelog;
|
||||
private string? BackingChangelogHTML;
|
||||
private string? BackingChangelogPlainText;
|
||||
|
||||
[JsonConstructor]
|
||||
private ReleaseResponse() { }
|
||||
|
||||
private static MarkdownDocument ExtractChangelogFromBody(string markdownText) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(markdownText);
|
||||
|
||||
MarkdownDocument markdownDocument = Markdown.Parse(markdownText);
|
||||
MarkdownDocument result = [];
|
||||
|
||||
foreach (Block block in markdownDocument.SkipWhile(static block => block is not HeadingBlock { Inline.FirstChild: LiteralInline literalInline } || (literalInline.Content.ToString()?.Equals("Changelog", StringComparison.OrdinalIgnoreCase) != true)).Skip(1).TakeWhile(static block => block is not ThematicBreakBlock).ToList()) {
|
||||
// All blocks that we're interested in must be removed from original markdownDocument firstly
|
||||
markdownDocument.Remove(block);
|
||||
result.Add(block);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@@ -22,45 +24,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Web.GitHub.Data;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ArchiSteamFarm.Web;
|
||||
namespace ArchiSteamFarm.Web.GitHub;
|
||||
|
||||
internal static class GitHub {
|
||||
internal static async Task<ReleaseResponse?> GetLatestRelease(bool stable = true, CancellationToken cancellationToken = default) {
|
||||
Uri request = new($"{SharedInfo.GithubReleaseURL}{(stable ? "/latest" : "?per_page=1")}");
|
||||
public static class GitHubService {
|
||||
private static Uri URL => new("https://api.github.com");
|
||||
|
||||
[PublicAPI]
|
||||
public static async Task<ReleaseResponse?> GetLatestRelease(string repoName, bool stable = true, CancellationToken cancellationToken = default) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(repoName);
|
||||
|
||||
if (stable) {
|
||||
Uri request = new(URL, $"/repos/{repoName}/releases/latest");
|
||||
|
||||
return await GetReleaseFromURL(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
ImmutableList<ReleaseResponse>? response = await GetReleasesFromURL(request, cancellationToken).ConfigureAwait(false);
|
||||
ImmutableList<ReleaseResponse>? response = await GetReleases(repoName, 1, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response?.FirstOrDefault();
|
||||
}
|
||||
|
||||
internal static async Task<ReleaseResponse?> GetRelease(string version, CancellationToken cancellationToken = default) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(version);
|
||||
[PublicAPI]
|
||||
public static async Task<ReleaseResponse?> GetRelease(string repoName, string tag, CancellationToken cancellationToken = default) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(repoName);
|
||||
ArgumentException.ThrowIfNullOrEmpty(tag);
|
||||
|
||||
Uri request = new($"{SharedInfo.GithubReleaseURL}/tags/{version}");
|
||||
Uri request = new(URL, $"/repos/{repoName}/releases/tags/{tag}");
|
||||
|
||||
return await GetReleaseFromURL(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static async Task<ImmutableList<ReleaseResponse>?> GetReleases(string repoName, byte count = 10, CancellationToken cancellationToken = default) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(repoName);
|
||||
ArgumentOutOfRangeException.ThrowIfZero(count);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(count, 100);
|
||||
|
||||
Uri request = new(URL, $"/repos/{repoName}/releases?per_page={count}");
|
||||
|
||||
return await GetReleasesFromURL(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static async Task<Dictionary<string, DateTime>?> GetWikiHistory(string page, CancellationToken cancellationToken = default) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(page);
|
||||
|
||||
@@ -152,21 +167,6 @@ internal static class GitHub {
|
||||
return markdownBodyNode?.InnerHtml.Trim() ?? "";
|
||||
}
|
||||
|
||||
private static MarkdownDocument ExtractChangelogFromBody(string markdownText) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(markdownText);
|
||||
|
||||
MarkdownDocument markdownDocument = Markdown.Parse(markdownText);
|
||||
MarkdownDocument result = [];
|
||||
|
||||
foreach (Block block in markdownDocument.SkipWhile(static block => block is not HeadingBlock { Inline.FirstChild: LiteralInline literalInline } || (literalInline.Content.ToString()?.Equals("Changelog", StringComparison.OrdinalIgnoreCase) != true)).Skip(1).TakeWhile(static block => block is not ThematicBreakBlock).ToList()) {
|
||||
// All blocks that we're interested in must be removed from original markdownDocument firstly
|
||||
markdownDocument.Remove(block);
|
||||
result.Add(block);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<ReleaseResponse?> GetReleaseFromURL(Uri request, CancellationToken cancellationToken = default) {
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
@@ -190,125 +190,4 @@ internal static class GitHub {
|
||||
|
||||
return response?.Content;
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
internal sealed class ReleaseResponse {
|
||||
internal string? ChangelogHTML {
|
||||
get {
|
||||
if (BackingChangelogHTML != null) {
|
||||
return BackingChangelogHTML;
|
||||
}
|
||||
|
||||
if (Changelog == null) {
|
||||
ASF.ArchiLogger.LogNullError(Changelog);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
using StringWriter writer = new();
|
||||
|
||||
HtmlRenderer renderer = new(writer);
|
||||
|
||||
renderer.Render(Changelog);
|
||||
writer.Flush();
|
||||
|
||||
return BackingChangelogHTML = writer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
internal string? ChangelogPlainText {
|
||||
get {
|
||||
if (BackingChangelogPlainText != null) {
|
||||
return BackingChangelogPlainText;
|
||||
}
|
||||
|
||||
if (Changelog == null) {
|
||||
ASF.ArchiLogger.LogNullError(Changelog);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
using StringWriter writer = new();
|
||||
|
||||
HtmlRenderer renderer = new(writer) {
|
||||
EnableHtmlForBlock = false,
|
||||
EnableHtmlForInline = false,
|
||||
EnableHtmlEscape = false
|
||||
};
|
||||
|
||||
renderer.Render(Changelog);
|
||||
writer.Flush();
|
||||
|
||||
return BackingChangelogPlainText = writer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private MarkdownDocument? Changelog {
|
||||
get {
|
||||
if (BackingChangelog != null) {
|
||||
return BackingChangelog;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(MarkdownBody)) {
|
||||
ASF.ArchiLogger.LogNullError(MarkdownBody);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return BackingChangelog = ExtractChangelogFromBody(MarkdownBody);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("assets")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<Asset> Assets { get; private init; } = ImmutableHashSet<Asset>.Empty;
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("prerelease")]
|
||||
[JsonRequired]
|
||||
internal bool IsPreRelease { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("published_at")]
|
||||
[JsonRequired]
|
||||
internal DateTime PublishedAt { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("tag_name")]
|
||||
[JsonRequired]
|
||||
internal string Tag { get; private init; } = "";
|
||||
|
||||
private MarkdownDocument? BackingChangelog;
|
||||
private string? BackingChangelogHTML;
|
||||
private string? BackingChangelogPlainText;
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("body")]
|
||||
[JsonRequired]
|
||||
private string? MarkdownBody { get; init; } = "";
|
||||
|
||||
[JsonConstructor]
|
||||
private ReleaseResponse() { }
|
||||
|
||||
internal sealed class Asset {
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("browser_download_url")]
|
||||
[JsonRequired]
|
||||
internal Uri? DownloadURL { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("name")]
|
||||
[JsonRequired]
|
||||
internal string? Name { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("size")]
|
||||
[JsonRequired]
|
||||
internal uint Size { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
private Asset() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,22 +177,24 @@ public sealed class WebBrowser : IDisposable {
|
||||
while (response.Content.CanRead) {
|
||||
int read = await response.Content.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (read == 0) {
|
||||
if (read <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Report progress in-between downloading only if file is big enough to justify it
|
||||
// Current logic below will report progress if file is bigger than ~800 KB
|
||||
if (batchIncreaseSize >= buffer.Length) {
|
||||
readThisBatch += read;
|
||||
|
||||
for (; (readThisBatch >= batchIncreaseSize) && (batch < 99); readThisBatch -= batchIncreaseSize) {
|
||||
// We need a copy of variable being passed when in for loops, as loop will proceed before our event is launched
|
||||
byte progress = ++batch;
|
||||
|
||||
progressReporter?.Report(progress);
|
||||
}
|
||||
}
|
||||
|
||||
await ms.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if ((progressReporter == null) || (batchIncreaseSize == 0) || (batch >= 99)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
readThisBatch += read;
|
||||
|
||||
while ((readThisBatch >= batchIncreaseSize) && (batch < 99)) {
|
||||
readThisBatch -= batchIncreaseSize;
|
||||
progressReporter.Report(++batch);
|
||||
}
|
||||
}
|
||||
} catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {
|
||||
throw;
|
||||
|
||||
Reference in New Issue
Block a user