This commit is contained in:
Tyrrrz
2021-12-08 23:50:21 +02:00
parent 8e7baee8a5
commit 880f400e2c
148 changed files with 14241 additions and 14396 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -5,48 +5,47 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace DiscordChatExporter.Core.Utils.Extensions
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class AsyncExtensions
{
public static class AsyncExtensions
private static async ValueTask<IReadOnlyList<T>> AggregateAsync<T>(
this IAsyncEnumerable<T> asyncEnumerable)
{
private static async ValueTask<IReadOnlyList<T>> AggregateAsync<T>(
this IAsyncEnumerable<T> asyncEnumerable)
var list = new List<T>();
await foreach (var i in asyncEnumerable)
list.Add(i);
return list;
}
public static ValueTaskAwaiter<IReadOnlyList<T>> GetAwaiter<T>(
this IAsyncEnumerable<T> asyncEnumerable) =>
asyncEnumerable.AggregateAsync().GetAwaiter();
public static async ValueTask ParallelForEachAsync<T>(
this IEnumerable<T> source,
Func<T, ValueTask> handleAsync,
int degreeOfParallelism,
CancellationToken cancellationToken = default)
{
using var semaphore = new SemaphoreSlim(degreeOfParallelism);
await Task.WhenAll(source.Select(async item =>
{
var list = new List<T>();
// ReSharper disable once AccessToDisposedClosure
await semaphore.WaitAsync(cancellationToken);
await foreach (var i in asyncEnumerable)
list.Add(i);
return list;
}
public static ValueTaskAwaiter<IReadOnlyList<T>> GetAwaiter<T>(
this IAsyncEnumerable<T> asyncEnumerable) =>
asyncEnumerable.AggregateAsync().GetAwaiter();
public static async ValueTask ParallelForEachAsync<T>(
this IEnumerable<T> source,
Func<T, ValueTask> handleAsync,
int degreeOfParallelism,
CancellationToken cancellationToken = default)
{
using var semaphore = new SemaphoreSlim(degreeOfParallelism);
await Task.WhenAll(source.Select(async item =>
try
{
await handleAsync(item);
}
finally
{
// ReSharper disable once AccessToDisposedClosure
await semaphore.WaitAsync(cancellationToken);
try
{
await handleAsync(item);
}
finally
{
// ReSharper disable once AccessToDisposedClosure
semaphore.Release();
}
}));
}
semaphore.Release();
}
}));
}
}

View File

@@ -1,19 +1,18 @@
using System.Text;
namespace DiscordChatExporter.Core.Utils.Extensions
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class BinaryExtensions
{
public static class BinaryExtensions
public static string ToHex(this byte[] data)
{
public static string ToHex(this byte[] data)
var buffer = new StringBuilder();
foreach (var t in data)
{
var buffer = new StringBuilder();
foreach (var t in data)
{
buffer.Append(t.ToString("X2"));
}
return buffer.ToString();
buffer.Append(t.ToString("X2"));
}
return buffer.ToString();
}
}

View File

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

View File

@@ -1,11 +1,10 @@
using System;
using System.Globalization;
namespace DiscordChatExporter.Core.Utils.Extensions
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class DateExtensions
{
public static class DateExtensions
{
public static string ToLocalString(this DateTimeOffset dateTime, string format) =>
dateTime.ToLocalTime().ToString(format, CultureInfo.InvariantCulture);
}
public static string ToLocalString(this DateTimeOffset dateTime, string format) =>
dateTime.ToLocalTime().ToString(format, CultureInfo.InvariantCulture);
}

View File

@@ -1,14 +1,13 @@
using System;
namespace DiscordChatExporter.Core.Utils.Extensions
{
public static class GenericExtensions
{
public static TOut Pipe<TIn, TOut>(this TIn input, Func<TIn, TOut> transform) => transform(input);
namespace DiscordChatExporter.Core.Utils.Extensions;
public static T? NullIf<T>(this T value, Func<T, bool> predicate) where T : struct =>
!predicate(value)
? value
: null;
}
public static class GenericExtensions
{
public static TOut Pipe<TIn, TOut>(this TIn input, Func<TIn, TOut> transform) => transform(input);
public static T? NullIf<T>(this T value, Func<T, bool> predicate) where T : struct =>
!predicate(value)
? value
: null;
}

View File

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

View File

@@ -1,33 +1,32 @@
using System.Collections.Generic;
using System.Text;
namespace DiscordChatExporter.Core.Utils.Extensions
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class StringExtensions
{
public static class StringExtensions
public static string? NullIfWhiteSpace(this string str) =>
!string.IsNullOrWhiteSpace(str)
? str
: null;
public static string Truncate(this string str, int charCount) =>
str.Length > charCount
? str[..charCount]
: str;
public static IEnumerable<Rune> GetRunes(this string str)
{
public static string? NullIfWhiteSpace(this string str) =>
!string.IsNullOrWhiteSpace(str)
? str
: null;
public static string Truncate(this string str, int charCount) =>
str.Length > charCount
? str[..charCount]
: str;
public static IEnumerable<Rune> GetRunes(this string str)
var lastIndex = 0;
while (lastIndex < str.Length && Rune.TryGetRuneAt(str, lastIndex, out var rune))
{
var lastIndex = 0;
while (lastIndex < str.Length && Rune.TryGetRuneAt(str, lastIndex, out var rune))
{
yield return rune;
lastIndex += rune.Utf16SequenceLength;
}
yield return rune;
lastIndex += rune.Utf16SequenceLength;
}
public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) =>
builder.Length > 0
? builder.Append(value)
: builder;
}
public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) =>
builder.Length > 0
? builder.Append(value)
: builder;
}

View File

@@ -3,25 +3,24 @@ using System.Diagnostics.CodeAnalysis;
using Superpower;
using Superpower.Parsers;
namespace DiscordChatExporter.Core.Utils.Extensions
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class SuperpowerExtensions
{
public static class SuperpowerExtensions
public static TextParser<string> Text(this TextParser<char[]> parser) =>
parser.Select(chars => new string(chars));
public static TextParser<T> Token<T>(this TextParser<T> parser) =>
parser.Between(Character.WhiteSpace.IgnoreMany(), Character.WhiteSpace.IgnoreMany());
// Only used for debugging while writing Superpower parsers.
// From https://twitter.com/nblumhardt/status/1389349059786264578
[ExcludeFromCodeCoverage]
public static TextParser<T> Log<T>(this TextParser<T> parser, string description) => i =>
{
public static TextParser<string> Text(this TextParser<char[]> parser) =>
parser.Select(chars => new string(chars));
public static TextParser<T> Token<T>(this TextParser<T> parser) =>
parser.Between(Character.WhiteSpace.IgnoreMany(), Character.WhiteSpace.IgnoreMany());
// Only used for debugging while writing Superpower parsers.
// From https://twitter.com/nblumhardt/status/1389349059786264578
[ExcludeFromCodeCoverage]
public static TextParser<T> Log<T>(this TextParser<T> parser, string description) => i =>
{
Console.WriteLine($"Trying {description} ->");
var r = parser(i);
Console.WriteLine($"Result was {r}");
return r;
};
}
Console.WriteLine($"Trying {description} ->");
var r = parser(i);
Console.WriteLine($"Result was {r}");
return r;
};
}

View File

@@ -1,41 +1,40 @@
using System;
using System.Collections.Generic;
namespace DiscordChatExporter.Core.Utils
namespace DiscordChatExporter.Core.Utils;
public static class FileFormat
{
public static class FileFormat
private static readonly HashSet<string> ImageFormats = new(StringComparer.OrdinalIgnoreCase)
{
private static readonly HashSet<string> ImageFormats = new(StringComparer.OrdinalIgnoreCase)
{
".jpg",
".jpeg",
".png",
".gif",
".gifv",
".bmp",
".webp"
};
".jpg",
".jpeg",
".png",
".gif",
".gifv",
".bmp",
".webp"
};
public static bool IsImage(string format) => ImageFormats.Contains(format);
public static bool IsImage(string format) => ImageFormats.Contains(format);
private static readonly HashSet<string> VideoFormats = new(StringComparer.OrdinalIgnoreCase)
{
".mp4",
".webm",
".mov"
};
private static readonly HashSet<string> VideoFormats = new(StringComparer.OrdinalIgnoreCase)
{
".mp4",
".webm",
".mov"
};
public static bool IsVideo(string format) => VideoFormats.Contains(format);
public static bool IsVideo(string format) => VideoFormats.Contains(format);
private static readonly HashSet<string> AudioFormats = new(StringComparer.OrdinalIgnoreCase)
{
".mp3",
".wav",
".ogg",
".flac",
".m4a"
};
private static readonly HashSet<string> AudioFormats = new(StringComparer.OrdinalIgnoreCase)
{
".mp3",
".wav",
".ogg",
".flac",
".m4a"
};
public static bool IsAudio(string format) => AudioFormats.Contains(format);
}
public static bool IsAudio(string format) => AudioFormats.Contains(format);
}

View File

@@ -8,60 +8,59 @@ using System.Threading.Tasks;
using DiscordChatExporter.Core.Utils.Extensions;
using Polly;
namespace DiscordChatExporter.Core.Utils
namespace DiscordChatExporter.Core.Utils;
public static class Http
{
public static class Http
{
public static HttpClient Client { get; } = new();
public static HttpClient Client { get; } = new();
public static IAsyncPolicy<HttpResponseMessage> ResponsePolicy { get; } =
Policy
.Handle<IOException>()
.Or<HttpRequestException>()
.OrResult<HttpResponseMessage>(m => m.StatusCode == HttpStatusCode.TooManyRequests)
.OrResult(m => m.StatusCode == HttpStatusCode.RequestTimeout)
.OrResult(m => m.StatusCode >= HttpStatusCode.InternalServerError)
.WaitAndRetryAsync(
8,
(i, result, _) =>
public static IAsyncPolicy<HttpResponseMessage> ResponsePolicy { get; } =
Policy
.Handle<IOException>()
.Or<HttpRequestException>()
.OrResult<HttpResponseMessage>(m => m.StatusCode == HttpStatusCode.TooManyRequests)
.OrResult(m => m.StatusCode == HttpStatusCode.RequestTimeout)
.OrResult(m => m.StatusCode >= HttpStatusCode.InternalServerError)
.WaitAndRetryAsync(
8,
(i, result, _) =>
{
// If rate-limited, use retry-after as a guide
if (result.Result?.StatusCode == HttpStatusCode.TooManyRequests)
{
// If rate-limited, use retry-after as a guide
if (result.Result?.StatusCode == HttpStatusCode.TooManyRequests)
// Only start respecting retry-after after a few attempts, because
// Discord often sends unreasonable (20+ minutes) retry-after
// on the very first request.
if (i > 3)
{
// Only start respecting retry-after after a few attempts, because
// Discord often sends unreasonable (20+ minutes) retry-after
// on the very first request.
if (i > 3)
{
var retryAfterDelay = result.Result.Headers.RetryAfter?.Delta;
if (retryAfterDelay is not null)
return retryAfterDelay.Value + TimeSpan.FromSeconds(1); // margin just in case
}
var retryAfterDelay = result.Result.Headers.RetryAfter?.Delta;
if (retryAfterDelay is not null)
return retryAfterDelay.Value + TimeSpan.FromSeconds(1); // margin just in case
}
}
return TimeSpan.FromSeconds(Math.Pow(2, i) + 1);
},
(_, _, _, _) => Task.CompletedTask
);
return TimeSpan.FromSeconds(Math.Pow(2, i) + 1);
},
(_, _, _, _) => Task.CompletedTask
);
private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex) =>
// This is extremely frail, but there's no other way
Regex
.Match(ex.Message, @": (\d+) \(")
.Groups[1]
.Value
.NullIfWhiteSpace()?
.Pipe(s => (HttpStatusCode) int.Parse(s, CultureInfo.InvariantCulture));
private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex) =>
// This is extremely frail, but there's no other way
Regex
.Match(ex.Message, @": (\d+) \(")
.Groups[1]
.Value
.NullIfWhiteSpace()?
.Pipe(s => (HttpStatusCode) int.Parse(s, CultureInfo.InvariantCulture));
public static IAsyncPolicy ExceptionPolicy { get; } =
Policy
.Handle<IOException>() // dangerous
.Or<HttpRequestException>(ex =>
TryGetStatusCodeFromException(ex) is
HttpStatusCode.TooManyRequests or
HttpStatusCode.RequestTimeout or
HttpStatusCode.InternalServerError
)
.WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1));
}
public static IAsyncPolicy ExceptionPolicy { get; } =
Policy
.Handle<IOException>() // dangerous
.Or<HttpRequestException>(ex =>
TryGetStatusCodeFromException(ex) is
HttpStatusCode.TooManyRequests or
HttpStatusCode.RequestTimeout or
HttpStatusCode.InternalServerError
)
.WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1));
}

View File

@@ -1,18 +1,17 @@
using System.IO;
using System.Text;
namespace DiscordChatExporter.Core.Utils
namespace DiscordChatExporter.Core.Utils;
public static class PathEx
{
public static class PathEx
public static StringBuilder EscapePath(StringBuilder pathBuffer)
{
public static StringBuilder EscapePath(StringBuilder pathBuffer)
{
foreach (var invalidChar in Path.GetInvalidFileNameChars())
pathBuffer.Replace(invalidChar, '_');
foreach (var invalidChar in Path.GetInvalidFileNameChars())
pathBuffer.Replace(invalidChar, '_');
return pathBuffer;
}
public static string EscapePath(string path) => EscapePath(new StringBuilder(path)).ToString();
return pathBuffer;
}
public static string EscapePath(string path) => EscapePath(new StringBuilder(path)).ToString();
}

View File

@@ -4,42 +4,41 @@ using System.Linq;
using System.Net;
using System.Text;
namespace DiscordChatExporter.Core.Utils
namespace DiscordChatExporter.Core.Utils;
public class UrlBuilder
{
public class UrlBuilder
private string _path = "";
private readonly Dictionary<string, string?> _queryParameters = new(StringComparer.OrdinalIgnoreCase);
public UrlBuilder SetPath(string path)
{
private string _path = "";
_path = path;
return this;
}
private readonly Dictionary<string, string?> _queryParameters = new(StringComparer.OrdinalIgnoreCase);
public UrlBuilder SetPath(string path)
{
_path = path;
public UrlBuilder SetQueryParameter(string key, string? value, bool ignoreUnsetValue = true)
{
if (ignoreUnsetValue && string.IsNullOrWhiteSpace(value))
return this;
}
public UrlBuilder SetQueryParameter(string key, string? value, bool ignoreUnsetValue = true)
{
if (ignoreUnsetValue && string.IsNullOrWhiteSpace(value))
return this;
var keyEncoded = WebUtility.UrlEncode(key);
var valueEncoded = WebUtility.UrlEncode(value);
_queryParameters[keyEncoded] = valueEncoded;
var keyEncoded = WebUtility.UrlEncode(key);
var valueEncoded = WebUtility.UrlEncode(value);
_queryParameters[keyEncoded] = valueEncoded;
return this;
}
return this;
}
public string Build()
{
var buffer = new StringBuilder();
public string Build()
{
var buffer = new StringBuilder();
buffer.Append(_path);
buffer.Append(_path);
if (_queryParameters.Any())
buffer.Append('?').AppendJoin('&', _queryParameters.Select(kvp => $"{kvp.Key}={kvp.Value}"));
if (_queryParameters.Any())
buffer.Append('?').AppendJoin('&', _queryParameters.Select(kvp => $"{kvp.Key}={kvp.Value}"));
return buffer.ToString();
}
return buffer.ToString();
}
}