Compare commits

...

3 Commits

Author SHA1 Message Date
Tyrrrz 5fc2fae28a Update version 2023-07-27 23:59:17 +03:00
Tyrrrz ddfbe51cfa Improve error reporting on unexpected HTTP status code 2023-07-25 18:59:05 +03:00
Tyrrrz bcf652edbe Don't throw when parent channel cannot be retrieved
Closes #1108
2023-07-23 16:04:53 +03:00
7 changed files with 77 additions and 46 deletions
+7
View File
@@ -1,5 +1,12 @@
# Changelog # Changelog
## v2.40.2 (27-Jul-2023)
- General changes:
- Improved error reporting on unexpected HTTP errors.
- CLI changes:
- Fixed an issue which caused a crash when trying to export a private (but accessible) channel inside a private (but not accessible) category.
## v2.40.1 (21-Jul-2023) ## v2.40.1 (21-Jul-2023)
- General changes: - General changes:
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<Version>2.40.1</Version> <Version>2.40.2</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>
@@ -20,8 +20,9 @@ public partial record Channel(
Snowflake? LastMessageId) : IHasId Snowflake? LastMessageId) : IHasId
{ {
// Used for visual backwards-compatibility with old exports, where // Used for visual backwards-compatibility with old exports, where
// channels without a parent (i.e. mostly DM channels) had a fallback // channels without a parent (i.e. mostly DM channels) or channels
// category created for them. // with an inaccessible parent (i.e. inside private categories) had
// a fallback category created for them.
public string Category => Parent?.Name ?? Kind switch public string Category => Parent?.Name ?? Kind switch
{ {
ChannelKind.GuildCategory => "Category", ChannelKind.GuildCategory => "Category",
@@ -108,7 +108,7 @@ public class DiscordClient
if (botResponse.StatusCode != HttpStatusCode.Unauthorized) if (botResponse.StatusCode != HttpStatusCode.Unauthorized)
return TokenKind.Bot; return TokenKind.Bot;
throw DiscordChatExporterException.Unauthorized(); throw new DiscordChatExporterException("Authentication token is invalid.", true);
} }
private async ValueTask<HttpResponseMessage> GetResponseAsync( private async ValueTask<HttpResponseMessage> GetResponseAsync(
@@ -129,10 +129,26 @@ public class DiscordClient
{ {
throw response.StatusCode switch throw response.StatusCode switch
{ {
HttpStatusCode.Unauthorized => DiscordChatExporterException.Unauthorized(), HttpStatusCode.Unauthorized => throw new DiscordChatExporterException(
HttpStatusCode.Forbidden => DiscordChatExporterException.Forbidden(), "Authentication token is invalid.",
HttpStatusCode.NotFound => DiscordChatExporterException.NotFound(url), true
_ => DiscordChatExporterException.FailedHttpRequest(response) ),
HttpStatusCode.Forbidden => throw new DiscordChatExporterException(
$"Request to '{url}' failed: forbidden."
),
HttpStatusCode.NotFound => throw new DiscordChatExporterException(
$"Request to '{url}' failed: not found."
),
_ => throw new DiscordChatExporterException(
$"""
Request to '{url}' failed: {response.StatusCode.ToString().ToSpaceSeparatedWords().ToLowerInvariant()}.
Response content: {await response.Content.ReadAsStringAsync(cancellationToken)}
""",
true
)
}; };
} }
@@ -368,11 +384,21 @@ public class DiscordClient
.GetNonWhiteSpaceStringOrNull()? .GetNonWhiteSpaceStringOrNull()?
.Pipe(Snowflake.Parse); .Pipe(Snowflake.Parse);
var parent = parentId is not null try
? await GetChannelAsync(parentId.Value, cancellationToken) {
: null; var parent = parentId is not null
? await GetChannelAsync(parentId.Value, cancellationToken)
: null;
return Channel.Parse(response, parent); return Channel.Parse(response, parent);
}
// It's possible for the parent channel to be inaccessible, despite the
// child channel being accessible.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1108
catch (DiscordChatExporterException)
{
return Channel.Parse(response);
}
} }
private async ValueTask<Message?> TryGetLastMessageAsync( private async ValueTask<Message?> TryGetLastMessageAsync(
@@ -1,9 +1,8 @@
using System; using System;
using System.Net.Http;
namespace DiscordChatExporter.Core.Exceptions; namespace DiscordChatExporter.Core.Exceptions;
public partial class DiscordChatExporterException : Exception public class DiscordChatExporterException : Exception
{ {
public bool IsFatal { get; } public bool IsFatal { get; }
@@ -12,33 +11,4 @@ public partial class DiscordChatExporterException : Exception
{ {
IsFatal = isFatal; IsFatal = isFatal;
} }
}
public partial class DiscordChatExporterException
{
internal static DiscordChatExporterException FailedHttpRequest(HttpResponseMessage response)
{
var message = $@"
Failed to perform an HTTP request.
[Request]
{response.RequestMessage}
[Response]
{response}";
return new DiscordChatExporterException(message.Trim(), true);
}
internal static DiscordChatExporterException Unauthorized() =>
new("Authentication token is invalid.", true);
internal static DiscordChatExporterException Forbidden() =>
new("Access is forbidden.");
internal static DiscordChatExporterException NotFound(string resourceId) =>
new($"Requested resource ({resourceId}) does not exist.");
internal static DiscordChatExporterException ChannelIsEmpty() =>
new("Channel is empty or contains no messages for the specified period.");
} }
@@ -20,11 +20,19 @@ public class ChannelExporter
{ {
// Check if the channel is empty // Check if the channel is empty
if (request.Channel.LastMessageId is null) if (request.Channel.LastMessageId is null)
throw DiscordChatExporterException.ChannelIsEmpty(); {
throw new DiscordChatExporterException(
"Channel does not contain any messages."
);
}
// Check if the 'after' boundary is valid // Check if the 'after' boundary is valid
if (request.After is not null && request.Channel.LastMessageId < request.After) if (request.After is not null && request.Channel.LastMessageId < request.After)
throw DiscordChatExporterException.ChannelIsEmpty(); {
throw new DiscordChatExporterException(
"Channel does not contain any messages within the specified period."
);
}
// Build context // Build context
var context = new ExportContext(_discord, request); var context = new ExportContext(_discord, request);
@@ -50,6 +58,10 @@ public class ChannelExporter
// Throw if no messages were exported // Throw if no messages were exported
if (messageExporter.MessagesExported <= 0) if (messageExporter.MessagesExported <= 0)
throw DiscordChatExporterException.ChannelIsEmpty(); {
throw new DiscordChatExporterException(
"Channel does not contain any matching messages within the specified period."
);
}
} }
} }
@@ -16,6 +16,21 @@ public static class StringExtensions
? str[..charCount] ? str[..charCount]
: str; : str;
public static string ToSpaceSeparatedWords(this string str)
{
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();
}
public static IEnumerable<Rune> GetRunes(this string str) public static IEnumerable<Rune> GetRunes(this string str)
{ {
var lastIndex = 0; var lastIndex = 0;