Refactor
Some checks failed
docker / pack (push) Has been cancelled
docker / deploy (push) Has been cancelled
main / format (push) Has been cancelled
main / test (push) Has been cancelled
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-arm) (push) Has been cancelled
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-arm64) (push) Has been cancelled
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-musl-x64) (push) Has been cancelled
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-x64) (push) Has been cancelled
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, osx-arm64) (push) Has been cancelled
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, osx-x64) (push) Has been cancelled
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-arm64) (push) Has been cancelled
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-x64) (push) Has been cancelled
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-x86) (push) Has been cancelled
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, linux-arm) (push) Has been cancelled
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, linux-arm64) (push) Has been cancelled
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, linux-musl-x64) (push) Has been cancelled
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, linux-x64) (push) Has been cancelled
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, osx-arm64) (push) Has been cancelled
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, osx-x64) (push) Has been cancelled
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, win-arm64) (push) Has been cancelled
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, win-x64) (push) Has been cancelled
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, win-x86) (push) Has been cancelled
main / release (push) Has been cancelled
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-arm) (push) Has been cancelled
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-arm64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-musl-x64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-x64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, osx-arm64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, osx-x64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-arm64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-x64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-x86) (push) Has been cancelled
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, linux-arm) (push) Has been cancelled
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, linux-arm64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, linux-musl-x64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, linux-x64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, osx-arm64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, osx-x64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, win-arm64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, win-x64) (push) Has been cancelled
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, win-x86) (push) Has been cancelled
main / notify (push) Has been cancelled

This commit is contained in:
tyrrrz 2026-06-26 13:31:15 +03:00
parent e2c633b004
commit 05f8df51e9

View file

@ -30,9 +30,8 @@ public class DiscordClient(
string url, string url,
TokenKind tokenKind, TokenKind tokenKind,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) ) =>
{ await Http.ResponseResiliencePipeline.ExecuteAsync(
return await Http.ResponseResiliencePipeline.ExecuteAsync(
async innerCancellationToken => async innerCancellationToken =>
{ {
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url)); using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
@ -91,7 +90,6 @@ public class DiscordClient(
}, },
cancellationToken cancellationToken
); );
}
private async ValueTask<TokenKind> ResolveTokenKindAsync( private async ValueTask<TokenKind> ResolveTokenKindAsync(
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
@ -364,6 +362,7 @@ public class DiscordClient(
$"guilds/{guildId}/members/{memberId}", $"guilds/{guildId}/members/{memberId}",
cancellationToken cancellationToken
); );
return response?.Pipe(j => Member.Parse(j, guildId)); return response?.Pipe(j => Member.Parse(j, guildId));
} }
@ -412,14 +411,12 @@ public class DiscordClient(
?.GetNonWhiteSpaceStringOrNull() ?.GetNonWhiteSpaceStringOrNull()
?.Pipe(Snowflake.Parse); ?.Pipe(Snowflake.Parse);
Channel? parent = null;
if (parentId is not null)
{
// It's possible for the parent channel to be inaccessible, despite the // It's possible for the parent channel to be inaccessible, despite the
// child channel being accessible. // child channel being accessible.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1108 // https://github.com/Tyrrrz/DiscordChatExporter/issues/1108
parent = await TryGetChannelAsync(parentId.Value, cancellationToken); var parent = parentId is not null
} ? await TryGetChannelAsync(parentId.Value, cancellationToken)
: null;
return Channel.Parse(response.Value, parent); return Channel.Parse(response.Value, parent);
} }
@ -607,8 +604,12 @@ public class DiscordClient(
.SetQueryParameter("after", (after ?? Snowflake.Zero).ToString()) .SetQueryParameter("after", (after ?? Snowflake.Zero).ToString())
.Build(); .Build();
var response = await GetJsonResponseAsync(url, cancellationToken); // Can be null on channels that the user cannot access
var message = response.EnumerateArray().Select(Message.Parse).FirstOrDefault(); var response = await TryGetJsonResponseAsync(url, cancellationToken);
if (response is null)
return null;
var message = response.Value.EnumerateArray().Select(Message.Parse).FirstOrDefault();
return message; return message;
} }
@ -625,8 +626,12 @@ public class DiscordClient(
.SetQueryParameter("before", before?.ToString()) .SetQueryParameter("before", before?.ToString())
.Build(); .Build();
var response = await GetJsonResponseAsync(url, cancellationToken); // Can be null on channels that the user cannot access
return response.EnumerateArray().Select(Message.Parse).LastOrDefault(); var response = await TryGetJsonResponseAsync(url, cancellationToken);
if (response is null)
return null;
return response.Value.EnumerateArray().Select(Message.Parse).LastOrDefault();
} }
public async ValueTask<Message?> TryGetMessageAsync( public async ValueTask<Message?> TryGetMessageAsync(
@ -656,33 +661,6 @@ public class DiscordClient(
.FirstOrDefault(m => m.Id == messageId); .FirstOrDefault(m => m.Id == messageId);
} }
private async ValueTask<Message?> ResolveThreadStarterMessageAsync(
Message message,
CancellationToken cancellationToken = default
)
{
// Threads created from a message contain an empty THREAD_STARTER_MESSAGE placeholder at
// the top of their history (in place of the actual starter message) that merely points
// back to the originating message in the parent channel. Resolve the placeholder to that
// actual message so the thread's starter message appears in the output, in its correct
// chronological position, with its real content.
// This doesn't apply to forum/media posts, whose starter message is already a regular
// message in the thread's own history (i.e. not a placeholder).
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1265
if (message.Kind != MessageKind.ThreadStarterMessage)
return message;
// The placeholder references the parent channel and the original message it points to.
if (message.Reference?.ChannelId is not { } channelId)
return null;
if (message.Reference?.MessageId is not { } messageId)
return null;
// The original message may no longer be accessible (e.g. deleted), in which case the
// empty placeholder is dropped as well.
return await TryGetMessageAsync(channelId, messageId, cancellationToken);
}
public async IAsyncEnumerable<Message> GetMessagesAsync( public async IAsyncEnumerable<Message> GetMessagesAsync(
Snowflake channelId, Snowflake channelId,
Snowflake? after = null, Snowflake? after = null,
@ -755,14 +733,20 @@ public class DiscordClient(
); );
} }
// Thread starter messages are returned as empty placeholders; resolve them to // Some messages, for example thread starter messages, are returned by the API as content-less references.
// the actual message they reference before yielding (or skip if unavailable). // Try to resolve them to the actual message so that they appear as they do in the Discord client.
var resolvedMessage = await ResolveThreadStarterMessageAsync( var actualMessage =
message, message.Kind == MessageKind.ThreadStarterMessage
&& message.Reference?.ChannelId is { } referencedChannelId
&& message.Reference?.MessageId is { } referencedMessageId
? await TryGetMessageAsync(
referencedChannelId,
referencedMessageId,
cancellationToken cancellationToken
); )
if (resolvedMessage is not null) : null;
yield return resolvedMessage;
yield return actualMessage ?? message;
currentAfter = message.Id; currentAfter = message.Id;
} }
@ -831,14 +815,20 @@ public class DiscordClient(
); );
} }
// Thread starter messages are returned as empty placeholders; resolve them to // Some messages, for example thread starter messages, are returned by the API as content-less references.
// the actual message they reference before yielding (or skip if unavailable). // Try to resolve them to the actual message so that they appear as they do in the Discord client.
var resolvedMessage = await ResolveThreadStarterMessageAsync( var actualMessage =
message, message.Kind == MessageKind.ThreadStarterMessage
&& message.Reference?.ChannelId is { } referencedChannelId
&& message.Reference?.MessageId is { } referencedMessageId
? await TryGetMessageAsync(
referencedChannelId,
referencedMessageId,
cancellationToken cancellationToken
); )
if (resolvedMessage is not null) : null;
yield return resolvedMessage;
yield return actualMessage ?? message;
} }
currentBefore = messages.Last().Id; currentBefore = messages.Last().Id;