mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-07 14:44:39 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9180d51e5e | |||
| a58509fda8 | |||
| fbfff4e51f | |||
| c466c17793 | |||
| 1b2bcf771d | |||
| 99b49f0b8f | |||
| 850414b017 | |||
| 92bf6eab2d |
@@ -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
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user