Refactor after last changes
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 / notify (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

This commit is contained in:
Tyrrrz 2025-04-21 00:45:26 +03:00
parent 7ddd55951c
commit 1fadc0755b

View file

@ -311,9 +311,14 @@ public class DiscordClient(string token)
// User accounts can only fetch threads using the search endpoint // User accounts can only fetch threads using the search endpoint
if (await ResolveTokenKindAsync(cancellationToken) == TokenKind.User) if (await ResolveTokenKindAsync(cancellationToken) == TokenKind.User)
{ {
// Active threads
foreach (var channel in channels) foreach (var channel in channels)
{ {
// Either include both active and archived threads, or only active threads
foreach (
var isArchived in includeArchived ? new[] { false, true } : new[] { false }
)
{
// Offset is just the index of the last thread in the previous batch
var currentOffset = 0; var currentOffset = 0;
while (true) while (true)
{ {
@ -321,7 +326,7 @@ public class DiscordClient(string token)
.SetPath($"channels/{channel.Id}/threads/search") .SetPath($"channels/{channel.Id}/threads/search")
.SetQueryParameter("sort_by", "last_message_time") .SetQueryParameter("sort_by", "last_message_time")
.SetQueryParameter("sort_order", "desc") .SetQueryParameter("sort_order", "desc")
.SetQueryParameter("archived", "false") .SetQueryParameter("archived", isArchived.ToString().ToLowerInvariant())
.SetQueryParameter("offset", currentOffset.ToString()) .SetQueryParameter("offset", currentOffset.ToString())
.Build(); .Build();
@ -339,56 +344,7 @@ public class DiscordClient(string token)
var thread = Channel.Parse(threadJson, channel); var thread = Channel.Parse(threadJson, channel);
// If the 'after' boundary is specified, we can break early, // If the 'after' boundary is specified, we can break early,
// because threads are sorted by last message time. // because threads are sorted by last message timestamp.
if (after is not null && !thread.MayHaveMessagesAfter(after.Value))
{
breakOuter = true;
break;
}
yield return thread;
currentOffset++;
}
if (breakOuter)
break;
if (!response.Value.GetProperty("has_more").GetBoolean())
break;
}
}
// Archived threads
if (includeArchived)
{
foreach (var channel in channels)
{
var currentOffset = 0;
while (true)
{
var url = new UrlBuilder()
.SetPath($"channels/{channel.Id}/threads/search")
.SetQueryParameter("sort_by", "last_message_time")
.SetQueryParameter("sort_order", "desc")
.SetQueryParameter("archived", "true")
.SetQueryParameter("offset", currentOffset.ToString())
.Build();
// Can be null on channels that the user cannot access or channels without threads
var response = await TryGetJsonResponseAsync(url, cancellationToken);
if (response is null)
break;
var breakOuter = false;
foreach (
var threadJson in response.Value.GetProperty("threads").EnumerateArray()
)
{
var thread = Channel.Parse(threadJson, channel);
// If the 'after' boundary is specified, we can break early,
// because threads are sorted by last message time.
if (after is not null && !thread.MayHaveMessagesAfter(after.Value)) if (after is not null && !thread.MayHaveMessagesAfter(after.Value))
{ {
breakOuter = true; breakOuter = true;
@ -437,60 +393,46 @@ public class DiscordClient(string token)
{ {
foreach (var channel in channels) foreach (var channel in channels)
{ {
// Public archived threads foreach (var archiveType in new[] { "public", "private" })
await foreach (
var th in GetAllArchivedThreadsAsync(channel, "public", cancellationToken)
)
yield return th;
// Private archived threads
await foreach (
var th in GetAllArchivedThreadsAsync(channel, "private", cancellationToken)
)
yield return th;
}
}
}
}
private async IAsyncEnumerable<Channel> GetAllArchivedThreadsAsync(
Channel channel,
string archiveType,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{ {
// Base endpoint: "public" or "private" // This endpoint parameter expects an ISO8601 timestamp, not a snowflake
var endpointBase = $"channels/{channel.Id}/threads/archived/{archiveType}"; var currentBefore = before
// Cursor parameter: ISO8601 timestamp string ?.ToDate()
string? beforeTimestamp = null; .ToString("O", CultureInfo.InvariantCulture);
bool hasMorePages = true;
while (hasMorePages && !cancellationToken.IsCancellationRequested) while (true)
{ {
// Build URL with optional before= parameter // Threads are sorted by archive timestamp, not by last message timestamp
var url = beforeTimestamp is null var url = new UrlBuilder()
? endpointBase .SetPath($"channels/{channel.Id}/threads/archived/{archiveType}")
: $"{endpointBase}?before={Uri.EscapeDataString(beforeTimestamp)}"; .SetQueryParameter("before", currentBefore)
.Build();
// Can be null on certain channels
var response = await TryGetJsonResponseAsync(url, cancellationToken); var response = await TryGetJsonResponseAsync(url, cancellationToken);
if (response is null) if (response is null)
yield break; break;
// Parse out the threads array foreach (
var threadsJson = response.Value.GetProperty("threads").EnumerateArray().ToList(); var threadJson in response
foreach (var threadJson in threadsJson) .Value.GetProperty("threads")
.EnumerateArray()
)
{ {
yield return Channel.Parse(threadJson, channel); var thread = Channel.Parse(threadJson, channel);
yield return thread;
currentBefore = threadJson
.GetProperty("thread_metadata")
.GetProperty("archive_timestamp")
.GetString();
} }
// Check pagination flag if (!response.Value.GetProperty("has_more").GetBoolean())
hasMorePages = response.Value.GetProperty("has_more").GetBoolean(); break;
}
if (hasMorePages && threadsJson.Count > 0) }
{ }
// Prepare next cursor: the archived timestamp of the last thread
var lastThreadMeta = threadsJson.Last().GetProperty("thread_metadata");
beforeTimestamp = lastThreadMeta.GetProperty("archive_timestamp").GetString();
} }
} }
} }
@ -708,9 +650,7 @@ public class DiscordClient(string token)
count++; count++;
} }
// Each batch can contain up to 100 users. if (count <= 0)
// If we got fewer, then it's definitely the last batch.
if (count < 100)
yield break; yield break;
} }
} }