Add PowerKit and replace custom utility extensions (#1525)

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot
2026-04-19 23:10:45 +03:00
committed by GitHub
parent 757daa7a60
commit 7456f0fe3a
75 changed files with 133 additions and 603 deletions

View File

@@ -1,6 +1,6 @@
using System.Text.Json;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -2,8 +2,8 @@
using System.IO;
using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data.Common;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -2,8 +2,8 @@
using System.Linq;
using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data.Common;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text.Json;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
@@ -47,9 +47,7 @@ public partial record Embed
var kind =
json.GetPropertyOrNull("type")
?.GetStringOrNull()
?.Pipe(s =>
Enum.TryParse<EmbedKind>(s, true, out var result) ? result : (EmbedKind?)null
)
.Pipe(s => Enum.ParseOrNull<EmbedKind>(s, true))
?? EmbedKind.Rich;
var url = json.GetPropertyOrNull("url")?.GetNonWhiteSpaceStringOrNull();
@@ -58,7 +56,7 @@ public partial record Embed
var color = json.GetPropertyOrNull("color")
?.GetInt32OrNull()
?.Pipe(System.Drawing.Color.FromArgb)
.ResetAlpha();
.WithFullAlpha();
var author = json.GetPropertyOrNull("author")?.Pipe(EmbedAuthor.Parse);
var description = json.GetPropertyOrNull("description")?.GetStringOrNull();

View File

@@ -1,8 +1,8 @@
using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data.Common;
using DiscordChatExporter.Core.Utils;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -1,7 +1,7 @@
using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data.Common;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -1,6 +1,6 @@
using System.Text.Json;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -1,7 +1,7 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -2,8 +2,8 @@
using System.Linq;
using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data.Common;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -4,8 +4,8 @@ using System.Linq;
using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data.Common;
using DiscordChatExporter.Core.Discord.Data.Embeds;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -1,6 +1,6 @@
using System.Text.Json;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -1,5 +1,5 @@
using System.Text.Json;
using DiscordChatExporter.Core.Utils.Extensions;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -1,8 +1,8 @@
using System.Drawing;
using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data.Common;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;
@@ -18,7 +18,7 @@ public record Role(Snowflake Id, string Name, int Position, Color? Color) : IHas
var color = json.GetPropertyOrNull("color")
?.GetInt32OrNull()
?.Pipe(System.Drawing.Color.FromArgb)
.ResetAlpha()
.WithFullAlpha()
.NullIf(c => c.ToRgb() <= 0);
return new Role(id, name, position, color);

View File

@@ -1,8 +1,8 @@
using System;
using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data.Common;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -1,7 +1,7 @@
using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data.Common;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data;

View File

@@ -11,10 +11,10 @@ using System.Threading.Tasks;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exceptions;
using DiscordChatExporter.Core.Utils;
using DiscordChatExporter.Core.Utils.Extensions;
using Gress;
using JsonExtensions.Http;
using JsonExtensions.Reading;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord;
@@ -59,12 +59,12 @@ public class DiscordClient(
{
var remainingRequestCount = response
.Headers.TryGetValue("X-RateLimit-Remaining")
?.Pipe(s => int.Parse(s, CultureInfo.InvariantCulture));
?.Pipe(s => int.ParseOrNull(s, CultureInfo.InvariantCulture));
var resetAfterDelay = response
.Headers.TryGetValue("X-RateLimit-Reset-After")
?.Pipe(s => double.Parse(s, CultureInfo.InvariantCulture))
.Pipe(TimeSpan.FromSeconds);
?.Pipe(s => double.ParseOrNull(s, CultureInfo.InvariantCulture))
?.Pipe(TimeSpan.FromSeconds);
// If this was the last request available before hitting the rate limit,
// wait out the reset time so that future requests can succeed.
@@ -161,7 +161,7 @@ public class DiscordClient(
$"""
Request to '{url}' failed: {response
.StatusCode.ToString()
.ToSpaceSeparatedWords()
.SeparateWords(' ')
.ToLowerInvariant()}.
Response content: {await response.Content.ReadAsStringAsync(
cancellationToken

View File

@@ -6,6 +6,7 @@
<PackageReference Include="Gress" />
<PackageReference Include="JsonExtensions" />
<PackageReference Include="Polly" />
<PackageReference Include="PowerKit" PrivateAssets="all" />
<PackageReference Include="RazorBlade" />
<PackageReference Include="Superpower" />
<PackageReference Include="WebMarkupMin.Core" />

View File

@@ -5,7 +5,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Utils.Extensions;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Exporting;

View File

@@ -9,7 +9,7 @@ using System.Threading.Tasks;
using System.Web;
using AsyncKeyedLock;
using DiscordChatExporter.Core.Utils;
using DiscordChatExporter.Core.Utils.Extensions;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Exporting;

View File

@@ -9,7 +9,7 @@ using System.Threading.Tasks;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Utils;
using DiscordChatExporter.Core.Utils.Extensions;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Exporting;

View File

@@ -7,7 +7,7 @@ using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exporting.Filtering;
using DiscordChatExporter.Core.Exporting.Partitioning;
using DiscordChatExporter.Core.Utils.Extensions;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Exporting;

View File

@@ -7,7 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Markdown;
using DiscordChatExporter.Core.Markdown.Parsing;
using DiscordChatExporter.Core.Utils.Extensions;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Exporting;

View File

@@ -9,8 +9,8 @@ using System.Threading.Tasks;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Discord.Data.Embeds;
using DiscordChatExporter.Core.Markdown.Parsing;
using DiscordChatExporter.Core.Utils.Extensions;
using JsonExtensions.Writing;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Exporting;
@@ -55,7 +55,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
Context.TryGetMember(user.Id)?.DisplayName ?? user.DisplayName
);
_writer.WriteString("color", Context.TryGetUserColor(user.Id)?.ToHex());
_writer.WriteString("color", Context.TryGetUserColor(user.Id)?.ToHexString());
_writer.WriteBoolean("isBot", user.IsBot);
if (includeRoles)
@@ -109,7 +109,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
_writer.WriteString("id", role.Id.ToString());
_writer.WriteString("name", role.Name);
_writer.WriteString("color", role.Color?.ToHex());
_writer.WriteString("color", role.Color?.ToHexString());
_writer.WriteNumber("position", role.Position);
_writer.WriteEndObject();
@@ -281,7 +281,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
);
if (embed.Color is not null)
_writer.WriteString("color", embed.Color.Value.ToHex());
_writer.WriteString("color", embed.Color.Value.ToHexString());
if (embed.Author is not null)
{

View File

@@ -6,6 +6,7 @@
@using DiscordChatExporter.Core.Discord.Data.Embeds
@using DiscordChatExporter.Core.Markdown.Parsing
@using DiscordChatExporter.Core.Utils.Extensions
@using PowerKit.Extensions
@inherits RazorBlade.HtmlTemplate

View File

@@ -3,7 +3,7 @@ using System.Threading;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Markdown;
using DiscordChatExporter.Core.Markdown.Parsing;
using DiscordChatExporter.Core.Utils.Extensions;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Exporting;

View File

@@ -1,7 +1,7 @@
using System.Globalization;
using System.Linq;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Utils.Extensions;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Exporting;

View File

@@ -5,7 +5,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Utils;
using DiscordChatExporter.Core.Utils.Extensions;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Markdown.Parsing;

View File

@@ -1,24 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class AsyncCollectionExtensions
{
extension<T>(IAsyncEnumerable<T> asyncEnumerable)
{
private async ValueTask<IReadOnlyList<T>> CollectAsync()
{
var list = new List<T>();
await foreach (var i in asyncEnumerable)
list.Add(i);
return list;
}
public ValueTaskAwaiter<IReadOnlyList<T>> GetAwaiter() =>
asyncEnumerable.CollectAsync().GetAwaiter();
}
}

View File

@@ -1,40 +0,0 @@
using System.Collections.Generic;
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class CollectionExtensions
{
extension<T>(T obj)
{
public IEnumerable<T> ToSingletonEnumerable()
{
yield return obj;
}
}
extension<T>(IEnumerable<T?> source)
where T : class
{
public IEnumerable<T> WhereNotNull()
{
foreach (var o in source)
{
if (o is not null)
yield return o;
}
}
}
extension<T>(IEnumerable<T?> source)
where T : struct
{
public IEnumerable<T> WhereNotNull()
{
foreach (var o in source)
{
if (o is not null)
yield return o.Value;
}
}
}
}

View File

@@ -1,17 +0,0 @@
using System.Drawing;
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class ColorExtensions
{
extension(Color color)
{
public Color WithAlpha(int alpha) => Color.FromArgb(alpha, color);
public Color ResetAlpha() => color.WithAlpha(255);
public int ToRgb() => color.ToArgb() & 0xffffff;
public string ToHex() => $"#{color.R:X2}{color.G:X2}{color.B:X2}";
}
}

View File

@@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class ExceptionExtensions
{
extension(Exception exception)
{
private void PopulateChildren(ICollection<Exception> children)
{
if (exception is AggregateException aggregateException)
{
foreach (var innerException in aggregateException.InnerExceptions)
{
children.Add(innerException);
PopulateChildren(innerException, children);
}
}
else if (exception.InnerException is not null)
{
children.Add(exception.InnerException);
PopulateChildren(exception.InnerException, children);
}
}
public IReadOnlyList<Exception> GetSelfAndChildren()
{
var children = new List<Exception> { exception };
PopulateChildren(exception, children);
return children;
}
}
}

View File

@@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class GenericExtensions
{
extension<TIn>(TIn input)
{
public TOut Pipe<TOut>(Func<TIn, TOut> transform) => transform(input);
}
extension<T>(T value)
where T : struct
{
public T? NullIf(Func<T, bool> predicate) => !predicate(value) ? value : null;
public T? NullIfDefault() =>
value.NullIf(v => EqualityComparer<T>.Default.Equals(v, default));
}
}

View File

@@ -1,12 +0,0 @@
using System.Net.Http.Headers;
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class HttpExtensions
{
extension(HttpHeaders headers)
{
public string? TryGetValue(string name) =>
headers.TryGetValues(name, out var values) ? string.Concat(values) : null;
}
}

View File

@@ -1,48 +0,0 @@
using System;
using System.IO;
using System.Text;
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class PathExtensions
{
// This is a union of invalid characters from Windows (NTFS/FAT32), Linux (ext4/XFS), and macOS (HFS+/APFS).
// We use this instead of Path.GetInvalidFileNameChars() because that only returns OS-specific characters,
// not filesystem-specific characters. It's possible to use, for example, an NTFS drive on Linux,
// which would make some additional characters invalid that are otherwise valid on Linux.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1452
private static readonly char[] InvalidFileNameChars =
[
'\0', // Null character - invalid on all filesystems
'/', // Path separator on Unix and Windows
'\\', // Path separator on Windows
':', // Reserved on Windows (drive letters, NTFS streams)
'*', // Wildcard on Windows
'?', // Wildcard on Windows
'"', // Reserved on Windows
'<', // Redirection on Windows
'>', // Redirection on Windows
'|', // Pipe on Windows
];
extension(Path)
{
public static string EscapeFileName(string path)
{
var buffer = new StringBuilder(path.Length);
foreach (var c in path)
buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_');
// File names cannot end with a dot on Windows
// https://github.com/Tyrrrz/DiscordChatExporter/issues/977
if (OperatingSystem.IsWindows())
{
while (buffer.Length > 0 && buffer[^1] == '.')
buffer.Remove(buffer.Length - 1, 1);
}
return buffer.ToString();
}
}
}

View File

@@ -1,34 +0,0 @@
using System.Text;
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class StringExtensions
{
extension(string str)
{
public string? NullIfWhiteSpace() => !string.IsNullOrWhiteSpace(str) ? str : null;
public string Truncate(int charCount) => str.Length > charCount ? str[..charCount] : str;
public string ToSpaceSeparatedWords()
{
var builder = new StringBuilder(str.Length * 2);
foreach (var c in str)
{
if (char.IsUpper(c) && builder.Length > 0)
builder.Append(' ');
builder.Append(c);
}
return builder.ToString();
}
}
extension(StringBuilder builder)
{
public StringBuilder AppendIfNotEmpty(char value) =>
builder.Length > 0 ? builder.Append(value) : builder;
}
}

View File

@@ -1,20 +0,0 @@
using System;
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class TimeSpanExtensions
{
extension(TimeSpan value)
{
public TimeSpan Clamp(TimeSpan min, TimeSpan max)
{
if (value < min)
return min;
if (value > max)
return max;
return value;
}
}
}

View File

@@ -5,9 +5,9 @@ using System.Net.Http;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Utils.Extensions;
using Polly;
using Polly.Retry;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Utils;
@@ -24,7 +24,7 @@ public static class Http
private static bool IsRetryableException(Exception exception) =>
exception
.GetSelfAndChildren()
.GetSelfAndDescendants()
.Any(ex =>
ex is TimeoutException or SocketException or AuthenticationException
|| ex is HttpRequestException hrex

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DiscordChatExporter.Core.Utils.Extensions;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Utils;