This commit is contained in:
Tyrrrz 2023-09-03 21:43:55 +03:00
parent 9583e2684d
commit 9601e0acea
3 changed files with 41 additions and 34 deletions

View file

@ -39,11 +39,17 @@ public partial record Channel(
_ => "Default" _ => "Default"
}; };
public bool IsEmpty => LastMessageId is null;
// Only needed for WPF data binding. Don't use anywhere else. // Only needed for WPF data binding. Don't use anywhere else.
public bool IsVoice => Kind.IsVoice(); public bool IsVoice => Kind.IsVoice();
// Only needed for WPF data binding. Don't use anywhere else. // Only needed for WPF data binding. Don't use anywhere else.
public bool IsThread => Kind.IsThread(); public bool IsThread => Kind.IsThread();
public bool MayHaveMessagesAfter(Snowflake messageId) => !IsEmpty && messageId < LastMessageId;
public bool MayHaveMessagesBefore(Snowflake messageId) => !IsEmpty && messageId > Id;
} }
public partial record Channel public partial record Channel

View file

@ -286,23 +286,25 @@ public class DiscordClient
yield break; yield break;
var tokenKind = _resolvedTokenKind ??= await GetTokenKindAsync(cancellationToken); var tokenKind = _resolvedTokenKind ??= await GetTokenKindAsync(cancellationToken);
var channels = await GetGuildChannelsAsync(guildId, cancellationToken); var channels = (await GetGuildChannelsAsync(guildId, cancellationToken))
var filteredChannels = channels
// Categories cannot have threads // Categories cannot have threads
.Where(c => c.Kind != ChannelKind.GuildCategory) .Where(c => c.Kind != ChannelKind.GuildCategory)
// Voice channels cannot have threads // Voice channels cannot have threads
.Where(c => !c.Kind.IsVoice()) .Where(c => !c.Kind.IsVoice())
// Ordinary channel or forum channel without LastMessageId cannot have threads // Empty channels cannot have threads
.Where(c => c.LastMessageId != null) .Where(c => !c.IsEmpty)
// Ff --before is specified, skip channels created after the specified date // If the 'before' boundary is specified, skip channels that don't have messages
.Where(c => before == null || before > c.Id); // for that range, because thread-start event should always be accompanied by a message.
// Note that we don't perform a similar check for the 'after' boundary, because
// threads may have messages in range, even if the parent channel doesn't.
.Where(c => before is null || c.MayHaveMessagesBefore(before.Value))
.ToArray();
// User accounts can only fetch threads using the search endpoint // User accounts can only fetch threads using the search endpoint
if (tokenKind == TokenKind.User) if (tokenKind == TokenKind.User)
{ {
// Active threads // Active threads
foreach (var channel in filteredChannels) foreach (var channel in channels)
{ {
var currentOffset = 0; var currentOffset = 0;
while (true) while (true)
@ -320,7 +322,7 @@ public class DiscordClient
if (response is null) if (response is null)
break; break;
var containsOlder = false; var breakOuter = false;
foreach ( foreach (
var threadJson in response.Value.GetProperty("threads").EnumerateArray() var threadJson in response.Value.GetProperty("threads").EnumerateArray()
@ -328,19 +330,19 @@ public class DiscordClient
{ {
var thread = Channel.Parse(threadJson, channel); var thread = Channel.Parse(threadJson, channel);
// if --after is specified, we can break early, because the threads are sorted by last message time // If the 'after' boundary is specified, we can break early,
if (after is not null && after > thread.LastMessageId) // because threads are sorted by last message time.
if (after is not null && !thread.MayHaveMessagesAfter(after.Value))
{ {
containsOlder = true; breakOuter = true;
break; break;
} }
yield return thread; yield return thread;
currentOffset++; currentOffset++;
} }
if (containsOlder) if (breakOuter)
break; break;
if (!response.Value.GetProperty("has_more").GetBoolean()) if (!response.Value.GetProperty("has_more").GetBoolean())
@ -351,7 +353,7 @@ public class DiscordClient
// Archived threads // Archived threads
if (includeArchived) if (includeArchived)
{ {
foreach (var channel in filteredChannels) foreach (var channel in channels)
{ {
var currentOffset = 0; var currentOffset = 0;
while (true) while (true)
@ -369,7 +371,7 @@ public class DiscordClient
if (response is null) if (response is null)
break; break;
var containsOlder = false; var breakOuter = false;
foreach ( foreach (
var threadJson in response.Value.GetProperty("threads").EnumerateArray() var threadJson in response.Value.GetProperty("threads").EnumerateArray()
@ -377,19 +379,19 @@ public class DiscordClient
{ {
var thread = Channel.Parse(threadJson, channel); var thread = Channel.Parse(threadJson, channel);
// if --after is specified, we can break early, because the threads are sorted by last message time // If the 'after' boundary is specified, we can break early,
if (after is not null && after > thread.LastMessageId) // because threads are sorted by last message time.
if (after is not null && !thread.MayHaveMessagesAfter(after.Value))
{ {
containsOlder = true; breakOuter = true;
break; break;
} }
yield return thread; yield return thread;
currentOffset++; currentOffset++;
} }
if (containsOlder) if (breakOuter)
break; break;
if (!response.Value.GetProperty("has_more").GetBoolean()) if (!response.Value.GetProperty("has_more").GetBoolean())
@ -403,7 +405,7 @@ public class DiscordClient
{ {
// Active threads // Active threads
{ {
var parentsById = filteredChannels.ToDictionary(c => c.Id); var parentsById = channels.ToDictionary(c => c.Id);
var response = await GetJsonResponseAsync( var response = await GetJsonResponseAsync(
$"guilds/{guildId}/threads/active", $"guilds/{guildId}/threads/active",
@ -425,7 +427,7 @@ public class DiscordClient
// Archived threads // Archived threads
if (includeArchived) if (includeArchived)
{ {
foreach (var channel in filteredChannels) foreach (var channel in channels)
{ {
// Public archived threads // Public archived threads
{ {

View file

@ -20,14 +20,16 @@ public class ChannelExporter
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
// Forum channels don't have messages, they are just a list of threads
if (request.Channel.Kind == ChannelKind.GuildForum)
throw new DiscordChatExporterException("Channel is a forum.");
// Check if the channel is empty // Check if the channel is empty
if (request.Channel.LastMessageId is null) if (request.Channel.IsEmpty)
{
throw new DiscordChatExporterException("Channel does not contain any messages."); 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.MayHaveMessagesAfter(request.After.Value))
{ {
throw new DiscordChatExporterException( throw new DiscordChatExporterException(
"Channel does not contain any messages within the specified period." "Channel does not contain any messages within the specified period."
@ -35,19 +37,16 @@ public class ChannelExporter
} }
// Check if the 'before' boundary is valid // Check if the 'before' boundary is valid
if (request.Before is not null && request.Channel.Id > request.Before) if (
request.Before is not null
&& !request.Channel.MayHaveMessagesBefore(request.Before.Value)
)
{ {
throw new DiscordChatExporterException( throw new DiscordChatExporterException(
"Channel does not contain any messages within the specified period." "Channel does not contain any messages within the specified period."
); );
} }
// Skip forum channels, they are exported as threads
if (request.Channel.Kind == ChannelKind.GuildForum)
{
throw new DiscordChatExporterException("Channel is a forum.");
}
// Build context // Build context
var context = new ExportContext(_discord, request); var context = new ExportContext(_discord, request);
await context.PopulateChannelsAndRolesAsync(cancellationToken); await context.PopulateChannelsAndRolesAsync(cancellationToken);