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] on: [push, pull_request]
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs: jobs:
test: test:
runs-on: windows-latest runs-on: windows-latest
permissions: permissions:
contents: read contents: read
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@@ -42,17 +42,11 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
pack: pack:
needs: test
runs-on: windows-latest runs-on: windows-latest
permissions: permissions:
actions: write actions: write
contents: read contents: read
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@@ -88,7 +82,9 @@ jobs:
deploy: deploy:
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
needs: pack needs:
- test
- pack
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
actions: read actions: read
+5
View File
@@ -1,5 +1,10 @@
# Changelog # 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) ## v2.41 (15-Sep-2023)
- General changes: - General changes:
+2 -2
View File
@@ -2,12 +2,12 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<Version>2.41</Version> <Version>2.41.1</Version>
<Company>Tyrrrz</Company> <Company>Tyrrrz</Company>
<Copyright>Copyright (c) Oleksii Holub</Copyright> <Copyright>Copyright (c) Oleksii Holub</Copyright>
<LangVersion>preview</LangVersion> <LangVersion>preview</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
@@ -12,18 +12,18 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AngleSharp" Version="1.0.4" /> <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="CSharpier.MsBuild" Version="0.25.0" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" Version="6.11.0" /> <PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.2" PrivateAssets="all" /> <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
<PackageReference Include="JsonExtensions" Version="1.2.0" /> <PackageReference Include="JsonExtensions" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.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.EnvironmentVariables" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" 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="ReflectionMagic" Version="5.0.0" />
<PackageReference Include="xunit" Version="2.5.0" /> <PackageReference Include="xunit" Version="2.5.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0" PrivateAssets="all" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.5.1" PrivateAssets="all" />
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -1,4 +1,5 @@
using System.Linq; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AngleSharp.Dom; using AngleSharp.Dom;
using DiscordChatExporter.Cli.Tests.Infra; using DiscordChatExporter.Cli.Tests.Infra;
@@ -27,7 +28,11 @@ public class HtmlAttachmentSpecs
.Select(e => e.GetAttribute("href")) .Select(e => e.GetAttribute("href"))
.Should() .Should()
.Contain( .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")) .Select(e => e.GetAttribute("src"))
.Should() .Should()
.Contain( .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"); var videoUrl = message.QuerySelector("video source")?.GetAttribute("src");
videoUrl videoUrl
.Should() .Should()
.Be( .StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885655761512968233/file_example_MP4_640_3MG.mp4" "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"); var audioUrl = message.QuerySelector("audio source")?.GetAttribute("src");
audioUrl audioUrl
.Should() .Should()
.Be( .StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885656175348187146/file_example_MP3_1MG.mp3" "https://cdn.discordapp.com/attachments/885587741654536192/885656175348187146/file_example_MP3_1MG.mp3"
); );
} }
@@ -145,7 +145,7 @@ public class HtmlEmbedSpecs
// Assert // Assert
var iframeUrl = message.QuerySelector("iframe")?.GetAttribute("src"); 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] [Fact]
@@ -161,7 +161,7 @@ public class HtmlEmbedSpecs
// Assert // Assert
var iframeUrl = message.QuerySelector("iframe")?.GetAttribute("src"); 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] [Fact]
@@ -19,7 +19,7 @@ public class HtmlStickerSpecs
// Assert // Assert
var stickerUrl = message.QuerySelector("[title='rock'] img")?.GetAttribute("src"); 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] [Fact]
@@ -35,6 +35,9 @@ public class HtmlStickerSpecs
var stickerUrl = message var stickerUrl = message
.QuerySelector("[title='Yikes'] [data-source]") .QuerySelector("[title='Yikes'] [data-source]")
?.GetAttribute("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") .GetProperty("url")
.GetString() .GetString()
.Should() .Should()
.Be( .StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885587844964417596/Test.txt" "https://cdn.discordapp.com/attachments/885587741654536192/885587844964417596/Test.txt"
); );
attachments[0].GetProperty("fileName").GetString().Should().Be("Test.txt"); attachments[0].GetProperty("fileName").GetString().Should().Be("Test.txt");
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(11); attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(11);
} }
@@ -54,9 +55,10 @@ public class JsonAttachmentSpecs
.GetProperty("url") .GetProperty("url")
.GetString() .GetString()
.Should() .Should()
.Be( .StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885654862430359613/bird-thumbnail.png" "https://cdn.discordapp.com/attachments/885587741654536192/885654862430359613/bird-thumbnail.png"
); );
attachments[0].GetProperty("fileName").GetString().Should().Be("bird-thumbnail.png"); attachments[0].GetProperty("fileName").GetString().Should().Be("bird-thumbnail.png");
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(466335); attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(466335);
} }
@@ -80,14 +82,16 @@ public class JsonAttachmentSpecs
.GetProperty("url") .GetProperty("url")
.GetString() .GetString()
.Should() .Should()
.Be( .StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885655761512968233/file_example_MP4_640_3MG.mp4" "https://cdn.discordapp.com/attachments/885587741654536192/885655761512968233/file_example_MP4_640_3MG.mp4"
); );
attachments[0] attachments[0]
.GetProperty("fileName") .GetProperty("fileName")
.GetString() .GetString()
.Should() .Should()
.Be("file_example_MP4_640_3MG.mp4"); .Be("file_example_MP4_640_3MG.mp4");
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(3114374); attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(3114374);
} }
@@ -110,9 +114,10 @@ public class JsonAttachmentSpecs
.GetProperty("url") .GetProperty("url")
.GetString() .GetString()
.Should() .Should()
.Be( .StartWith(
"https://cdn.discordapp.com/attachments/885587741654536192/885656175348187146/file_example_MP3_1MG.mp3" "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("fileName").GetString().Should().Be("file_example_MP3_1MG.mp3");
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(1087849); attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(1087849);
} }
@@ -28,7 +28,7 @@ public class JsonStickerSpecs
.GetProperty("sourceUrl") .GetProperty("sourceUrl")
.GetString() .GetString()
.Should() .Should()
.Be("https://cdn.discordapp.com/stickers/904215665597120572.png"); .StartWith("https://cdn.discordapp.com/stickers/904215665597120572.png");
} }
[Fact] [Fact]
@@ -50,6 +50,6 @@ public class JsonStickerSpecs
.GetProperty("sourceUrl") .GetProperty("sourceUrl")
.GetString() .GetString()
.Should() .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 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( var progressTask = progressContext.AddTask(
// Don't recognize random square brackets as style tags // Don't recognize random square brackets as style tags
Markup.Escape(description), Markup.Escape(actualDescription),
new ProgressTaskSettings { MaxValue = 1 } new ProgressTaskSettings { MaxValue = 1 }
); );
@@ -33,7 +33,7 @@ public class DiscordClient
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
return await Http.ResponseResiliencePolicy.ExecuteAsync( return await Http.ResponseResiliencePipeline.ExecuteAsync(
async innerCancellationToken => async innerCancellationToken =>
{ {
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url)); 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="CSharpier.MsBuild" Version="0.25.0" PrivateAssets="all" />
<PackageReference Include="Gress" Version="2.1.1" /> <PackageReference Include="Gress" Version="2.1.1" />
<PackageReference Include="JsonExtensions" Version="1.2.0" /> <PackageReference Include="JsonExtensions" Version="1.2.0" />
<PackageReference Include="Polly" Version="7.2.4" /> <PackageReference Include="Polly" Version="8.0.0" />
<PackageReference Include="RazorBlade" Version="0.4.3" /> <PackageReference Include="RazorBlade" Version="0.4.4" />
<PackageReference Include="Superpower" Version="3.0.0" /> <PackageReference Include="Superpower" Version="3.0.0" />
<PackageReference Include="WebMarkupMin.Core" Version="2.14.0" /> <PackageReference Include="WebMarkupMin.Core" Version="2.14.0" />
<PackageReference Include="YoutubeExplode" Version="6.3.1" /> <PackageReference Include="YoutubeExplode" Version="6.3.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -53,44 +53,47 @@ internal partial class ExportAssetDownloader
Directory.CreateDirectory(_workingDirPath); Directory.CreateDirectory(_workingDirPath);
await Http.ResiliencePolicy.ExecuteAsync(async () => await Http.ResiliencePipeline.ExecuteAsync(
{ async innerCancellationToken =>
// 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
{ {
var lastModified = response.Content.Headers // Download the file
.TryGetValue("Last-Modified") using var response = await Http.Client.GetAsync(url, innerCancellationToken);
?.Pipe( await using (var output = File.Create(filePath))
s => await response.Content.CopyToAsync(output, innerCancellationToken);
DateTimeOffset.TryParse(
s,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var instant
)
? instant
: (DateTimeOffset?)null
);
if (lastModified is not null) // Try to set the file date according to the last-modified header
try
{ {
File.SetCreationTimeUtc(filePath, lastModified.Value.UtcDateTime); var lastModified = response.Content.Headers
File.SetLastWriteTimeUtc(filePath, lastModified.Value.UtcDateTime); .TryGetValue("Last-Modified")
File.SetLastAccessTimeUtc(filePath, lastModified.Value.UtcDateTime); ?.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
catch {
{ // This can apparently fail for some reason.
// This can apparently fail for some reason. // Updating the file date is not a critical task, so we'll just ignore exceptions thrown here.
// Updating the file date is not a critical task, so we'll just ignore exceptions thrown here. // https://github.com/Tyrrrz/DiscordChatExporter/issues/585
// https://github.com/Tyrrrz/DiscordChatExporter/issues/585 }
} },
}); cancellationToken
);
return _previousPathsByUrl[url] = filePath; return _previousPathsByUrl[url] = filePath;
} }
+40 -23
View File
@@ -7,6 +7,7 @@ using System.Security.Authentication;
using System.Threading.Tasks; using System.Threading.Tasks;
using DiscordChatExporter.Core.Utils.Extensions; using DiscordChatExporter.Core.Utils.Extensions;
using Polly; using Polly;
using Polly.Retry;
namespace DiscordChatExporter.Core.Utils; namespace DiscordChatExporter.Core.Utils;
@@ -31,29 +32,45 @@ public static class Http
&& IsRetryableStatusCode(hrex.StatusCode ?? HttpStatusCode.OK) && IsRetryableStatusCode(hrex.StatusCode ?? HttpStatusCode.OK)
); );
public static IAsyncPolicy ResiliencePolicy { get; } = public static ResiliencePipeline ResiliencePipeline { get; } =
Policy new ResiliencePipelineBuilder()
.Handle<Exception>(IsRetryableException) .AddRetry(
.WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1)); new RetryStrategyOptions
public static IAsyncPolicy<HttpResponseMessage> ResponseResiliencePolicy { get; } =
Policy
.Handle<Exception>(IsRetryableException)
.OrResult<HttpResponseMessage>(m => IsRetryableStatusCode(m.StatusCode))
.WaitAndRetryAsync(
8,
(i, result, _) =>
{ {
// If rate-limited, use retry-after header as the guide. ShouldHandle = new PredicateBuilder().Handle<Exception>(IsRetryableException),
// The response can be null here if an exception was thrown. MaxRetryAttempts = 4,
if (result.Result?.Headers.RetryAfter?.Delta is { } retryAfter) BackoffType = DelayBackoffType.Exponential,
{ Delay = TimeSpan.FromSeconds(1)
// Add some buffer just in case }
return retryAfter + TimeSpan.FromSeconds(1); )
} .Build();
return TimeSpan.FromSeconds(Math.Pow(2, i) + 1); public static ResiliencePipeline<HttpResponseMessage> ResponseResiliencePipeline { get; } =
}, new ResiliencePipelineBuilder<HttpResponseMessage>()
(_, _, _, _) => Task.CompletedTask .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();
} }