diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs index fcc95ce..eb8526d 100644 --- a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -20,6 +20,12 @@ public class GetChannelsCommand : DiscordCommandBase )] public required Snowflake GuildId { get; init; } + [CommandOption( + "include-threads", + Description = "Display threads alongside channels." + )] + public bool IncludeTHreads { get; init; } + public override async ValueTask ExecuteAsync(IConsole console) { var cancellationToken = console.RegisterCancellationHandler(); @@ -44,6 +50,32 @@ public class GetChannelsCommand : DiscordCommandBase // Channel category / name using (console.WithForegroundColor(ConsoleColor.White)) await console.Output.WriteLineAsync($"{channel.Category.Name} / {channel.Name}"); + + if (IncludeThreads) + { + var threads = (await Discord.GetGuildChannelThreadsAsync(channel.Id.ToString(), cancellationToken)) + .OrderBy(c => c.Name) + .ToArray(); + + foreach (var thread in threads) + { + // Indent + await console.Output.WriteAsync('\t'); + + // Thread ID + await console.Output.WriteAsync( + thread.Id.ToString().PadRight(18, ' ') + ); + + // Separator + using (console.WithForegroundColor(ConsoleColor.DarkGray)) + await console.Output.WriteAsync(" | "); + + // Thread / thread name + using (console.WithForegroundColor(ConsoleColor.White)) + await console.Output.WriteLineAsync($"Thread / {thread.Name}"); + } + } } } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Data/ThreadChannel.cs b/DiscordChatExporter.Core/Discord/Data/ThreadChannel.cs new file mode 100644 index 0000000..cbec3f6 --- /dev/null +++ b/DiscordChatExporter.Core/Discord/Data/ThreadChannel.cs @@ -0,0 +1,50 @@ +using System.Linq; +using System.Text.Json; +using DiscordChatExporter.Core.Discord.Data.Common; +using DiscordChatExporter.Core.Utils.Extensions; +using JsonExtensions.Reading; + +namespace DiscordChatExporter.Core.Discord.Data; + +// https://discord.com/developers/docs/resources/channel#channel-object-example-thread-channel +public partial record ThreadChannel( + Snowflake Id, + ChannelKind Kind, + Snowflake GuildId, + string Name, + Snowflake? LastMessageId) : IHasId +{ + +} + +public partial record ThreadChannel +{ + public static ThreadChannel 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 lastMessageId = json + .GetPropertyOrNull("last_message_id")? + .GetNonWhiteSpaceStringOrNull()? + .Pipe(Snowflake.Parse); + + return new ThreadChannel( + id, + kind, + guildId, + name, + lastMessageId + ); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/DiscordClient.cs b/DiscordChatExporter.Core/Discord/DiscordClient.cs index 02013f9..9988ed2 100644 --- a/DiscordChatExporter.Core/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Core/Discord/DiscordClient.cs @@ -200,6 +200,35 @@ public class DiscordClient return Guild.Parse(response); } + public async IAsyncEnumerable 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 GetGuildChannelsAsync( Snowflake guildId, [EnumeratorCancellation] CancellationToken cancellationToken = default)