From b0ee4ba6468ba195fd9f56d9fb34b946343b6f91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:50:29 +0000 Subject: [PATCH] list commands always output JSON; add CliJsonSerializerContext + SnowflakeJsonConverter in CLI Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/58698f45-e22e-4bd4-aec4-31f801051467 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Commands/ExportChannelsCommand.cs | 19 +++- .../Commands/GetChannelsCommand.cs | 95 +++---------------- .../Commands/GetDirectChannelsCommand.cs | 37 ++------ .../Commands/GetGuildsCommand.cs | 26 ++--- .../Utils/Json/CliJsonSerializerContext.cs | 26 +++++ .../Utils/Json/SnowflakeJsonConverter.cs | 21 ++++ 6 files changed, 92 insertions(+), 132 deletions(-) create mode 100644 DiscordChatExporter.Cli/Utils/Json/CliJsonSerializerContext.cs create mode 100644 DiscordChatExporter.Cli/Utils/Json/SnowflakeJsonConverter.cs diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs index 0ec06634..0e711029 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json; using System.Threading.Tasks; using CliFx; using CliFx.Binding; @@ -35,7 +36,23 @@ public partial class ExportChannelsCommand : ExportCommandBase if (channelIds.Count == 0 && console.IsInputRedirected) { await foreach (var line in console.Input.ReadLinesAsync(cancellationToken)) - channelIds.Add(Snowflake.Parse(line.Trim())); + { + var trimmed = line.Trim(); + if (string.IsNullOrEmpty(trimmed)) + continue; + + // JSON array produced by 'list channels' / 'list channels dm' + if (trimmed.StartsWith('[')) + { + using var doc = JsonDocument.Parse(trimmed); + foreach (var element in doc.RootElement.EnumerateArray()) + channelIds.Add(Snowflake.Parse(element.GetProperty("id").GetString()!)); + } + else + { + channelIds.Add(Snowflake.Parse(trimmed)); + } + } } if (channelIds.Count == 0) diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs index 10eadda3..f0c8839e 100644 --- a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -1,12 +1,13 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using CliFx.Binding; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands.Base; using DiscordChatExporter.Cli.Commands.Converters; using DiscordChatExporter.Cli.Commands.Shared; +using DiscordChatExporter.Cli.Utils.Json; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Utils.Extensions; @@ -35,6 +36,8 @@ public partial class GetChannelsCommand : DiscordCommandBase var cancellationToken = console.RegisterCancellationHandler(); + var allChannels = new List(); + foreach (var guildId in GuildIds) { var channels = (await Discord.GetGuildChannelsAsync(guildId, cancellationToken)) @@ -59,86 +62,18 @@ public partial class GetChannelsCommand : DiscordCommandBase .ToArray() : []; - // If output is redirected, print only channel IDs (one per line) for easy piping - if (console.IsOutputRedirected) + foreach (var channel in channels) { - foreach (var channel in channels) - { - await console.Output.WriteLineAsync(channel.Id.ToString()); - foreach (var channelThread in threads.Where(t => t.Parent?.Id == channel.Id)) - await console.Output.WriteLineAsync(channelThread.Id.ToString()); - } - } - else - { - // Show server header when listing multiple servers - if (GuildIds.Count > 1) - { - var guild = await Discord.GetGuildAsync(guildId, cancellationToken); - - using (console.WithForegroundColor(ConsoleColor.Cyan)) - await console.Output.WriteLineAsync($"{guild.Id} | {guild.Name}"); - } - - var channelIdMaxLength = channels - .Select(c => c.Id.ToString().Length) - .OrderDescending() - .FirstOrDefault(); - - foreach (var channel in channels) - { - // Channel ID - await console.Output.WriteAsync( - channel.Id.ToString().PadRight(channelIdMaxLength, ' ') - ); - - // Separator - using (console.WithForegroundColor(ConsoleColor.DarkGray)) - await console.Output.WriteAsync(" | "); - - // Channel name - using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteLineAsync(channel.GetHierarchicalName()); - - var channelThreads = threads.Where(t => t.Parent?.Id == channel.Id).ToArray(); - var channelThreadIdMaxLength = channelThreads - .Select(t => t.Id.ToString().Length) - .OrderDescending() - .FirstOrDefault(); - - foreach (var channelThread in channelThreads) - { - // Indent - await console.Output.WriteAsync(" * "); - - // Thread ID - await console.Output.WriteAsync( - channelThread.Id.ToString().PadRight(channelThreadIdMaxLength, ' ') - ); - - // Separator - using (console.WithForegroundColor(ConsoleColor.DarkGray)) - await console.Output.WriteAsync(" | "); - - // Thread name - using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteAsync($"Thread / {channelThread.Name}"); - - // Separator - using (console.WithForegroundColor(ConsoleColor.DarkGray)) - await console.Output.WriteAsync(" | "); - - // Thread status - using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteLineAsync( - channelThread.IsArchived ? "Archived" : "Active" - ); - } - } - - if (GuildIds.Count > 1) - await console.Output.WriteLineAsync(); + allChannels.Add(channel); + allChannels.AddRange(threads.Where(t => t.Parent?.Id == channel.Id)); } } + + await console.Output.WriteLineAsync( + JsonSerializer.Serialize( + allChannels.ToArray(), + CliJsonSerializerContext.Instance.ChannelArray + ) + ); } } diff --git a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs index e2251570..95b38a19 100644 --- a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs @@ -1,9 +1,10 @@ -using System; -using System.Linq; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using CliFx.Binding; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Cli.Utils.Json; using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Utils.Extensions; @@ -25,34 +26,8 @@ public partial class GetDirectChannelsCommand : DiscordCommandBase .ThenBy(c => c.Name) .ToArray(); - var channelIdMaxLength = channels - .Select(c => c.Id.ToString().Length) - .OrderDescending() - .FirstOrDefault(); - - // If output is redirected, print only channel IDs (one per line) for easy piping - if (console.IsOutputRedirected) - { - foreach (var channel in channels) - await console.Output.WriteLineAsync(channel.Id.ToString()); - } - else - { - foreach (var channel in channels) - { - // Channel ID - await console.Output.WriteAsync( - channel.Id.ToString().PadRight(channelIdMaxLength, ' ') - ); - - // Separator - using (console.WithForegroundColor(ConsoleColor.DarkGray)) - await console.Output.WriteAsync(" | "); - - // Channel name - using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteLineAsync(channel.GetHierarchicalName()); - } - } + await console.Output.WriteLineAsync( + JsonSerializer.Serialize(channels, CliJsonSerializerContext.Instance.ChannelArray) + ); } } diff --git a/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs b/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs index af8303d1..9b77cfd7 100644 --- a/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs @@ -1,9 +1,10 @@ -using System; -using System.Linq; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using CliFx.Binding; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Cli.Utils.Json; using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Utils.Extensions; @@ -24,23 +25,8 @@ public partial class GetGuildsCommand : DiscordCommandBase .ThenBy(g => g.Name) .ToArray(); - var guildIdMaxLength = guilds - .Select(g => g.Id.ToString().Length) - .OrderDescending() - .FirstOrDefault(); - - foreach (var guild in guilds) - { - // Guild ID - await console.Output.WriteAsync(guild.Id.ToString().PadRight(guildIdMaxLength, ' ')); - - // Separator - using (console.WithForegroundColor(ConsoleColor.DarkGray)) - await console.Output.WriteAsync(" | "); - - // Guild name - using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteLineAsync(guild.Name); - } + await console.Output.WriteLineAsync( + JsonSerializer.Serialize(guilds, CliJsonSerializerContext.Instance.GuildArray) + ); } } diff --git a/DiscordChatExporter.Cli/Utils/Json/CliJsonSerializerContext.cs b/DiscordChatExporter.Cli/Utils/Json/CliJsonSerializerContext.cs new file mode 100644 index 00000000..ac8cf31e --- /dev/null +++ b/DiscordChatExporter.Cli/Utils/Json/CliJsonSerializerContext.cs @@ -0,0 +1,26 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using DiscordChatExporter.Core.Discord.Data; + +namespace DiscordChatExporter.Cli.Utils.Json; + +[JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + GenerationMode = JsonSourceGenerationMode.Metadata +)] +[JsonSerializable(typeof(Channel[]))] +[JsonSerializable(typeof(Guild[]))] +internal partial class CliJsonSerializerContext : JsonSerializerContext +{ + // Instance pre-configured with converters for Snowflake (serialised as a string) + // and all enum types (serialised as their name). Defined here so the Core types + // are never touched. + public static CliJsonSerializerContext Instance { get; } = + new( + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new SnowflakeJsonConverter(), new JsonStringEnumConverter() }, + } + ); +} diff --git a/DiscordChatExporter.Cli/Utils/Json/SnowflakeJsonConverter.cs b/DiscordChatExporter.Cli/Utils/Json/SnowflakeJsonConverter.cs new file mode 100644 index 00000000..032012b5 --- /dev/null +++ b/DiscordChatExporter.Cli/Utils/Json/SnowflakeJsonConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using DiscordChatExporter.Core.Discord; + +namespace DiscordChatExporter.Cli.Utils.Json; + +internal class SnowflakeJsonConverter : JsonConverter +{ + public override Snowflake Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) => Snowflake.Parse(reader.GetString()!); + + public override void Write( + Utf8JsonWriter writer, + Snowflake value, + JsonSerializerOptions options + ) => writer.WriteStringValue(value.ToString()); +}