Skip to content

Commit

Permalink
Merge
Browse files Browse the repository at this point in the history
  • Loading branch information
nulldg committed Sep 4, 2023
2 parents 71a0814 + ccdf082 commit 4ae1b83
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 21 deletions.
2 changes: 2 additions & 0 deletions DiscordChatExporter.Cli/Commands/ExportAllCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ public override async ValueTask ExecuteAsync(IConsole console)
var thread in Discord.GetGuildThreadsAsync(
guild.Id,
ThreadInclusionMode == ThreadInclusionMode.All,
Before,
After,
cancellationToken
)
)
Expand Down
2 changes: 2 additions & 0 deletions DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public override async ValueTask ExecuteAsync(IConsole console)
var thread in Discord.GetGuildThreadsAsync(
GuildId,
ThreadInclusionMode == ThreadInclusionMode.All,
Before,
After,
cancellationToken
)
)
Expand Down
19 changes: 13 additions & 6 deletions DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,19 @@ public override async ValueTask ExecuteAsync(IConsole console)
.OrderDescending()
.FirstOrDefault();

var threads = ThreadInclusionMode != ThreadInclusionMode.None
? (await Discord.GetGuildThreadsAsync(
GuildId,
ThreadInclusionMode == ThreadInclusionMode.All,
cancellationToken
)).OrderBy(c => c.Name).ToArray()
var threads =
ThreadInclusionMode != ThreadInclusionMode.None
? (
await Discord.GetGuildThreadsAsync(
GuildId,
ThreadInclusionMode == ThreadInclusionMode.All,
null,
null,
cancellationToken
)
)
.OrderBy(c => c.Name)
.ToArray()
: Array.Empty<Channel>();

foreach (var channel in channels)
Expand Down
20 changes: 12 additions & 8 deletions DiscordChatExporter.Core/Discord/Data/Channel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ public partial record Channel(
_ => "Default"
};

public bool IsEmpty => LastMessageId is null;

// Only needed for WPF data binding. Don't use anywhere else.
public bool IsVoice => Kind.IsVoice();

// Only needed for WPF data binding. Don't use anywhere else.
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
Expand All @@ -58,17 +64,15 @@ public static Channel Parse(JsonElement json, Channel? parent = null, int? posit

var name =
// Guild channel
json.GetPropertyOrNull("name")?.GetNonWhiteSpaceStringOrNull() ??

json.GetPropertyOrNull("name")?.GetNonWhiteSpaceStringOrNull()
// DM channel
json.GetPropertyOrNull("recipients")?
.EnumerateArrayOrNull()?
.Select(User.Parse)
?? json.GetPropertyOrNull("recipients")
?.EnumerateArrayOrNull()
?.Select(User.Parse)
.Select(u => u.DisplayName)
.Pipe(s => string.Join(", ", s)) ??

.Pipe(s => string.Join(", ", s))
// Fallback
id.ToString();
?? id.ToString();

var position =
positionHint ??
Expand Down
54 changes: 51 additions & 3 deletions DiscordChatExporter.Core/Discord/DiscordClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,28 @@ public async IAsyncEnumerable<Channel> GetGuildChannelsAsync(
public async IAsyncEnumerable<Channel> GetGuildThreadsAsync(
Snowflake guildId,
bool includeArchived = false,
Snowflake? before = null,
Snowflake? after = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default
)
{
if (guildId == Guild.DirectMessages.Id)
yield break;

var tokenKind = _resolvedTokenKind ??= await GetTokenKindAsync(cancellationToken);
var channels = await GetGuildChannelsAsync(guildId, cancellationToken);
var channels = (await GetGuildChannelsAsync(guildId, cancellationToken))
// Categories cannot have threads
.Where(c => c.Kind != ChannelKind.GuildCategory)
// Voice channels cannot have threads
.Where(c => !c.Kind.IsVoice())
// Empty channels cannot have threads
.Where(c => !c.IsEmpty)
// If the 'before' boundary is specified, skip channels that don't have messages
// 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
if (tokenKind == TokenKind.User)
Expand All @@ -297,6 +311,8 @@ public async IAsyncEnumerable<Channel> GetGuildThreadsAsync(
{
var url = new UrlBuilder()
.SetPath($"channels/{channel.Id}/threads/search")
.SetQueryParameter("sort_by", "last_message_time")
.SetQueryParameter("sort_order", "desc")
.SetQueryParameter("archived", "false")
.SetQueryParameter("offset", currentOffset.ToString())
.Build();
Expand All @@ -306,14 +322,29 @@ public async IAsyncEnumerable<Channel> GetGuildThreadsAsync(
if (response is null)
break;

var breakOuter = false;

foreach (
var threadJson in response.Value.GetProperty("threads").EnumerateArray()
)
{
yield return Channel.Parse(threadJson, channel);
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))
{
breakOuter = true;
break;
}

yield return thread;
currentOffset++;
}

if (breakOuter)
break;

if (!response.Value.GetProperty("has_more").GetBoolean())
break;
}
Expand All @@ -329,6 +360,8 @@ var threadJson in response.Value.GetProperty("threads").EnumerateArray()
{
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();
Expand All @@ -338,14 +371,29 @@ var threadJson in response.Value.GetProperty("threads").EnumerateArray()
if (response is null)
break;

var breakOuter = false;

foreach (
var threadJson in response.Value.GetProperty("threads").EnumerateArray()
)
{
yield return Channel.Parse(threadJson, channel);
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))
{
breakOuter = true;
break;
}

yield return thread;
currentOffset++;
}

if (breakOuter)
break;

if (!response.Value.GetProperty("has_more").GetBoolean())
break;
}
Expand Down
20 changes: 16 additions & 4 deletions DiscordChatExporter.Core/Exporting/ChannelExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exceptions;
using Gress;

Expand All @@ -18,16 +19,27 @@ public async ValueTask ExportChannelAsync(
IProgress<Percentage>? progress = null,
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
if (request.Channel.LastMessageId is null)
if (request.Channel.IsEmpty)
throw new DiscordChatExporterException("Channel does not contain any messages.");

// Check if the 'after' boundary is valid
if (request.After is not null && !request.Channel.MayHaveMessagesAfter(request.After.Value))
{
throw new DiscordChatExporterException(
"Channel does not contain any messages."
"Channel does not contain any messages within the specified period."
);
}

// Check if the 'after' boundary is valid
if (request.After is not null && request.Channel.LastMessageId < request.After)
// Check if the 'before' boundary is valid
if (
request.Before is not null
&& !request.Channel.MayHaveMessagesBefore(request.Before.Value)
)
{
throw new DiscordChatExporterException(
"Channel does not contain any messages within the specified period."
Expand Down

0 comments on commit 4ae1b83

Please sign in to comment.