Compare commits

...

8 Commits

Author SHA1 Message Date
Tyrrrz 9180d51e5e Update version 2023-09-28 19:30:21 +03:00
Tyrrrz a58509fda8 Upgrade to Polly 8 usage 2023-09-28 19:30:12 +03:00
Tyrrrz fbfff4e51f Make tests more resilient 2023-09-28 19:18:52 +03:00
Tyrrrz c466c17793 Update NuGet packages 2023-09-28 16:16:20 +03:00
Tyrrrz 1b2bcf771d Add fallback for progress tasks without name in CLI
Closes #1133
2023-09-26 00:13:22 +03:00
Tyrrrz 99b49f0b8f Allow pack job to run in parallel to test 2023-09-22 14:51:42 +03:00
Tyrrrz 850414b017 Clean up workflow 2023-09-21 20:18:16 +03:00
Tyrrrz 92bf6eab2d Treat all warnings as errors 2023-09-20 21:36:43 +03:00
14 changed files with 139 additions and 97 deletions
+8 -12
View File
@@ -2,17 +2,17 @@ name: main
on: [push, pull_request]
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
test:
runs-on: windows-latest
permissions:
contents: read
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -42,17 +42,11 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
pack:
needs: test
runs-on: windows-latest
permissions:
actions: write
contents: read
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -88,7 +82,9 @@ jobs:
deploy:
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
needs: pack
needs:
- test
- pack
runs-on: ubuntu-latest
permissions:
actions: read
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## v2.41.1 (28-Sep-2023)
- CLI changes:
- Fixed an issue where the export failed to export channels with an empty name.
## v2.41 (15-Sep-2023)
- General changes:
+2 -2
View File
@@ -2,12 +2,12 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>2.41</Version>
<Version>2.41.1</Version>
<Company>Tyrrrz</Company>
<Copyright>Copyright (c) Oleksii Holub</Copyright>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
@@ -12,18 +12,18 @@
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.0.4" />
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="CSharpier.MsBuild" Version="0.25.0" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.2" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
<PackageReference Include="JsonExtensions" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="ReflectionMagic" Version="5.0.0" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0" PrivateAssets="all" />
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.5.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.1" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
@@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;
using AngleSharp.Dom;
using DiscordChatExporter.Cli.Tests.Infra;
@@ -27,7 +28,11 @@ public class HtmlAttachmentSpecs
.Select(e => e.GetAttribute("href"))
.Should()
.Contain(
"https://cdn.discordapp.com/attachments/885587741654536192/885587844964417596/Test.txt"
u =>
u.StartsWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885587844964417596/Test.txt",
StringComparison.Ordinal
)
);
}
@@ -48,7 +53,11 @@ public class HtmlAttachmentSpecs
.Select(e => e.GetAttribute("src"))
.Should()
.Contain(
"https://cdn.discordapp.com/attachments/885587741654536192/885654862430359613/bird-thumbnail.png"
u =>
u.StartsWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885654862430359613/bird-thumbnail.png",
StringComparison.Ordinal
)
);
}
@@ -69,7 +78,7 @@ public class HtmlAttachmentSpecs
var videoUrl = message.QuerySelector("video source")?.GetAttribute("src");
videoUrl
.Should()
.Be(
.StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885655761512968233/file_example_MP4_640_3MG.mp4"
);
}
@@ -91,7 +100,7 @@ public class HtmlAttachmentSpecs
var audioUrl = message.QuerySelector("audio source")?.GetAttribute("src");
audioUrl
.Should()
.Be(
.StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885656175348187146/file_example_MP3_1MG.mp3"
);
}
@@ -145,7 +145,7 @@ public class HtmlEmbedSpecs
// Assert
var iframeUrl = message.QuerySelector("iframe")?.GetAttribute("src");
iframeUrl.Should().Be("https://open.spotify.com/embed/track/1LHZMWefF9502NPfArRfvP");
iframeUrl.Should().StartWith("https://open.spotify.com/embed/track/1LHZMWefF9502NPfArRfvP");
}
[Fact]
@@ -161,7 +161,7 @@ public class HtmlEmbedSpecs
// Assert
var iframeUrl = message.QuerySelector("iframe")?.GetAttribute("src");
iframeUrl.Should().Be("https://www.youtube.com/embed/qOWW4OlgbvE");
iframeUrl.Should().StartWith("https://www.youtube.com/embed/qOWW4OlgbvE");
}
[Fact]
@@ -19,7 +19,7 @@ public class HtmlStickerSpecs
// Assert
var stickerUrl = message.QuerySelector("[title='rock'] img")?.GetAttribute("src");
stickerUrl.Should().Be("https://cdn.discordapp.com/stickers/904215665597120572.png");
stickerUrl.Should().StartWith("https://cdn.discordapp.com/stickers/904215665597120572.png");
}
[Fact]
@@ -35,6 +35,9 @@ public class HtmlStickerSpecs
var stickerUrl = message
.QuerySelector("[title='Yikes'] [data-source]")
?.GetAttribute("data-source");
stickerUrl.Should().Be("https://cdn.discordapp.com/stickers/816087132447178774.json");
stickerUrl
.Should()
.StartWith("https://cdn.discordapp.com/stickers/816087132447178774.json");
}
}
@@ -28,9 +28,10 @@ public class JsonAttachmentSpecs
.GetProperty("url")
.GetString()
.Should()
.Be(
.StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885587844964417596/Test.txt"
);
attachments[0].GetProperty("fileName").GetString().Should().Be("Test.txt");
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(11);
}
@@ -54,9 +55,10 @@ public class JsonAttachmentSpecs
.GetProperty("url")
.GetString()
.Should()
.Be(
.StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885654862430359613/bird-thumbnail.png"
);
attachments[0].GetProperty("fileName").GetString().Should().Be("bird-thumbnail.png");
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(466335);
}
@@ -80,14 +82,16 @@ public class JsonAttachmentSpecs
.GetProperty("url")
.GetString()
.Should()
.Be(
.StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885655761512968233/file_example_MP4_640_3MG.mp4"
);
attachments[0]
.GetProperty("fileName")
.GetString()
.Should()
.Be("file_example_MP4_640_3MG.mp4");
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(3114374);
}
@@ -110,9 +114,10 @@ public class JsonAttachmentSpecs
.GetProperty("url")
.GetString()
.Should()
.Be(
.StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885656175348187146/file_example_MP3_1MG.mp3"
);
attachments[0].GetProperty("fileName").GetString().Should().Be("file_example_MP3_1MG.mp3");
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(1087849);
}
@@ -28,7 +28,7 @@ public class JsonStickerSpecs
.GetProperty("sourceUrl")
.GetString()
.Should()
.Be("https://cdn.discordapp.com/stickers/904215665597120572.png");
.StartWith("https://cdn.discordapp.com/stickers/904215665597120572.png");
}
[Fact]
@@ -50,6 +50,6 @@ public class JsonStickerSpecs
.GetProperty("sourceUrl")
.GetString()
.Should()
.Be("https://cdn.discordapp.com/stickers/816087132447178774.json");
.StartWith("https://cdn.discordapp.com/stickers/816087132447178774.json");
}
}
@@ -36,9 +36,13 @@ internal static class ConsoleExtensions
Func<ProgressTask, ValueTask> performOperationAsync
)
{
// Description cannot be empty
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1133
var actualDescription = !string.IsNullOrWhiteSpace(description) ? description : "?";
var progressTask = progressContext.AddTask(
// Don't recognize random square brackets as style tags
Markup.Escape(description),
Markup.Escape(actualDescription),
new ProgressTaskSettings { MaxValue = 1 }
);
@@ -33,7 +33,7 @@ public class DiscordClient
CancellationToken cancellationToken = default
)
{
return await Http.ResponseResiliencePolicy.ExecuteAsync(
return await Http.ResponseResiliencePipeline.ExecuteAsync(
async innerCancellationToken =>
{
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
@@ -5,11 +5,11 @@
<PackageReference Include="CSharpier.MsBuild" Version="0.25.0" PrivateAssets="all" />
<PackageReference Include="Gress" Version="2.1.1" />
<PackageReference Include="JsonExtensions" Version="1.2.0" />
<PackageReference Include="Polly" Version="7.2.4" />
<PackageReference Include="RazorBlade" Version="0.4.3" />
<PackageReference Include="Polly" Version="8.0.0" />
<PackageReference Include="RazorBlade" Version="0.4.4" />
<PackageReference Include="Superpower" Version="3.0.0" />
<PackageReference Include="WebMarkupMin.Core" Version="2.14.0" />
<PackageReference Include="YoutubeExplode" Version="6.3.1" />
<PackageReference Include="YoutubeExplode" Version="6.3.4" />
</ItemGroup>
</Project>
@@ -53,44 +53,47 @@ internal partial class ExportAssetDownloader
Directory.CreateDirectory(_workingDirPath);
await Http.ResiliencePolicy.ExecuteAsync(async () =>
{
// Download the file
using var response = await Http.Client.GetAsync(url, cancellationToken);
await using (var output = File.Create(filePath))
await response.Content.CopyToAsync(output, cancellationToken);
// Try to set the file date according to the last-modified header
try
await Http.ResiliencePipeline.ExecuteAsync(
async innerCancellationToken =>
{
var lastModified = response.Content.Headers
.TryGetValue("Last-Modified")
?.Pipe(
s =>
DateTimeOffset.TryParse(
s,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var instant
)
? instant
: (DateTimeOffset?)null
);
// Download the file
using var response = await Http.Client.GetAsync(url, innerCancellationToken);
await using (var output = File.Create(filePath))
await response.Content.CopyToAsync(output, innerCancellationToken);
if (lastModified is not null)
// Try to set the file date according to the last-modified header
try
{
File.SetCreationTimeUtc(filePath, lastModified.Value.UtcDateTime);
File.SetLastWriteTimeUtc(filePath, lastModified.Value.UtcDateTime);
File.SetLastAccessTimeUtc(filePath, lastModified.Value.UtcDateTime);
var lastModified = response.Content.Headers
.TryGetValue("Last-Modified")
?.Pipe(
s =>
DateTimeOffset.TryParse(
s,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var instant
)
? instant
: (DateTimeOffset?)null
);
if (lastModified is not null)
{
File.SetCreationTimeUtc(filePath, lastModified.Value.UtcDateTime);
File.SetLastWriteTimeUtc(filePath, lastModified.Value.UtcDateTime);
File.SetLastAccessTimeUtc(filePath, lastModified.Value.UtcDateTime);
}
}
}
catch
{
// This can apparently fail for some reason.
// Updating the file date is not a critical task, so we'll just ignore exceptions thrown here.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/585
}
});
catch
{
// This can apparently fail for some reason.
// Updating the file date is not a critical task, so we'll just ignore exceptions thrown here.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/585
}
},
cancellationToken
);
return _previousPathsByUrl[url] = filePath;
}
+40 -23
View File
@@ -7,6 +7,7 @@ using System.Security.Authentication;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Utils.Extensions;
using Polly;
using Polly.Retry;
namespace DiscordChatExporter.Core.Utils;
@@ -31,29 +32,45 @@ public static class Http
&& IsRetryableStatusCode(hrex.StatusCode ?? HttpStatusCode.OK)
);
public static IAsyncPolicy ResiliencePolicy { get; } =
Policy
.Handle<Exception>(IsRetryableException)
.WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1));
public static IAsyncPolicy<HttpResponseMessage> ResponseResiliencePolicy { get; } =
Policy
.Handle<Exception>(IsRetryableException)
.OrResult<HttpResponseMessage>(m => IsRetryableStatusCode(m.StatusCode))
.WaitAndRetryAsync(
8,
(i, result, _) =>
public static ResiliencePipeline ResiliencePipeline { get; } =
new ResiliencePipelineBuilder()
.AddRetry(
new RetryStrategyOptions
{
// If rate-limited, use retry-after header as the guide.
// The response can be null here if an exception was thrown.
if (result.Result?.Headers.RetryAfter?.Delta is { } retryAfter)
{
// Add some buffer just in case
return retryAfter + TimeSpan.FromSeconds(1);
}
ShouldHandle = new PredicateBuilder().Handle<Exception>(IsRetryableException),
MaxRetryAttempts = 4,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromSeconds(1)
}
)
.Build();
return TimeSpan.FromSeconds(Math.Pow(2, i) + 1);
},
(_, _, _, _) => Task.CompletedTask
);
public static ResiliencePipeline<HttpResponseMessage> ResponseResiliencePipeline { get; } =
new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddRetry(
new RetryStrategyOptions<HttpResponseMessage>
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<Exception>(IsRetryableException)
.HandleResult(m => IsRetryableStatusCode(m.StatusCode)),
MaxRetryAttempts = 8,
DelayGenerator = args =>
{
// If rate-limited, use retry-after header as the guide.
// The response can be null here if an exception was thrown.
if (args.Outcome.Result?.Headers.RetryAfter?.Delta is { } retryAfter)
{
// Add some buffer just in case
return ValueTask.FromResult<TimeSpan?>(
retryAfter + TimeSpan.FromSeconds(1)
);
}
return ValueTask.FromResult<TimeSpan?>(
TimeSpan.FromSeconds(Math.Pow(2, args.AttemptNumber) + 1)
);
}
}
)
.Build();
}