diff --git a/DiscordChatExporter.Core/Discord/AuthToken.cs b/DiscordChatExporter.Core/Discord/AuthToken.cs index 6f7702a..f8e8bd2 100644 --- a/DiscordChatExporter.Core/Discord/AuthToken.cs +++ b/DiscordChatExporter.Core/Discord/AuthToken.cs @@ -2,8 +2,6 @@ namespace DiscordChatExporter.Core.Discord { - public enum AuthTokenType { User, Bot } - public class AuthToken { public AuthTokenType Type { get; } diff --git a/DiscordChatExporter.Core/Discord/AuthTokenType.cs b/DiscordChatExporter.Core/Discord/AuthTokenType.cs new file mode 100644 index 0000000..b52ff6f --- /dev/null +++ b/DiscordChatExporter.Core/Discord/AuthTokenType.cs @@ -0,0 +1,8 @@ +namespace DiscordChatExporter.Core.Discord +{ + public enum AuthTokenType + { + User, + Bot + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Data/Channel.cs b/DiscordChatExporter.Core/Discord/Data/Channel.cs index a252f4a..d0cd592 100644 --- a/DiscordChatExporter.Core/Discord/Data/Channel.cs +++ b/DiscordChatExporter.Core/Discord/Data/Channel.cs @@ -7,32 +7,19 @@ using Tyrrrz.Extensions; namespace DiscordChatExporter.Core.Discord.Data { - // https://discord.com/developers/docs/resources/channel#channel-object-channel-types - // Order of enum fields needs to match the order in the docs. - public enum ChannelType - { - GuildTextChat, - DirectTextChat, - GuildVoiceChat, - DirectGroupTextChat, - GuildCategory, - GuildNews, - GuildStore - } - // https://discord.com/developers/docs/resources/channel#channel-object - public partial class Channel : IHasId, IHasPosition + public partial class Channel : IHasId { public Snowflake Id { get; } public ChannelType Type { get; } - public bool IsTextChannel => - Type == ChannelType.GuildTextChat || - Type == ChannelType.DirectTextChat || - Type == ChannelType.DirectGroupTextChat || - Type == ChannelType.GuildNews || - Type == ChannelType.GuildStore; + public bool IsTextChannel => Type is + ChannelType.GuildTextChat or + ChannelType.DirectTextChat or + ChannelType.DirectGroupTextChat or + ChannelType.GuildNews or + ChannelType.GuildStore; public Snowflake GuildId { get; } @@ -48,7 +35,7 @@ namespace DiscordChatExporter.Core.Discord.Data Snowflake id, ChannelType type, Snowflake guildId, - ChannelCategory? category, + ChannelCategory category, string name, int? position, string? topic) @@ -56,14 +43,13 @@ namespace DiscordChatExporter.Core.Discord.Data Id = id; Type = type; GuildId = guildId; - Category = category ?? GetFallbackCategory(type); + Category = category; Name = name; Position = position; Topic = topic; } public override string ToString() => Name; - } public partial class Channel @@ -79,7 +65,7 @@ namespace DiscordChatExporter.Core.Discord.Data ChannelType.GuildStore => "Store", _ => "Default" }, - 0 + null ); public static Channel Parse(JsonElement json, ChannelCategory? category = null, int? position = null) @@ -87,23 +73,23 @@ namespace DiscordChatExporter.Core.Discord.Data var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse); var guildId = json.GetPropertyOrNull("guild_id")?.GetString().Pipe(Snowflake.Parse); var topic = json.GetPropertyOrNull("topic")?.GetString(); - var type = (ChannelType) json.GetProperty("type").GetInt32(); var name = + // Guild channel json.GetPropertyOrNull("name")?.GetString() ?? + // DM channel json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(User.Parse).Select(u => u.Name).JoinToString(", ") ?? + // Fallback id.ToString(); - position ??= json.GetPropertyOrNull("position")?.GetInt32(); - return new Channel( id, type, guildId ?? Guild.DirectMessages.Id, category ?? GetFallbackCategory(type), name, - position, + position ?? json.GetPropertyOrNull("position")?.GetInt32(), topic ); } diff --git a/DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs b/DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs index dc8180c..a418403 100644 --- a/DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs +++ b/DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs @@ -1,13 +1,11 @@ -using System.Linq; -using System.Text.Json; +using System.Text.Json; using DiscordChatExporter.Core.Discord.Data.Common; using DiscordChatExporter.Core.Utils.Extensions; using JsonExtensions.Reading; -using Tyrrrz.Extensions; namespace DiscordChatExporter.Core.Discord.Data { - public partial class ChannelCategory : IHasId, IHasPosition + public partial class ChannelCategory : IHasId { public Snowflake Id { get; } @@ -23,7 +21,6 @@ namespace DiscordChatExporter.Core.Discord.Data } public override string ToString() => Name; - } public partial class ChannelCategory @@ -31,19 +28,18 @@ namespace DiscordChatExporter.Core.Discord.Data public static ChannelCategory Parse(JsonElement json, int? position = null) { var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse); - position ??= json.GetPropertyOrNull("position")?.GetInt32(); - var name = json.GetPropertyOrNull("name")?.GetString() ?? - json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(User.Parse).Select(u => u.Name).JoinToString(", ") ?? + var name = + json.GetPropertyOrNull("name")?.GetString() ?? id.ToString(); return new ChannelCategory( id, name, - position + position ?? json.GetPropertyOrNull("position")?.GetInt32() ); } public static ChannelCategory Empty { get; } = new(Snowflake.Zero, "", 0); } -} +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Data/ChannelType.cs b/DiscordChatExporter.Core/Discord/Data/ChannelType.cs new file mode 100644 index 0000000..18ac9ff --- /dev/null +++ b/DiscordChatExporter.Core/Discord/Data/ChannelType.cs @@ -0,0 +1,15 @@ +namespace DiscordChatExporter.Core.Discord.Data +{ + // https://discord.com/developers/docs/resources/channel#channel-object-channel-types + // Order of enum fields needs to match the order in the docs. + public enum ChannelType + { + GuildTextChat = 0, + DirectTextChat, + GuildVoiceChat, + DirectGroupTextChat, + GuildCategory, + GuildNews, + GuildStore + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Data/Common/IHasPosition.cs b/DiscordChatExporter.Core/Discord/Data/Common/IHasPosition.cs deleted file mode 100644 index 7c6c77a..0000000 --- a/DiscordChatExporter.Core/Discord/Data/Common/IHasPosition.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DiscordChatExporter.Core.Discord.Data.Common -{ - public interface IHasPosition - { - int? Position { get; } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Data/Embed.cs b/DiscordChatExporter.Core/Discord/Data/Embed.cs index 1f6c4a6..b1c53b5 100644 --- a/DiscordChatExporter.Core/Discord/Data/Embed.cs +++ b/DiscordChatExporter.Core/Discord/Data/Embed.cs @@ -63,10 +63,10 @@ namespace DiscordChatExporter.Core.Discord.Data public static Embed Parse(JsonElement json) { var title = json.GetPropertyOrNull("title")?.GetString(); - var description = json.GetPropertyOrNull("description")?.GetString(); var url = json.GetPropertyOrNull("url")?.GetString(); var timestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset(); var color = json.GetPropertyOrNull("color")?.GetInt32().Pipe(System.Drawing.Color.FromArgb).ResetAlpha(); + var description = json.GetPropertyOrNull("description")?.GetString(); var author = json.GetPropertyOrNull("author")?.Pipe(EmbedAuthor.Parse); var thumbnail = json.GetPropertyOrNull("thumbnail")?.Pipe(EmbedImage.Parse); diff --git a/DiscordChatExporter.Core/Discord/Data/Emoji.cs b/DiscordChatExporter.Core/Discord/Data/Emoji.cs index a1ec9b4..b66b38c 100644 --- a/DiscordChatExporter.Core/Discord/Data/Emoji.cs +++ b/DiscordChatExporter.Core/Discord/Data/Emoji.cs @@ -18,13 +18,12 @@ namespace DiscordChatExporter.Core.Discord.Data public string ImageUrl { get; } - public Emoji(string? id, string name, bool isAnimated) + public Emoji(string? id, string name, bool isAnimated, string imageUrl) { Id = id; Name = name; IsAnimated = isAnimated; - - ImageUrl = GetImageUrl(id, name, isAnimated); + ImageUrl = imageUrl; } public override string ToString() => Name; @@ -53,12 +52,9 @@ namespace DiscordChatExporter.Core.Discord.Data // Custom emoji if (!string.IsNullOrWhiteSpace(id)) { - // Animated - if (isAnimated) - return $"https://cdn.discordapp.com/emojis/{id}.gif"; - - // Non-animated - return $"https://cdn.discordapp.com/emojis/{id}.png"; + return isAnimated + ? $"https://cdn.discordapp.com/emojis/{id}.gif" + : $"https://cdn.discordapp.com/emojis/{id}.png"; } // Standard emoji @@ -73,7 +69,9 @@ namespace DiscordChatExporter.Core.Discord.Data var name = json.GetProperty("name").GetString(); var isAnimated = json.GetPropertyOrNull("animated")?.GetBoolean() ?? false; - return new Emoji(id, name, isAnimated); + var imageUrl = GetImageUrl(id, name, isAnimated); + + return new Emoji(id, name, isAnimated, imageUrl); } } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Data/Member.cs b/DiscordChatExporter.Core/Discord/Data/Member.cs index 310a3fc..eea66a8 100644 --- a/DiscordChatExporter.Core/Discord/Data/Member.cs +++ b/DiscordChatExporter.Core/Discord/Data/Member.cs @@ -19,10 +19,10 @@ namespace DiscordChatExporter.Core.Discord.Data public IReadOnlyList RoleIds { get; } - public Member(User user, string? nick, IReadOnlyList roleIds) + public Member(User user, string nick, IReadOnlyList roleIds) { User = user; - Nick = nick ?? user.Name; + Nick = nick; RoleIds = roleIds; } @@ -33,7 +33,7 @@ namespace DiscordChatExporter.Core.Discord.Data { public static Member CreateForUser(User user) => new( user, - null, + user.Name, Array.Empty() ); @@ -43,12 +43,12 @@ namespace DiscordChatExporter.Core.Discord.Data var nick = json.GetPropertyOrNull("nick")?.GetString(); var roleIds = - json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString().Pipe(Snowflake.Parse)).ToArray() ?? + json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString()).Select(Snowflake.Parse).ToArray() ?? Array.Empty(); return new Member( user, - nick, + nick ?? user.Name, roleIds ); } diff --git a/DiscordChatExporter.Core/Discord/Data/Message.cs b/DiscordChatExporter.Core/Discord/Data/Message.cs index 6b774b2..fcff27a 100644 --- a/DiscordChatExporter.Core/Discord/Data/Message.cs +++ b/DiscordChatExporter.Core/Discord/Data/Message.cs @@ -100,7 +100,7 @@ namespace DiscordChatExporter.Core.Discord.Data var type = (MessageType) json.GetProperty("type").GetInt32(); var isPinned = json.GetPropertyOrNull("pinned")?.GetBoolean() ?? false; var messageReference = json.GetPropertyOrNull("message_reference")?.Pipe(MessageReference.Parse); - var referencedMessage = json.GetPropertyOrNull("referenced_message")?.Pipe(Message.Parse); + var referencedMessage = json.GetPropertyOrNull("referenced_message")?.Pipe(Parse); var content = type switch { diff --git a/DiscordChatExporter.Core/Discord/Data/Reaction.cs b/DiscordChatExporter.Core/Discord/Data/Reaction.cs index 3696c12..15c9f44 100644 --- a/DiscordChatExporter.Core/Discord/Data/Reaction.cs +++ b/DiscordChatExporter.Core/Discord/Data/Reaction.cs @@ -23,8 +23,8 @@ namespace DiscordChatExporter.Core.Discord.Data { public static Reaction Parse(JsonElement json) { - var count = json.GetProperty("count").GetInt32(); var emoji = json.GetProperty("emoji").Pipe(Emoji.Parse); + var count = json.GetProperty("count").GetInt32(); return new Reaction(emoji, count); } diff --git a/DiscordChatExporter.Core/Discord/Data/Role.cs b/DiscordChatExporter.Core/Discord/Data/Role.cs index d7578f1..773a0f7 100644 --- a/DiscordChatExporter.Core/Discord/Data/Role.cs +++ b/DiscordChatExporter.Core/Discord/Data/Role.cs @@ -40,7 +40,8 @@ namespace DiscordChatExporter.Core.Discord.Data var name = json.GetProperty("name").GetString(); var position = json.GetProperty("position").GetInt32(); - var color = json.GetPropertyOrNull("color")? + var color = json + .GetPropertyOrNull("color")? .GetInt32() .Pipe(System.Drawing.Color.FromArgb) .ResetAlpha() diff --git a/DiscordChatExporter.Core/Discord/Data/User.cs b/DiscordChatExporter.Core/Discord/Data/User.cs index 2772037..5ff6b16 100644 --- a/DiscordChatExporter.Core/Discord/Data/User.cs +++ b/DiscordChatExporter.Core/Discord/Data/User.cs @@ -58,10 +58,10 @@ namespace DiscordChatExporter.Core.Discord.Data public static User Parse(JsonElement json) { var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse); + var isBot = json.GetPropertyOrNull("bot")?.GetBoolean() ?? false; var discriminator = json.GetProperty("discriminator").GetString().Pipe(int.Parse); var name = json.GetProperty("username").GetString(); var avatarHash = json.GetProperty("avatar").GetString(); - var isBot = json.GetPropertyOrNull("bot")?.GetBoolean() ?? false; var avatarUrl = !string.IsNullOrWhiteSpace(avatarHash) ? GetAvatarUrl(id, avatarHash) diff --git a/DiscordChatExporter.Core/Discord/DiscordClient.cs b/DiscordChatExporter.Core/Discord/DiscordClient.cs index 8bd8345..e67bce5 100644 --- a/DiscordChatExporter.Core/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Core/Discord/DiscordClient.cs @@ -11,7 +11,6 @@ using DiscordChatExporter.Core.Utils; using DiscordChatExporter.Core.Utils.Extensions; using JsonExtensions.Http; using JsonExtensions.Reading; -using Tyrrrz.Extensions; namespace DiscordChatExporter.Core.Discord { @@ -29,7 +28,9 @@ namespace DiscordChatExporter.Core.Discord } public DiscordClient(AuthToken token) - : this(Http.Client, token) {} + : this(Http.Client, token) + { + } private async ValueTask GetResponseAsync(string url) => await Http.ResponsePolicy.ExecuteAsync(async () => @@ -64,7 +65,7 @@ namespace DiscordChatExporter.Core.Discord return response.IsSuccessStatusCode ? await response.Content.ReadAsJsonAsync() - : (JsonElement?) null; + : null; } public async IAsyncEnumerable GetUserGuildsAsync() @@ -118,29 +119,30 @@ namespace DiscordChatExporter.Core.Discord { var response = await GetJsonResponseAsync($"guilds/{guildId}/channels"); - var orderedResponse = response + var responseOrdered = response .EnumerateArray() .OrderBy(j => j.GetProperty("position").GetInt32()) - .ThenBy(j => ulong.Parse(j.GetProperty("id").GetString())) + .ThenBy(j => Snowflake.Parse(j.GetProperty("id").GetString())) .ToArray(); - var categories = orderedResponse - .Where(j => j.GetProperty("type").GetInt32() == (int)ChannelType.GuildCategory) + var categories = responseOrdered + .Where(j => j.GetProperty("type").GetInt32() == (int) ChannelType.GuildCategory) .Select((j, index) => ChannelCategory.Parse(j, index + 1)) - .ToDictionary(j => j.Id.ToString()); + .ToDictionary(j => j.Id.ToString(), StringComparer.Ordinal); var position = 0; - foreach (var channelJson in orderedResponse) + foreach (var channelJson in responseOrdered) { var parentId = channelJson.GetPropertyOrNull("parent_id")?.GetString(); + var category = !string.IsNullOrWhiteSpace(parentId) ? categories.GetValueOrDefault(parentId) : null; var channel = Channel.Parse(channelJson, category, position); - // Skip non-text channels + // We are only interested in text channels if (!channel.IsTextChannel) continue; @@ -178,15 +180,12 @@ namespace DiscordChatExporter.Core.Discord var response = await GetJsonResponseAsync($"channels/{channelId}"); return ChannelCategory.Parse(response); } - /*** - * In some cases, the Discord API returns an empty body when requesting some channel category info. - * Instead, we use an empty channel category as a fallback. - */ + // In some cases, the Discord API returns an empty body when requesting channel category. + // Instead, we use an empty channel category as a fallback. catch (DiscordChatExporterException) { return ChannelCategory.Empty; } - } public async ValueTask GetChannelAsync(Snowflake channelId) @@ -221,7 +220,7 @@ namespace DiscordChatExporter.Core.Discord IProgress? progress = null) { // Get the last message in the specified range. - // This snapshots the boundaries, which means that messages posted after the exported started + // This snapshots the boundaries, which means that messages posted after the export started // will not appear in the output. // Additionally, it provides the date of the last message, which is used to calculate progress. var lastMessage = await TryGetLastMessageAsync(channelId, before); @@ -271,7 +270,7 @@ namespace DiscordChatExporter.Core.Discord progress.Report(exportedDuration / totalDuration); } // Avoid division by zero if all messages have the exact same timestamp - // (which can happen easily if there's only one message in the channel) + // (which may be the case if there's only one message in the channel) else { progress.Report(1); @@ -284,4 +283,4 @@ namespace DiscordChatExporter.Core.Discord } } } -} +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Snowflake.cs b/DiscordChatExporter.Core/Discord/Snowflake.cs index b7c15e6..36bd378 100644 --- a/DiscordChatExporter.Core/Discord/Snowflake.cs +++ b/DiscordChatExporter.Core/Discord/Snowflake.cs @@ -10,8 +10,9 @@ namespace DiscordChatExporter.Core.Discord public Snowflake(ulong value) => Value = value; - public DateTimeOffset ToDate() => - DateTimeOffset.FromUnixTimeMilliseconds((long) ((Value >> 22) + 1420070400000UL)).ToLocalTime(); + public DateTimeOffset ToDate() => DateTimeOffset.FromUnixTimeMilliseconds( + (long) ((Value >> 22) + 1420070400000UL) + ).ToLocalTime(); public override string ToString() => Value.ToString(CultureInfo.InvariantCulture); } @@ -53,9 +54,11 @@ namespace DiscordChatExporter.Core.Discord public static Snowflake Parse(string str) => Parse(str, null); } - public partial struct Snowflake : IEquatable + public partial struct Snowflake : IComparable, IEquatable { - public bool Equals(Snowflake other) => Value == other.Value; + public int CompareTo(Snowflake other) => Value.CompareTo(other.Value); + + public bool Equals(Snowflake other) => CompareTo(other) == 0; public override bool Equals(object? obj) => obj is Snowflake other && Equals(other); diff --git a/DiscordChatExporter.Core/Exporting/ExportContext.cs b/DiscordChatExporter.Core/Exporting/ExportContext.cs index f6b760c..cb44eb2 100644 --- a/DiscordChatExporter.Core/Exporting/ExportContext.cs +++ b/DiscordChatExporter.Core/Exporting/ExportContext.cs @@ -79,7 +79,7 @@ namespace DiscordChatExporter.Core.Exporting : filePath; // HACK: for HTML, we need to format the URL properly - if (Request.Format == ExportFormat.HtmlDark || Request.Format == ExportFormat.HtmlLight) + if (Request.Format is ExportFormat.HtmlDark or ExportFormat.HtmlLight) { // Need to escape each path segment while keeping the directory separators intact return relativeFilePath @@ -93,7 +93,7 @@ namespace DiscordChatExporter.Core.Exporting // Try to catch only exceptions related to failed HTTP requests // https://github.com/Tyrrrz/DiscordChatExporter/issues/332 // https://github.com/Tyrrrz/DiscordChatExporter/issues/372 - catch (Exception ex) when (ex is HttpRequestException || ex is OperationCanceledException) + catch (Exception ex) when (ex is HttpRequestException or OperationCanceledException) { // TODO: add logging so we can be more liberal with catching exceptions // We don't want this to crash the exporting process in case of failure diff --git a/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroup.cs b/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroup.cs index ad1fd8b..b9c90f1 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroup.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroup.cs @@ -36,9 +36,13 @@ namespace DiscordChatExporter.Core.Exporting.Writers.Html internal partial class MessageGroup { public static bool CanJoin(Message message1, Message message2) => + // Must be from the same author message1.Author.Id == message2.Author.Id && + // Author's name must not have changed between messages string.Equals(message1.Author.FullName, message2.Author.FullName, StringComparison.Ordinal) && + // Duration between messages must be 7 minutes or less (message2.Timestamp - message1.Timestamp).Duration().TotalMinutes <= 7 && + // Other message must not be a reply message2.Reference is null; public static MessageGroup Join(IReadOnlyList messages) diff --git a/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs b/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs index 3bc3d22..b355e4f 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs @@ -20,6 +20,8 @@ namespace DiscordChatExporter.Core.Exporting.Writers { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Indented = true, + // Validation errors may mask actual failures + // https://github.com/Tyrrrz/DiscordChatExporter/issues/413 SkipValidation = true }); } @@ -300,4 +302,4 @@ namespace DiscordChatExporter.Core.Exporting.Writers await base.DisposeAsync(); } } -} +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Utils/Extensions/GenericExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/GenericExtensions.cs index ad5befe..48744f8 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/GenericExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/GenericExtensions.cs @@ -9,6 +9,6 @@ namespace DiscordChatExporter.Core.Utils.Extensions public static T? NullIf(this T value, Func predicate) where T : struct => !predicate(value) ? value - : (T?) null; + : null; } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs index 06ca98f..4ece79d 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs @@ -4,9 +4,14 @@ namespace DiscordChatExporter.Core.Utils.Extensions { 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.Substring(0, charCount) + ? str[..charCount] : str; public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) => diff --git a/DiscordChatExporter.Core/Utils/Http.cs b/DiscordChatExporter.Core/Utils/Http.cs index 68595fd..dd4e2a1 100644 --- a/DiscordChatExporter.Core/Utils/Http.cs +++ b/DiscordChatExporter.Core/Utils/Http.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; +using DiscordChatExporter.Core.Utils.Extensions; using Polly; namespace DiscordChatExporter.Core.Utils @@ -41,21 +42,24 @@ namespace DiscordChatExporter.Core.Utils }, (_, _, _, _) => Task.CompletedTask); - private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex) - { + private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex) => // This is extremely frail, but there's no other way - var statusCodeRaw = Regex.Match(ex.Message, @": (\d+) \(").Groups[1].Value; - return !string.IsNullOrWhiteSpace(statusCodeRaw) - ? (HttpStatusCode) int.Parse(statusCodeRaw, CultureInfo.InvariantCulture) - : (HttpStatusCode?) null; - } + Regex + .Match(ex.Message, @": (\d+) \(") + .Groups[1] + .Value + .NullIfWhiteSpace()? + .Pipe(s => (HttpStatusCode) int.Parse(s, CultureInfo.InvariantCulture)); public static IAsyncPolicy ExceptionPolicy { get; } = Policy .Handle() // dangerous - .Or(ex => TryGetStatusCodeFromException(ex) == HttpStatusCode.TooManyRequests) - .Or(ex => TryGetStatusCodeFromException(ex) == HttpStatusCode.RequestTimeout) - .Or(ex => TryGetStatusCodeFromException(ex) >= HttpStatusCode.InternalServerError) + .Or(ex => + TryGetStatusCodeFromException(ex) is + HttpStatusCode.TooManyRequests or + HttpStatusCode.RequestTimeout or + HttpStatusCode.InternalServerError + ) .WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1)); } } \ No newline at end of file