Fix typo and refactor

pull/1037/head
Tyrrrz 1 year ago
parent 25caf04445
commit 0ae9062a30

@ -24,7 +24,7 @@ public class GetChannelsCommand : DiscordCommandBase
"include-threads",
Description = "Display threads alongside channels."
)]
public bool IncludeTHreads { get; init; }
public bool IncludeThreads { get; init; }
public override async ValueTask ExecuteAsync(IConsole console)
{
@ -53,7 +53,7 @@ public class GetChannelsCommand : DiscordCommandBase
if (IncludeThreads)
{
var threads = (await Discord.GetGuildChannelThreadsAsync(channel.Id.ToString(), cancellationToken))
var threads = (await Discord.GetChannelThreadsAsync(channel.Id, cancellationToken))
.OrderBy(c => c.Name)
.ToArray();
@ -71,7 +71,7 @@ public class GetChannelsCommand : DiscordCommandBase
using (console.WithForegroundColor(ConsoleColor.DarkGray))
await console.Output.WriteAsync(" | ");
// Thread / thread name
// Thread name
using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteLineAsync($"Thread / {thread.Name}");
}

@ -7,39 +7,26 @@ using JsonExtensions.Reading;
namespace DiscordChatExporter.Core.Discord.Data;
// https://discord.com/developers/docs/resources/channel#channel-object-example-thread-channel
public partial record ThreadChannel(
public record ChannelThread(
Snowflake Id,
ChannelKind Kind,
Snowflake GuildId,
string Name,
Snowflake? LastMessageId) : IHasId
{
}
public partial record ThreadChannel
{
public static ThreadChannel Parse(JsonElement json)
public static ChannelThread Parse(JsonElement json)
{
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
var kind = (ChannelKind)json.GetProperty("type").GetInt32();
var guildId =
json.GetPropertyOrNull("guild_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse) ?? default;
var name =
// Guild channel
json.GetPropertyOrNull("name")?.GetNonWhiteSpaceStringOrNull() ??
// Fallback
id.ToString();
var guildId = json.GetProperty("guild_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
var name = json.GetProperty("name").GetNonWhiteSpaceString();
var lastMessageId = json
.GetPropertyOrNull("last_message_id")?
.GetNonWhiteSpaceStringOrNull()?
.Pipe(Snowflake.Parse);
return new ThreadChannel(
return new ChannelThread(
id,
kind,
guildId,

@ -144,7 +144,6 @@ public class DiscordClient
CancellationToken cancellationToken = default)
{
using var response = await GetResponseAsync(url, cancellationToken);
return response.IsSuccessStatusCode
? await response.Content.ReadAsJsonAsync(cancellationToken)
: null;
@ -164,7 +163,6 @@ public class DiscordClient
yield return Guild.DirectMessages;
var currentAfter = Snowflake.Zero;
while (true)
{
var url = new UrlBuilder()
@ -176,8 +174,9 @@ public class DiscordClient
var response = await GetJsonResponseAsync(url, cancellationToken);
var isEmpty = true;
foreach (var guild in response.EnumerateArray().Select(Guild.Parse))
foreach (var guildJson in response.EnumerateArray())
{
var guild = Guild.Parse(guildJson);
yield return guild;
currentAfter = guild.Id;
@ -200,35 +199,6 @@ public class DiscordClient
return Guild.Parse(response);
}
public async IAsyncEnumerable<ThreadChannel> GetGuildChannelThreadsAsync(
string channelId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
int currentOffset = 0;
while (true)
{
var url = new UrlBuilder()
.SetPath($"channels/{channelId}/threads/search")
.SetQueryParameter("offset", currentOffset.ToString())
.Build();
var response = await TryGetJsonResponseAsync(url, cancellationToken);
if (response is null)
break;
foreach (var threadJson in response.Value.GetProperty("threads").EnumerateArray())
yield return ThreadChannel.Parse(threadJson);
if (!response.Value.GetProperty("has_more").GetBoolean())
{
break;
}
currentOffset += response.Value.GetProperty("threads").GetArrayLength();
}
}
public async IAsyncEnumerable<Channel> GetGuildChannelsAsync(
Snowflake guildId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
@ -243,24 +213,26 @@ public class DiscordClient
{
var response = await GetJsonResponseAsync($"guilds/{guildId}/channels", cancellationToken);
var responseOrdered = response
var channelsJson = response
.EnumerateArray()
.OrderBy(j => j.GetProperty("position").GetInt32())
.ThenBy(j => j.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse))
.ToArray();
var categories = responseOrdered
var categories = channelsJson
.Where(j => j.GetProperty("type").GetInt32() == (int) ChannelKind.GuildCategory)
.Select((j, index) => ChannelCategory.Parse(j, index + 1))
.ToDictionary(j => j.Id.ToString(), StringComparer.Ordinal);
// Discord positions are not deterministic, so we need to normalize them
// because the user may refer to the channel position via file name template.
// Discord channel positions are relative, so we need to normalize them
// so that the user may refer to them more easily in file name templates.
var position = 0;
foreach (var channelJson in responseOrdered)
foreach (var channelJson in channelsJson)
{
var parentId = channelJson.GetPropertyOrNull("parent_id")?.GetNonWhiteSpaceStringOrNull();
var parentId = channelJson
.GetPropertyOrNull("parent_id")?
.GetNonWhiteSpaceStringOrNull();
var category = !string.IsNullOrWhiteSpace(parentId)
? categories.GetValueOrDefault(parentId)
@ -283,7 +255,6 @@ public class DiscordClient
yield break;
var response = await GetJsonResponseAsync($"guilds/{guildId}/roles", cancellationToken);
foreach (var roleJson in response.EnumerateArray())
yield return Role.Parse(roleJson);
}
@ -317,8 +288,8 @@ public class DiscordClient
var response = await GetJsonResponseAsync($"channels/{channelId}", cancellationToken);
return ChannelCategory.Parse(response);
}
// In some cases, the Discord API returns an empty body when requesting channel category.
// Instead, we use an empty channel category as a fallback.
// In some cases, the Discord API returns an empty body when requesting a channel.
// Return an empty channel category as fallback in these cases.
catch (DiscordChatExporterException)
{
return new ChannelCategory(channelId, "Unknown Category", 0);
@ -331,7 +302,10 @@ public class DiscordClient
{
var response = await GetJsonResponseAsync($"channels/{channelId}", cancellationToken);
var parentId = response.GetPropertyOrNull("parent_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse);
var parentId = response
.GetPropertyOrNull("parent_id")?
.GetNonWhiteSpaceStringOrNull()?
.Pipe(Snowflake.Parse);
var category = parentId is not null
? await GetChannelCategoryAsync(parentId.Value, cancellationToken)
@ -340,6 +314,33 @@ public class DiscordClient
return Channel.Parse(response, category);
}
public async IAsyncEnumerable<ChannelThread> GetChannelThreadsAsync(
Snowflake channelId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var currentOffset = 0;
while (true)
{
var url = new UrlBuilder()
.SetPath($"channels/{channelId}/threads/search")
.SetQueryParameter("offset", currentOffset.ToString())
.Build();
var response = await TryGetJsonResponseAsync(url, cancellationToken);
if (response is null)
break;
foreach (var threadJson in response.Value.GetProperty("threads").EnumerateArray())
{
yield return ChannelThread.Parse(threadJson);
currentOffset++;
}
if (!response.Value.GetProperty("has_more").GetBoolean())
break;
}
}
private async ValueTask<Message?> TryGetLastMessageAsync(
Snowflake channelId,
Snowflake? before = null,
@ -362,17 +363,17 @@ public class DiscordClient
IProgress<Percentage>? progress = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Get the last message in the specified range, so we can later calculate progress based on its date.
// This also snapshots the boundaries, which means that messages posted after the export started
// will not appear in the output.
// Get the last message in the specified range, so we can later calculate the
// progress based on the difference between message timestamps.
// This also snapshots the boundaries, which means that messages posted after
// the export started will not appear in the output.
var lastMessage = await TryGetLastMessageAsync(channelId, before, cancellationToken);
if (lastMessage is null || lastMessage.Timestamp < after?.ToDate())
yield break;
// Keep track of first message in range in order to calculate progress
// Keep track of the first message in range in order to calculate progress
var firstMessage = default(Message);
var currentAfter = after ?? Snowflake.Zero;
while (true)
{
var url = new UrlBuilder()
@ -386,7 +387,8 @@ public class DiscordClient
var messages = response
.EnumerateArray()
.Select(Message.Parse)
.Reverse() // reverse because messages appear newest first
// Messages are returned from newest to oldest, so we need to reverse them
.Reverse()
.ToArray();
// Break if there are no messages (can happen if messages are deleted during execution)
@ -397,11 +399,11 @@ public class DiscordClient
{
firstMessage ??= message;
// Ensure messages are in range (take into account that last message could have been deleted)
// Ensure that the messages are in range
if (message.Timestamp > lastMessage.Timestamp)
yield break;
// Report progress based on the duration of exported messages divided by total
// Report progress based on timestamps
if (progress is not null)
{
var exportedDuration = (message.Timestamp - firstMessage.Timestamp).Duration();
@ -409,7 +411,7 @@ public class DiscordClient
progress.Report(Percentage.FromFraction(
// Avoid division by zero if all messages have the exact same timestamp
// (which may be the case if there's only one message in the channel)
// (which happens when there's only one message in the channel)
totalDuration > TimeSpan.Zero
? exportedDuration / totalDuration
: 1

Loading…
Cancel
Save