From 716ea79f600d88b45420b06f241f6fb66a512c99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:36:19 +0000 Subject: [PATCH 01/23] Initial plan From efb371f09351fefcd4f68307ea62c75aeb3469eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:42:04 +0000 Subject: [PATCH 02/23] Rework CLI commands to be composable: pipe channels to export, remove exportguild/exportall/exportdm Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/5f305835-64af-456e-b0b4-6163ece1e8cf Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .docs/Using-the-CLI.md | 59 +++---- .../Commands/ExportAllCommand.cs | 156 ------------------ .../Commands/ExportChannelsCommand.cs | 30 +++- .../Commands/ExportDirectMessagesCommand.cs | 27 --- .../Commands/ExportGuildCommand.cs | 61 ------- .../Commands/GetChannelsCommand.cs | 84 ++++++---- .../Commands/GetDirectChannelsCommand.cs | 35 ++-- 7 files changed, 128 insertions(+), 324 deletions(-) delete mode 100644 DiscordChatExporter.Cli/Commands/ExportAllCommand.cs delete mode 100644 DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs delete mode 100644 DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index 36de6c95b..0a0aa0d37 100644 --- a/.docs/Using-the-CLI.md +++ b/.docs/Using-the-CLI.md @@ -33,16 +33,13 @@ Type the following command in your terminal of choice, then press ENTER to run i ## CLI commands -| Command | Description | -| ----------- | ---------------------------------------------------- | -| export | Exports a channel | -| exportdm | Exports all direct message channels | -| exportguild | Exports all channels within the specified server | -| exportall | Exports all accessible channels | -| channels | Outputs the list of channels in the given server | -| dm | Outputs the list of direct message channels | -| guilds | Outputs the list of accessible servers | -| guide | Explains how to obtain token, server, and channel ID | +| Command | Description | +| -------- | ---------------------------------------------------- | +| export | Exports one or more channels | +| channels | Outputs the list of channels in the given server | +| dm | Outputs the list of direct message channels | +| guilds | Outputs the list of accessible servers | +| guide | Explains how to obtain token, server, and channel ID | To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./DiscordChatExporter.Cli guide`. @@ -225,46 +222,38 @@ Documentation on message filter syntax can be found [here](https://github.com/Ty ### Export channels from a specific server -To export all channels in a specific server, use the `exportguild` command and provide the server ID through the `-g|--guild` option: +To export all channels in a specific server, use the `channels` command to list channels and pipe the result to `export`: ```console -./DiscordChatExporter.Cli exportguild -t "mfa.Ifrn" -g 21814 +./DiscordChatExporter.Cli channels -t "mfa.Ifrn" -g 21814 | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" ``` +> **Tip**: To avoid repeating `--token` (or `-t`) twice, set the `DISCORD_TOKEN` environment variable. + #### Including threads -By default, threads are not included in the export. You can change this behavior by using `--include-threads` and -specifying which threads should be included. It has possible values of `none`, `active`, or `all`, indicating which -threads should be included. To include both active and archived threads, use `--include-threads all`. +By default, threads are not included. You can change this behavior by passing `--include-threads` to the `channels` command. It has possible values of `none`, `active`, or `all`, indicating which threads should be included. To include both active and archived threads, use `--include-threads all`. ```console -./DiscordChatExporter.Cli exportguild -t "mfa.Ifrn" -g 21814 --include-threads all +./DiscordChatExporter.Cli channels -t "mfa.Ifrn" -g 21814 --include-threads all | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" ``` #### Including voice channels -By default, voice channels are included in the export. You can change this behavior by using `--include-vc` and -specifying whether to include voice channels in the export. It has possible values of `true` or `false`, to exclude -voice channels, use `--include-vc false`. +By default, voice channels are included. You can change this behavior by passing `--include-vc false` to the `channels` command. ```console -./DiscordChatExporter.Cli exportguild -t "mfa.Ifrn" -g 21814 --include-vc false +./DiscordChatExporter.Cli channels -t "mfa.Ifrn" -g 21814 --include-vc false | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" ``` ### Export all channels -To export all accessible channels, use the `exportall` command: - -```console -./DiscordChatExporter.Cli exportall -t "mfa.Ifrn" -``` +To export all accessible channels, first list all guilds and then pipe each guild's channels to `export`. You can also use `dm` to include direct message channels. -#### Excluding DMs - -To exclude DMs, add the `--include-dm false` option. +To export all DMs: ```console -./DiscordChatExporter.Cli exportall -t "mfa.Ifrn" --include-dm false +./DiscordChatExporter.Cli dm -t "mfa.Ifrn" | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" ``` ### List channels in a server @@ -275,6 +264,12 @@ To list the channels available in a specific server, use the `channels` command ./DiscordChatExporter.Cli channels -t "mfa.Ifrn" -g 21814 ``` +When the output is redirected or piped, the `channels` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: + +```console +./DiscordChatExporter.Cli channels -t "mfa.Ifrn" -g 21814 | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +``` + ### List direct message channels To list all DM channels accessible to the current account, use the `dm` command: @@ -283,6 +278,12 @@ To list all DM channels accessible to the current account, use the `dm` command: ./DiscordChatExporter.Cli dm -t "mfa.Ifrn" ``` +When the output is redirected or piped, the `dm` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: + +```console +./DiscordChatExporter.Cli dm -t "mfa.Ifrn" | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +``` + ### List servers To list all servers accessible by the current account, use the `guilds` command: diff --git a/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs b/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs deleted file mode 100644 index 464ecf0a8..000000000 --- a/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using CliFx.Binding; -using CliFx.Infrastructure; -using DiscordChatExporter.Cli.Commands.Base; -using DiscordChatExporter.Cli.Utils.Extensions; -using DiscordChatExporter.Core.Discord.Data; -using DiscordChatExporter.Core.Discord.Dump; -using DiscordChatExporter.Core.Exceptions; -using Spectre.Console; - -namespace DiscordChatExporter.Cli.Commands; - -[Command("exportall", Description = "Exports all accessible channels.")] -public partial class ExportAllCommand : ExportCommandBase -{ - [CommandOption("include-dm", Description = "Include direct message channels.")] - public bool IncludeDirectChannels { get; set; } = true; - - [CommandOption("include-guilds", Description = "Include server channels.")] - public bool IncludeGuildChannels { get; set; } = true; - - [CommandOption("include-vc", Description = "Include voice channels.")] - public bool IncludeVoiceChannels { get; set; } = true; - - [CommandOption( - "data-package", - Description = "Path to the personal data package (ZIP file) requested from Discord. " - + "If provided, only channels referenced in the dump will be exported." - )] - public string? DataPackageFilePath { get; set; } - - public override async ValueTask ExecuteAsync(IConsole console) - { - await base.ExecuteAsync(console); - - var cancellationToken = console.RegisterCancellationHandler(); - var channels = new List(); - - // Pull from the API - if (string.IsNullOrWhiteSpace(DataPackageFilePath)) - { - await foreach (var guild in Discord.GetUserGuildsAsync(cancellationToken)) - { - // Regular channels - await console.Output.WriteLineAsync( - $"Fetching channels for server '{guild.Name}'..." - ); - - var fetchedChannelsCount = 0; - await console - .CreateStatusTicker() - .StartAsync( - "...", - async ctx => - { - await foreach ( - var channel in Discord.GetGuildChannelsAsync( - guild.Id, - cancellationToken - ) - ) - { - if (channel.IsCategory) - continue; - - if (!IncludeVoiceChannels && channel.IsVoice) - continue; - - channels.Add(channel); - - ctx.Status( - Markup.Escape($"Fetched '{channel.GetHierarchicalName()}'.") - ); - - fetchedChannelsCount++; - } - } - ); - - await console.Output.WriteLineAsync($"Fetched {fetchedChannelsCount} channel(s)."); - } - } - // Pull from the data package - else - { - await console.Output.WriteLineAsync("Extracting channels..."); - - var dump = await DataDump.LoadAsync(DataPackageFilePath, cancellationToken); - var inaccessibleChannels = new List(); - - await console - .CreateStatusTicker() - .StartAsync( - "...", - async ctx => - { - foreach (var dumpChannel in dump.Channels) - { - ctx.Status( - Markup.Escape( - $"Fetching '{dumpChannel.Name}' ({dumpChannel.Id})..." - ) - ); - - try - { - var channel = await Discord.GetChannelAsync( - dumpChannel.Id, - cancellationToken - ); - - channels.Add(channel); - } - catch (DiscordChatExporterException) - { - inaccessibleChannels.Add(dumpChannel); - } - } - } - ); - - await console.Output.WriteLineAsync($"Fetched {channels} channel(s)."); - - // Print inaccessible channels - if (inaccessibleChannels.Any()) - { - await console.Output.WriteLineAsync(); - - using (console.WithForegroundColor(ConsoleColor.Red)) - { - await console.Error.WriteLineAsync( - "Failed to access the following channel(s):" - ); - } - - foreach (var dumpChannel in inaccessibleChannels) - await console.Error.WriteLineAsync($"{dumpChannel.Name} ({dumpChannel.Id})"); - - await console.Error.WriteLineAsync(); - } - } - - // Filter out unwanted channels - if (!IncludeDirectChannels) - channels.RemoveAll(c => c.IsDirect); - if (!IncludeGuildChannels) - channels.RemoveAll(c => c.IsGuild); - if (!IncludeVoiceChannels) - channels.RemoveAll(c => c.IsVoice); - - await ExportAsync(console, channels); - } -} diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs index 3e963cade..86d59bb88 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using CliFx; using CliFx.Binding; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands.Base; @@ -17,9 +18,11 @@ public partial class ExportChannelsCommand : ExportCommandBase "channel", 'c', Description = "Channel ID(s). " - + "If provided with category ID(s), all channels inside those categories will be exported." + + "If provided with category ID(s), all channels inside those categories will be exported. " + + "If not provided, channel IDs are read from standard input (one per line), " + + "enabling piping from the 'channels' or 'dm' commands." )] - public required IReadOnlyList ChannelIds { get; set; } + public IReadOnlyList ChannelIds { get; set; } = []; public override async ValueTask ExecuteAsync(IConsole console) { @@ -27,12 +30,33 @@ public override async ValueTask ExecuteAsync(IConsole console) var cancellationToken = console.RegisterCancellationHandler(); + // If no channel IDs were specified, read them from stdin + var channelIds = new List(ChannelIds); + if (channelIds.Count == 0 && console.IsInputRedirected) + { + string? line; + while ((line = await console.Input.ReadLineAsync()) is not null) + { + line = line.Trim(); + if (!string.IsNullOrEmpty(line)) + channelIds.Add(Snowflake.Parse(line)); + } + } + + if (channelIds.Count == 0) + { + throw new CommandException( + "No channel IDs provided. " + + "Specify channel IDs via the '--channel' option or pipe them from the 'channels' or 'dm' commands." + ); + } + await console.Output.WriteLineAsync("Resolving channel(s)..."); var channels = new List(); var channelsByGuild = new Dictionary>(); - foreach (var channelId in ChannelIds) + foreach (var channelId in channelIds) { var channel = await Discord.GetChannelAsync(channelId, cancellationToken); diff --git a/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs b/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs deleted file mode 100644 index 39f41f40e..000000000 --- a/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Binding; -using CliFx.Infrastructure; -using DiscordChatExporter.Cli.Commands.Base; -using DiscordChatExporter.Core.Discord.Data; -using DiscordChatExporter.Core.Utils.Extensions; - -namespace DiscordChatExporter.Cli.Commands; - -[Command("exportdm", Description = "Exports all direct message channels.")] -public partial class ExportDirectMessagesCommand : ExportCommandBase -{ - public override async ValueTask ExecuteAsync(IConsole console) - { - await base.ExecuteAsync(console); - - var cancellationToken = console.RegisterCancellationHandler(); - - await console.Output.WriteLineAsync("Fetching channels..."); - var channels = await Discord.GetGuildChannelsAsync( - Guild.DirectMessages.Id, - cancellationToken - ); - - await ExportAsync(console, channels); - } -} diff --git a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs deleted file mode 100644 index 037d8c58f..000000000 --- a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Binding; -using CliFx.Infrastructure; -using DiscordChatExporter.Cli.Commands.Base; -using DiscordChatExporter.Cli.Utils.Extensions; -using DiscordChatExporter.Core.Discord; -using DiscordChatExporter.Core.Discord.Data; -using Spectre.Console; - -namespace DiscordChatExporter.Cli.Commands; - -[Command("exportguild", Description = "Exports all channels within the specified server.")] -public partial class ExportGuildCommand : ExportCommandBase -{ - [CommandOption("guild", 'g', Description = "Server ID.")] - public required Snowflake GuildId { get; set; } - - [CommandOption("include-vc", Description = "Include voice channels.")] - public bool IncludeVoiceChannels { get; set; } = true; - - public override async ValueTask ExecuteAsync(IConsole console) - { - await base.ExecuteAsync(console); - - var cancellationToken = console.RegisterCancellationHandler(); - var channels = new List(); - - await console.Output.WriteLineAsync("Fetching channels..."); - - var fetchedChannelsCount = 0; - await console - .CreateStatusTicker() - .StartAsync( - "...", - async ctx => - { - await foreach ( - var channel in Discord.GetGuildChannelsAsync(GuildId, cancellationToken) - ) - { - if (channel.IsCategory) - continue; - - if (!IncludeVoiceChannels && channel.IsVoice) - continue; - - channels.Add(channel); - - ctx.Status(Markup.Escape($"Fetched '{channel.GetHierarchicalName()}'.")); - - fetchedChannelsCount++; - } - } - ); - - await console.Output.WriteLineAsync($"Fetched {fetchedChannelsCount} channel(s)."); - - await ExportAsync(console, channels); - } -} diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs index b0116ed77..b603d9cd2 100644 --- a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -60,54 +60,68 @@ await Discord.GetGuildThreadsAsync( .ToArray() : []; - foreach (var channel in channels) + // If output is redirected, print only channel IDs (one per line) for easy piping + if (console.IsOutputRedirected) { - // 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) + foreach (var channel in channels) { - // Indent - await console.Output.WriteAsync(" * "); + await console.Output.WriteLineAsync(channel.Id.ToString()); - // Thread ID + foreach (var channelThread in threads.Where(t => t.Parent?.Id == channel.Id)) + await console.Output.WriteLineAsync(channelThread.Id.ToString()); + } + } + else + { + foreach (var channel in channels) + { + // Channel ID await console.Output.WriteAsync( - channelThread.Id.ToString().PadRight(channelThreadIdMaxLength, ' ') + channel.Id.ToString().PadRight(channelIdMaxLength, ' ') ); // Separator using (console.WithForegroundColor(ConsoleColor.DarkGray)) await console.Output.WriteAsync(" | "); - // Thread name + // Channel name using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteAsync($"Thread / {channelThread.Name}"); + 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(" | "); + // 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" - ); + // 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" + ); + } } } } diff --git a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs index 06ea69b14..b395b3ccd 100644 --- a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs @@ -30,20 +30,29 @@ await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken) .OrderDescending() .FirstOrDefault(); - foreach (var channel in channels) + // If output is redirected, print only channel IDs (one per line) for easy piping + if (console.IsOutputRedirected) { - // 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()); + 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()); + } } } } From 6750195a3513360d559786a7b873bfa04de5c81c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:45:14 +0000 Subject: [PATCH 03/23] Address code review: fix Snowflake.TryParse usage and remove blank line in channels redirect block Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/5f305835-64af-456e-b0b4-6163ece1e8cf Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Commands/ExportChannelsCommand.cs | 17 +++++++++++++++-- .../Commands/GetChannelsCommand.cs | 1 - 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs index 86d59bb88..1fbb085ee 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs @@ -34,12 +34,25 @@ public override async ValueTask ExecuteAsync(IConsole console) var channelIds = new List(ChannelIds); if (channelIds.Count == 0 && console.IsInputRedirected) { + var lineNumber = 0; string? line; while ((line = await console.Input.ReadLineAsync()) is not null) { + lineNumber++; line = line.Trim(); - if (!string.IsNullOrEmpty(line)) - channelIds.Add(Snowflake.Parse(line)); + if (string.IsNullOrEmpty(line)) + continue; + + var snowflake = Snowflake.TryParse(line); + if (snowflake is null) + { + throw new CommandException( + $"Invalid channel ID on line {lineNumber}: '{line}'. " + + "Each line must contain a valid channel ID." + ); + } + + channelIds.Add(snowflake.Value); } } diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs index b603d9cd2..59b43df95 100644 --- a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -66,7 +66,6 @@ await Discord.GetGuildThreadsAsync( 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()); } From 16ecb2802acf6517b387bfe5349e940220747577 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:46:54 +0000 Subject: [PATCH 04/23] Clarify DISCORD_TOKEN env var tip in documentation Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/5f305835-64af-456e-b0b4-6163ece1e8cf Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .docs/Using-the-CLI.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index 0a0aa0d37..7da211b8a 100644 --- a/.docs/Using-the-CLI.md +++ b/.docs/Using-the-CLI.md @@ -228,7 +228,7 @@ To export all channels in a specific server, use the `channels` command to list ./DiscordChatExporter.Cli channels -t "mfa.Ifrn" -g 21814 | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" ``` -> **Tip**: To avoid repeating `--token` (or `-t`) twice, set the `DISCORD_TOKEN` environment variable. +> **Tip**: To avoid repeating `--token` (or `-t`) twice, set the `DISCORD_TOKEN` environment variable: `export DISCORD_TOKEN="mfa.Ifrn"` (Linux/macOS) or `set DISCORD_TOKEN=mfa.Ifrn` (Windows). Then you can omit `-t` from both commands. #### Including threads From 0e3b4556554d47abc6e6f85231f8e87f5d4b9bcc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:05:43 +0000 Subject: [PATCH 05/23] Restructure commands: list channels/guilds subcommands with positional parameters Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/27f23ff6-5b39-46d3-a7dc-387749ee63fa Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .docs/Using-the-CLI.md | 90 +++++----- .../Commands/ExportChannelsCommand.cs | 11 +- .../Commands/GetChannelsCommand.cs | 159 ++++++++++-------- .../Commands/GetDirectChannelsCommand.cs | 2 +- .../Commands/GetGuildsCommand.cs | 2 +- 5 files changed, 142 insertions(+), 122 deletions(-) diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index 7da211b8a..a3d626d77 100644 --- a/.docs/Using-the-CLI.md +++ b/.docs/Using-the-CLI.md @@ -33,13 +33,13 @@ Type the following command in your terminal of choice, then press ENTER to run i ## CLI commands -| Command | Description | -| -------- | ---------------------------------------------------- | -| export | Exports one or more channels | -| channels | Outputs the list of channels in the given server | -| dm | Outputs the list of direct message channels | -| guilds | Outputs the list of accessible servers | -| guide | Explains how to obtain token, server, and channel ID | +| Command | Description | +| ----------------- | ---------------------------------------------------- | +| export | Exports one or more channels | +| list channels | Outputs the list of channels in the given server(s) | +| list channels dm | Outputs the list of direct message channels | +| list guilds | Outputs the list of accessible servers | +| guide | Explains how to obtain token, server, and channel ID | To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./DiscordChatExporter.Cli guide`. @@ -57,10 +57,10 @@ For example, to figure out how to use the `export` command, run: ## Export a specific channel -You can quickly export with DCE's default settings by using just `-t token` and `-c channelid`. +You can quickly export with DCE's default settings by using just `-t token` and the channel ID as a positional argument. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 ``` #### Changing the format @@ -69,7 +69,7 @@ You can change the export format to `HtmlDark`, `HtmlLight`, `PlainText` `Json` format is `HtmlDark`. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 -f Json +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -f Json ``` #### Changing the output filename @@ -77,7 +77,7 @@ format is `HtmlDark`. You can change the filename by using `-o name.ext`. e.g. for the `HTML` format: ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 -o myserver.html +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -o myserver.html ``` #### Changing the output directory @@ -87,7 +87,7 @@ extension. If any of the folders in the path have a space in its name, escape them with quotes ("). ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 -o "C:\Discord Exports" +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports" ``` #### Changing the filename and output directory @@ -97,7 +97,7 @@ Note that the filename must have an extension, otherwise it will be considered a If any of the folders in the path have a space in its name, escape them with quotes ("). ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 -o "C:\Discord Exports\myserver.html" +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports\myserver.html" ``` #### Generating the filename and output directory dynamically @@ -105,7 +105,7 @@ If any of the folders in the path have a space in its name, escape them with quo You can use template tokens to generate the output file path based on the server and channel metadata. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 -o "C:\Discord Exports\%G\%T\%C.html" +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports\%G\%T\%C.html" ``` Assuming you are exporting a channel named `"my-channel"` in the `"Text channels"` category from a server @@ -133,13 +133,13 @@ You can use partitioning to split files after a given number of messages or file For example, a channel with 36 messages set to be partitioned every 10 messages will output 4 files. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 -p 10 +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -p 10 ``` A 45 MB channel set to be partitioned every 20 MB will output 3 files. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 -p 20mb +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -p 20mb ``` #### Downloading assets @@ -150,7 +150,7 @@ downloaded when using the plain text (TXT) export format. A folder containing the assets will be created along with the exported chat. They must be kept together. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 --media +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --media ``` #### Reusing assets @@ -159,7 +159,7 @@ Previously downloaded assets can be reused to skip redundant downloads as long a same folder. Using this option can speed up future exports. This option requires the `--media` option. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 --media --reuse-media +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --media --reuse-media ``` #### Changing the media directory @@ -168,7 +168,7 @@ By default, the media directory is created alongside the exported chat. You can providing a path that ends with a slash. All of the exported media will be stored in this directory. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 --media --media-dir "C:\Discord Media" +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --media --media-dir "C:\Discord Media" ``` #### Changing the date format @@ -177,7 +177,7 @@ You can customize how dates are formatted in the exported files by using `--loca locales. The default locale is `en-US`. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 --locale "de-DE" +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --locale "de-DE" ``` #### Date ranges @@ -186,14 +186,14 @@ locales. The default locale is `en-US`. Use `--before` to export messages sent before the provided date. E.g. messages sent before September 18th, 2019: ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 --before 2019-09-18 +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --before 2019-09-18 ``` **Messages sent after a date** Use `--after` to export messages sent after the provided date. E.g. messages sent after September 17th, 2019 11:34 PM: ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 --after "2019-09-17 23:34" +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --after "2019-09-17 23:34" ``` **Messages sent in a date range** @@ -201,7 +201,7 @@ Use `--before` and `--after` to export messages sent during the provided date ra September 17th, 2019 11:34 PM and September 18th: ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 --after "2019-09-17 23:34" --before "2019-09-18" +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --after "2019-09-17 23:34" --before "2019-09-18" ``` You can try different formats like `17-SEP-2019 11:34 PM` or even refine your ranges down to @@ -215,79 +215,83 @@ formats [here](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custo Use `--filter` to filter what messages are included in the export. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 --filter "from:Tyrrrz has:image" +./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --filter "from:Tyrrrz has:image" ``` Documentation on message filter syntax can be found [here](https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs/Message-filters.md). ### Export channels from a specific server -To export all channels in a specific server, use the `channels` command to list channels and pipe the result to `export`: +To export all channels in a specific server, use `list channels` to list channels and pipe the result to `export`: ```console -./DiscordChatExporter.Cli channels -t "mfa.Ifrn" -g 21814 | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" ``` > **Tip**: To avoid repeating `--token` (or `-t`) twice, set the `DISCORD_TOKEN` environment variable: `export DISCORD_TOKEN="mfa.Ifrn"` (Linux/macOS) or `set DISCORD_TOKEN=mfa.Ifrn` (Windows). Then you can omit `-t` from both commands. +You can also list channels for multiple guilds at once: + +```console +./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 35930 | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +``` + #### Including threads -By default, threads are not included. You can change this behavior by passing `--include-threads` to the `channels` command. It has possible values of `none`, `active`, or `all`, indicating which threads should be included. To include both active and archived threads, use `--include-threads all`. +By default, threads are not included. You can change this behavior by passing `--include-threads` to the `list channels` command. It has possible values of `none`, `active`, or `all`, indicating which threads should be included. To include both active and archived threads, use `--include-threads all`. ```console -./DiscordChatExporter.Cli channels -t "mfa.Ifrn" -g 21814 --include-threads all | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 --include-threads all | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" ``` #### Including voice channels -By default, voice channels are included. You can change this behavior by passing `--include-vc false` to the `channels` command. +By default, voice channels are included. You can change this behavior by passing `--include-vc false` to the `list channels` command. ```console -./DiscordChatExporter.Cli channels -t "mfa.Ifrn" -g 21814 --include-vc false | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 --include-vc false | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" ``` ### Export all channels -To export all accessible channels, first list all guilds and then pipe each guild's channels to `export`. You can also use `dm` to include direct message channels. - To export all DMs: ```console -./DiscordChatExporter.Cli dm -t "mfa.Ifrn" | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +./DiscordChatExporter.Cli list channels dm -t "mfa.Ifrn" | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" ``` ### List channels in a server -To list the channels available in a specific server, use the `channels` command and provide the server ID through the `-g|--guild` option: +To list the channels available in a specific server, use the `list channels` command and provide the server ID as an argument: ```console -./DiscordChatExporter.Cli channels -t "mfa.Ifrn" -g 21814 +./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 ``` -When the output is redirected or piped, the `channels` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: +When the output is redirected or piped, the `list channels` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: ```console -./DiscordChatExporter.Cli channels -t "mfa.Ifrn" -g 21814 | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" ``` ### List direct message channels -To list all DM channels accessible to the current account, use the `dm` command: +To list all DM channels accessible to the current account, use the `list channels dm` command: ```console -./DiscordChatExporter.Cli dm -t "mfa.Ifrn" +./DiscordChatExporter.Cli list channels dm -t "mfa.Ifrn" ``` -When the output is redirected or piped, the `dm` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: +When the output is redirected or piped, the `list channels dm` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: ```console -./DiscordChatExporter.Cli dm -t "mfa.Ifrn" | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +./DiscordChatExporter.Cli list channels dm -t "mfa.Ifrn" | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" ``` ### List servers -To list all servers accessible by the current account, use the `guilds` command: +To list all servers accessible by the current account, use the `list guilds` command: ```console -./DiscordChatExporter.Cli guilds -t "mfa.Ifrn" > C:\path\to\output.txt +./DiscordChatExporter.Cli list guilds -t "mfa.Ifrn" > C:\path\to\output.txt ``` diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs index 1fbb085ee..fc5f46289 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs @@ -13,14 +13,13 @@ namespace DiscordChatExporter.Cli.Commands; [Command("export", Description = "Exports one or multiple channels.")] public partial class ExportChannelsCommand : ExportCommandBase { - // TODO: change this to plural (breaking change) - [CommandOption( - "channel", - 'c', + [CommandParameter( + 0, + Name = "channel-ids", Description = "Channel ID(s). " + "If provided with category ID(s), all channels inside those categories will be exported. " + "If not provided, channel IDs are read from standard input (one per line), " - + "enabling piping from the 'channels' or 'dm' commands." + + "enabling piping from the 'list channels' or 'list channels dm' commands." )] public IReadOnlyList ChannelIds { get; set; } = []; @@ -60,7 +59,7 @@ public override async ValueTask ExecuteAsync(IConsole console) { throw new CommandException( "No channel IDs provided. " - + "Specify channel IDs via the '--channel' option or pipe them from the 'channels' or 'dm' commands." + + "Specify channel IDs as arguments or pipe them from the 'list channels' or 'list channels dm' commands." ); } diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs index 59b43df95..97559cddc 100644 --- a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using CliFx.Binding; @@ -7,15 +8,16 @@ using DiscordChatExporter.Cli.Commands.Converters; using DiscordChatExporter.Cli.Commands.Shared; using DiscordChatExporter.Core.Discord; +using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Utils.Extensions; namespace DiscordChatExporter.Cli.Commands; -[Command("channels", Description = "Get the list of channels in a server.")] +[Command("list channels", Description = "Gets the list of channels in one or more servers.")] public partial class GetChannelsCommand : DiscordCommandBase { - [CommandOption("guild", 'g', Description = "Server ID.")] - public required Snowflake GuildId { get; set; } + [CommandParameter(0, Name = "guild-ids", Description = "Server ID(s).")] + public required IReadOnlyList GuildIds { get; set; } [CommandOption("include-vc", Description = "Include voice channels.")] public bool IncludeVoiceChannels { get; set; } = true; @@ -33,94 +35,109 @@ public override async ValueTask ExecuteAsync(IConsole console) var cancellationToken = console.RegisterCancellationHandler(); - var channels = (await Discord.GetGuildChannelsAsync(GuildId, cancellationToken)) - .Where(c => !c.IsCategory) - .Where(c => IncludeVoiceChannels || !c.IsVoice) - .OrderBy(c => c.Parent?.Position) - .ThenBy(c => c.Name) - .ToArray(); - - var channelIdMaxLength = channels - .Select(c => c.Id.ToString().Length) - .OrderDescending() - .FirstOrDefault(); - - var threads = - ThreadInclusionMode != ThreadInclusionMode.None - ? ( - await Discord.GetGuildThreadsAsync( - GuildId, - ThreadInclusionMode == ThreadInclusionMode.All, - null, - null, - cancellationToken + foreach (var guildId in GuildIds) + { + var channels = (await Discord.GetGuildChannelsAsync(guildId, cancellationToken)) + .Where(c => !c.IsCategory) + .Where(c => IncludeVoiceChannels || !c.IsVoice) + .OrderBy(c => c.Parent?.Position) + .ThenBy(c => c.Name) + .ToArray(); + + var threads = + ThreadInclusionMode != ThreadInclusionMode.None + ? ( + await Discord.GetGuildThreadsAsync( + guildId, + ThreadInclusionMode == ThreadInclusionMode.All, + null, + null, + cancellationToken + ) ) - ) - .OrderBy(c => c.Name) - .ToArray() - : []; + .OrderBy(c => c.Name) + .ToArray() + : []; - // If output is redirected, print only channel IDs (one per line) for easy piping - if (console.IsOutputRedirected) - { - foreach (var channel in channels) + // If output is redirected, print only channel IDs (one per line) for easy piping + if (console.IsOutputRedirected) { - 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()); + 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 - { - foreach (var channel in channels) + else { - // 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) + // Show guild header when listing multiple guilds + 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 channelThread in channelThreads) + foreach (var channel in channels) { - // Indent - await console.Output.WriteAsync(" * "); - - // Thread ID + // Channel ID await console.Output.WriteAsync( - channelThread.Id.ToString().PadRight(channelThreadIdMaxLength, ' ') + channel.Id.ToString().PadRight(channelIdMaxLength, ' ') ); // Separator using (console.WithForegroundColor(ConsoleColor.DarkGray)) await console.Output.WriteAsync(" | "); - // Thread name + // Channel name using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteAsync($"Thread / {channelThread.Name}"); + 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(" | "); + // 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" - ); + // 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(); } } } diff --git a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs index b395b3ccd..e2251570d 100644 --- a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs @@ -9,7 +9,7 @@ namespace DiscordChatExporter.Cli.Commands; -[Command("dm", Description = "Gets the list of all direct message channels.")] +[Command("list channels dm", Description = "Gets the list of direct message channels.")] public partial class GetDirectChannelsCommand : DiscordCommandBase { public override async ValueTask ExecuteAsync(IConsole console) diff --git a/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs b/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs index 6d8b5410e..c089fe29d 100644 --- a/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs @@ -9,7 +9,7 @@ namespace DiscordChatExporter.Cli.Commands; -[Command("guilds", Description = "Gets the list of accessible servers.")] +[Command("list guilds", Description = "Gets the list of accessible servers.")] public partial class GetGuildsCommand : DiscordCommandBase { public override async ValueTask ExecuteAsync(IConsole console) From f3638ecbddbee7bd959bd8b00413517741719eee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:10:46 +0000 Subject: [PATCH 06/23] Add stub 'list' command that errors with help text when executed Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/b148b0cc-7ec8-4fa1-bbd6-36dfd116ceec Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- DiscordChatExporter.Cli/Commands/ListCommand.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 DiscordChatExporter.Cli/Commands/ListCommand.cs diff --git a/DiscordChatExporter.Cli/Commands/ListCommand.cs b/DiscordChatExporter.Cli/Commands/ListCommand.cs new file mode 100644 index 000000000..3d8e154e6 --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/ListCommand.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using CliFx; +using CliFx.Binding; +using CliFx.Infrastructure; + +namespace DiscordChatExporter.Cli.Commands; + +[Command("list", Description = "Lists channels, DMs, or servers.")] +public partial class ListCommand : ICommand +{ + public ValueTask ExecuteAsync(IConsole console) => + throw new CommandException( + "A subcommand is required ('channels', 'channels dm', or 'guilds').", + showHelp: true + ); +} From 55c06ef40e292b82a31b80003f1976c5974f0f98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:13:58 +0000 Subject: [PATCH 07/23] Add dce.sh/dce.bat alias scripts and update docs with inline DISCORD_TOKEN pipeline examples Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/98d87128-db13-4085-84c3-6290d3faffca Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .docs/Using-the-CLI.md | 36 +++++++++++++------ .../DiscordChatExporter.Cli.csproj | 13 +++++++ DiscordChatExporter.Cli/dce.bat | 2 ++ DiscordChatExporter.Cli/dce.sh | 2 ++ 4 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 DiscordChatExporter.Cli/dce.bat create mode 100644 DiscordChatExporter.Cli/dce.sh diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index a3d626d77..ed1e86836 100644 --- a/.docs/Using-the-CLI.md +++ b/.docs/Using-the-CLI.md @@ -222,18 +222,25 @@ Documentation on message filter syntax can be found [here](https://github.com/Ty ### Export channels from a specific server -To export all channels in a specific server, use `list channels` to list channels and pipe the result to `export`: +To export all channels in a specific server, use `list channels` to list channels and pipe the result to `export`. + +**Linux/macOS** (one-liner, token set inline): ```console -./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 | ./DiscordChatExporter.Cli export ``` -> **Tip**: To avoid repeating `--token` (or `-t`) twice, set the `DISCORD_TOKEN` environment variable: `export DISCORD_TOKEN="mfa.Ifrn"` (Linux/macOS) or `set DISCORD_TOKEN=mfa.Ifrn` (Windows). Then you can omit `-t` from both commands. +**Windows**: + +```console +set DISCORD_TOKEN=mfa.Ifrn +DiscordChatExporter.Cli list channels 21814 | DiscordChatExporter.Cli export +``` You can also list channels for multiple guilds at once: ```console -./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 35930 | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 35930 | ./DiscordChatExporter.Cli export ``` #### Including threads @@ -241,7 +248,7 @@ You can also list channels for multiple guilds at once: By default, threads are not included. You can change this behavior by passing `--include-threads` to the `list channels` command. It has possible values of `none`, `active`, or `all`, indicating which threads should be included. To include both active and archived threads, use `--include-threads all`. ```console -./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 --include-threads all | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 --include-threads all | ./DiscordChatExporter.Cli export ``` #### Including voice channels @@ -249,15 +256,24 @@ By default, threads are not included. You can change this behavior by passing `- By default, voice channels are included. You can change this behavior by passing `--include-vc false` to the `list channels` command. ```console -./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 --include-vc false | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 --include-vc false | ./DiscordChatExporter.Cli export ``` -### Export all channels +### Export all DMs To export all DMs: +**Linux/macOS**: + +```console +DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels dm | ./DiscordChatExporter.Cli export +``` + +**Windows**: + ```console -./DiscordChatExporter.Cli list channels dm -t "mfa.Ifrn" | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +set DISCORD_TOKEN=mfa.Ifrn +DiscordChatExporter.Cli list channels dm | DiscordChatExporter.Cli export ``` ### List channels in a server @@ -271,7 +287,7 @@ To list the channels available in a specific server, use the `list channels` com When the output is redirected or piped, the `list channels` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: ```console -./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 | ./DiscordChatExporter.Cli export ``` ### List direct message channels @@ -285,7 +301,7 @@ To list all DM channels accessible to the current account, use the `list channel When the output is redirected or piped, the `list channels dm` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: ```console -./DiscordChatExporter.Cli list channels dm -t "mfa.Ifrn" | ./DiscordChatExporter.Cli export -t "mfa.Ifrn" +DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels dm | ./DiscordChatExporter.Cli export ``` ### List servers diff --git a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj index 9e4b02c88..f6650ab96 100644 --- a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj +++ b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj @@ -17,4 +17,17 @@ + + + + + diff --git a/DiscordChatExporter.Cli/dce.bat b/DiscordChatExporter.Cli/dce.bat new file mode 100644 index 000000000..0db4aa975 --- /dev/null +++ b/DiscordChatExporter.Cli/dce.bat @@ -0,0 +1,2 @@ +@echo off +"%~dp0DiscordChatExporter.Cli.exe" %* diff --git a/DiscordChatExporter.Cli/dce.sh b/DiscordChatExporter.Cli/dce.sh new file mode 100644 index 000000000..4665f18ec --- /dev/null +++ b/DiscordChatExporter.Cli/dce.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +exec "$(dirname "$0")/DiscordChatExporter.Cli" "$@" From 5c86beb80923aeb0581eeaa69bb58f438fe48158 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:48:36 +0000 Subject: [PATCH 08/23] Update ListCommand error message and use dce alias throughout CLI docs Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/b521468e-a2a4-4360-ab0c-847ef5e4d366 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .docs/Using-the-CLI.md | 66 +++++++++---------- .../Commands/ListCommand.cs | 5 +- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index ed1e86836..9e95e01bf 100644 --- a/.docs/Using-the-CLI.md +++ b/.docs/Using-the-CLI.md @@ -23,11 +23,11 @@ Now we're ready to run the commands. Type the following command in your terminal of choice, then press ENTER to run it. This will list all available subcommands and options. ```console -./DiscordChatExporter.Cli +./dce.sh ``` > **Note**: -> On Windows, if you're using the default Command Prompt (`cmd`), omit the leading `./` at the start of the command. +> On Windows, use `dce.bat` instead of `./dce.sh`. > **Docker** users, please refer to the [Docker usage instructions](Docker.md). @@ -41,18 +41,18 @@ Type the following command in your terminal of choice, then press ENTER to run i | list guilds | Outputs the list of accessible servers | | guide | Explains how to obtain token, server, and channel ID | -To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./DiscordChatExporter.Cli guide`. +To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./dce.sh guide`. To get help with a specific command, run: ```console -./DiscordChatExporter.Cli command --help +./dce.sh command --help ``` For example, to figure out how to use the `export` command, run: ```console -./DiscordChatExporter.Cli export --help +./dce.sh export --help ``` ## Export a specific channel @@ -60,7 +60,7 @@ For example, to figure out how to use the `export` command, run: You can quickly export with DCE's default settings by using just `-t token` and the channel ID as a positional argument. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 +./dce.sh export -t "mfa.Ifrn" 53555 ``` #### Changing the format @@ -69,7 +69,7 @@ You can change the export format to `HtmlDark`, `HtmlLight`, `PlainText` `Json` format is `HtmlDark`. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -f Json +./dce.sh export -t "mfa.Ifrn" 53555 -f Json ``` #### Changing the output filename @@ -77,7 +77,7 @@ format is `HtmlDark`. You can change the filename by using `-o name.ext`. e.g. for the `HTML` format: ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -o myserver.html +./dce.sh export -t "mfa.Ifrn" 53555 -o myserver.html ``` #### Changing the output directory @@ -87,7 +87,7 @@ extension. If any of the folders in the path have a space in its name, escape them with quotes ("). ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports" +./dce.sh export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports" ``` #### Changing the filename and output directory @@ -97,7 +97,7 @@ Note that the filename must have an extension, otherwise it will be considered a If any of the folders in the path have a space in its name, escape them with quotes ("). ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports\myserver.html" +./dce.sh export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports\myserver.html" ``` #### Generating the filename and output directory dynamically @@ -105,7 +105,7 @@ If any of the folders in the path have a space in its name, escape them with quo You can use template tokens to generate the output file path based on the server and channel metadata. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports\%G\%T\%C.html" +./dce.sh export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports\%G\%T\%C.html" ``` Assuming you are exporting a channel named `"my-channel"` in the `"Text channels"` category from a server @@ -133,13 +133,13 @@ You can use partitioning to split files after a given number of messages or file For example, a channel with 36 messages set to be partitioned every 10 messages will output 4 files. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -p 10 +./dce.sh export -t "mfa.Ifrn" 53555 -p 10 ``` A 45 MB channel set to be partitioned every 20 MB will output 3 files. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 -p 20mb +./dce.sh export -t "mfa.Ifrn" 53555 -p 20mb ``` #### Downloading assets @@ -150,7 +150,7 @@ downloaded when using the plain text (TXT) export format. A folder containing the assets will be created along with the exported chat. They must be kept together. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --media +./dce.sh export -t "mfa.Ifrn" 53555 --media ``` #### Reusing assets @@ -159,7 +159,7 @@ Previously downloaded assets can be reused to skip redundant downloads as long a same folder. Using this option can speed up future exports. This option requires the `--media` option. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --media --reuse-media +./dce.sh export -t "mfa.Ifrn" 53555 --media --reuse-media ``` #### Changing the media directory @@ -168,7 +168,7 @@ By default, the media directory is created alongside the exported chat. You can providing a path that ends with a slash. All of the exported media will be stored in this directory. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --media --media-dir "C:\Discord Media" +./dce.sh export -t "mfa.Ifrn" 53555 --media --media-dir "C:\Discord Media" ``` #### Changing the date format @@ -177,7 +177,7 @@ You can customize how dates are formatted in the exported files by using `--loca locales. The default locale is `en-US`. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --locale "de-DE" +./dce.sh export -t "mfa.Ifrn" 53555 --locale "de-DE" ``` #### Date ranges @@ -186,14 +186,14 @@ locales. The default locale is `en-US`. Use `--before` to export messages sent before the provided date. E.g. messages sent before September 18th, 2019: ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --before 2019-09-18 +./dce.sh export -t "mfa.Ifrn" 53555 --before 2019-09-18 ``` **Messages sent after a date** Use `--after` to export messages sent after the provided date. E.g. messages sent after September 17th, 2019 11:34 PM: ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --after "2019-09-17 23:34" +./dce.sh export -t "mfa.Ifrn" 53555 --after "2019-09-17 23:34" ``` **Messages sent in a date range** @@ -201,7 +201,7 @@ Use `--before` and `--after` to export messages sent during the provided date ra September 17th, 2019 11:34 PM and September 18th: ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --after "2019-09-17 23:34" --before "2019-09-18" +./dce.sh export -t "mfa.Ifrn" 53555 --after "2019-09-17 23:34" --before "2019-09-18" ``` You can try different formats like `17-SEP-2019 11:34 PM` or even refine your ranges down to @@ -215,7 +215,7 @@ formats [here](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custo Use `--filter` to filter what messages are included in the export. ```console -./DiscordChatExporter.Cli export -t "mfa.Ifrn" 53555 --filter "from:Tyrrrz has:image" +./dce.sh export -t "mfa.Ifrn" 53555 --filter "from:Tyrrrz has:image" ``` Documentation on message filter syntax can be found [here](https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs/Message-filters.md). @@ -227,20 +227,20 @@ To export all channels in a specific server, use `list channels` to list channel **Linux/macOS** (one-liner, token set inline): ```console -DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 | ./DiscordChatExporter.Cli export +DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 | ./dce.sh export ``` **Windows**: ```console set DISCORD_TOKEN=mfa.Ifrn -DiscordChatExporter.Cli list channels 21814 | DiscordChatExporter.Cli export +dce.bat list channels 21814 | dce.bat export ``` You can also list channels for multiple guilds at once: ```console -DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 35930 | ./DiscordChatExporter.Cli export +DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 35930 | ./dce.sh export ``` #### Including threads @@ -248,7 +248,7 @@ DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 35930 | . By default, threads are not included. You can change this behavior by passing `--include-threads` to the `list channels` command. It has possible values of `none`, `active`, or `all`, indicating which threads should be included. To include both active and archived threads, use `--include-threads all`. ```console -DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 --include-threads all | ./DiscordChatExporter.Cli export +DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 --include-threads all | ./dce.sh export ``` #### Including voice channels @@ -256,7 +256,7 @@ DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 --include By default, voice channels are included. You can change this behavior by passing `--include-vc false` to the `list channels` command. ```console -DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 --include-vc false | ./DiscordChatExporter.Cli export +DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 --include-vc false | ./dce.sh export ``` ### Export all DMs @@ -266,14 +266,14 @@ To export all DMs: **Linux/macOS**: ```console -DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels dm | ./DiscordChatExporter.Cli export +DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels dm | ./dce.sh export ``` **Windows**: ```console set DISCORD_TOKEN=mfa.Ifrn -DiscordChatExporter.Cli list channels dm | DiscordChatExporter.Cli export +dce.bat list channels dm | dce.bat export ``` ### List channels in a server @@ -281,13 +281,13 @@ DiscordChatExporter.Cli list channels dm | DiscordChatExporter.Cli export To list the channels available in a specific server, use the `list channels` command and provide the server ID as an argument: ```console -./DiscordChatExporter.Cli list channels -t "mfa.Ifrn" 21814 +./dce.sh list channels -t "mfa.Ifrn" 21814 ``` When the output is redirected or piped, the `list channels` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: ```console -DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 | ./DiscordChatExporter.Cli export +DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 | ./dce.sh export ``` ### List direct message channels @@ -295,13 +295,13 @@ DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels 21814 | ./Disco To list all DM channels accessible to the current account, use the `list channels dm` command: ```console -./DiscordChatExporter.Cli list channels dm -t "mfa.Ifrn" +./dce.sh list channels dm -t "mfa.Ifrn" ``` When the output is redirected or piped, the `list channels dm` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: ```console -DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels dm | ./DiscordChatExporter.Cli export +DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels dm | ./dce.sh export ``` ### List servers @@ -309,5 +309,5 @@ DISCORD_TOKEN="mfa.Ifrn" ./DiscordChatExporter.Cli list channels dm | ./DiscordC To list all servers accessible by the current account, use the `list guilds` command: ```console -./DiscordChatExporter.Cli list guilds -t "mfa.Ifrn" > C:\path\to\output.txt +./dce.sh list guilds -t "mfa.Ifrn" > C:\path\to\output.txt ``` diff --git a/DiscordChatExporter.Cli/Commands/ListCommand.cs b/DiscordChatExporter.Cli/Commands/ListCommand.cs index 3d8e154e6..719084a48 100644 --- a/DiscordChatExporter.Cli/Commands/ListCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ListCommand.cs @@ -9,8 +9,5 @@ namespace DiscordChatExporter.Cli.Commands; public partial class ListCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => - throw new CommandException( - "A subcommand is required ('channels', 'channels dm', or 'guilds').", - showHelp: true - ); + throw new CommandException("Use one of the named commands listed below.", showHelp: true); } From a31629f7221209bde726f29949b069dac8c7dab0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:52:29 +0000 Subject: [PATCH 09/23] Fix docs: put positional params before options per CliFx convention Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/41047efd-2bac-4c7a-8b26-42d9a4383281 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .docs/Using-the-CLI.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index 9e95e01bf..ef98b05da 100644 --- a/.docs/Using-the-CLI.md +++ b/.docs/Using-the-CLI.md @@ -57,10 +57,10 @@ For example, to figure out how to use the `export` command, run: ## Export a specific channel -You can quickly export with DCE's default settings by using just `-t token` and the channel ID as a positional argument. +You can quickly export with DCE's default settings by providing the channel ID as a positional argument and `-t token`. ```console -./dce.sh export -t "mfa.Ifrn" 53555 +./dce.sh export 53555 -t "mfa.Ifrn" ``` #### Changing the format @@ -69,7 +69,7 @@ You can change the export format to `HtmlDark`, `HtmlLight`, `PlainText` `Json` format is `HtmlDark`. ```console -./dce.sh export -t "mfa.Ifrn" 53555 -f Json +./dce.sh export 53555 -t "mfa.Ifrn" -f Json ``` #### Changing the output filename @@ -77,7 +77,7 @@ format is `HtmlDark`. You can change the filename by using `-o name.ext`. e.g. for the `HTML` format: ```console -./dce.sh export -t "mfa.Ifrn" 53555 -o myserver.html +./dce.sh export 53555 -t "mfa.Ifrn" -o myserver.html ``` #### Changing the output directory @@ -87,7 +87,7 @@ extension. If any of the folders in the path have a space in its name, escape them with quotes ("). ```console -./dce.sh export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports" +./dce.sh export 53555 -t "mfa.Ifrn" -o "C:\Discord Exports" ``` #### Changing the filename and output directory @@ -97,7 +97,7 @@ Note that the filename must have an extension, otherwise it will be considered a If any of the folders in the path have a space in its name, escape them with quotes ("). ```console -./dce.sh export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports\myserver.html" +./dce.sh export 53555 -t "mfa.Ifrn" -o "C:\Discord Exports\myserver.html" ``` #### Generating the filename and output directory dynamically @@ -105,7 +105,7 @@ If any of the folders in the path have a space in its name, escape them with quo You can use template tokens to generate the output file path based on the server and channel metadata. ```console -./dce.sh export -t "mfa.Ifrn" 53555 -o "C:\Discord Exports\%G\%T\%C.html" +./dce.sh export 53555 -t "mfa.Ifrn" -o "C:\Discord Exports\%G\%T\%C.html" ``` Assuming you are exporting a channel named `"my-channel"` in the `"Text channels"` category from a server @@ -133,13 +133,13 @@ You can use partitioning to split files after a given number of messages or file For example, a channel with 36 messages set to be partitioned every 10 messages will output 4 files. ```console -./dce.sh export -t "mfa.Ifrn" 53555 -p 10 +./dce.sh export 53555 -t "mfa.Ifrn" -p 10 ``` A 45 MB channel set to be partitioned every 20 MB will output 3 files. ```console -./dce.sh export -t "mfa.Ifrn" 53555 -p 20mb +./dce.sh export 53555 -t "mfa.Ifrn" -p 20mb ``` #### Downloading assets @@ -150,7 +150,7 @@ downloaded when using the plain text (TXT) export format. A folder containing the assets will be created along with the exported chat. They must be kept together. ```console -./dce.sh export -t "mfa.Ifrn" 53555 --media +./dce.sh export 53555 -t "mfa.Ifrn" --media ``` #### Reusing assets @@ -159,7 +159,7 @@ Previously downloaded assets can be reused to skip redundant downloads as long a same folder. Using this option can speed up future exports. This option requires the `--media` option. ```console -./dce.sh export -t "mfa.Ifrn" 53555 --media --reuse-media +./dce.sh export 53555 -t "mfa.Ifrn" --media --reuse-media ``` #### Changing the media directory @@ -168,7 +168,7 @@ By default, the media directory is created alongside the exported chat. You can providing a path that ends with a slash. All of the exported media will be stored in this directory. ```console -./dce.sh export -t "mfa.Ifrn" 53555 --media --media-dir "C:\Discord Media" +./dce.sh export 53555 -t "mfa.Ifrn" --media --media-dir "C:\Discord Media" ``` #### Changing the date format @@ -177,7 +177,7 @@ You can customize how dates are formatted in the exported files by using `--loca locales. The default locale is `en-US`. ```console -./dce.sh export -t "mfa.Ifrn" 53555 --locale "de-DE" +./dce.sh export 53555 -t "mfa.Ifrn" --locale "de-DE" ``` #### Date ranges @@ -186,14 +186,14 @@ locales. The default locale is `en-US`. Use `--before` to export messages sent before the provided date. E.g. messages sent before September 18th, 2019: ```console -./dce.sh export -t "mfa.Ifrn" 53555 --before 2019-09-18 +./dce.sh export 53555 -t "mfa.Ifrn" --before 2019-09-18 ``` **Messages sent after a date** Use `--after` to export messages sent after the provided date. E.g. messages sent after September 17th, 2019 11:34 PM: ```console -./dce.sh export -t "mfa.Ifrn" 53555 --after "2019-09-17 23:34" +./dce.sh export 53555 -t "mfa.Ifrn" --after "2019-09-17 23:34" ``` **Messages sent in a date range** @@ -201,7 +201,7 @@ Use `--before` and `--after` to export messages sent during the provided date ra September 17th, 2019 11:34 PM and September 18th: ```console -./dce.sh export -t "mfa.Ifrn" 53555 --after "2019-09-17 23:34" --before "2019-09-18" +./dce.sh export 53555 -t "mfa.Ifrn" --after "2019-09-17 23:34" --before "2019-09-18" ``` You can try different formats like `17-SEP-2019 11:34 PM` or even refine your ranges down to @@ -215,7 +215,7 @@ formats [here](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custo Use `--filter` to filter what messages are included in the export. ```console -./dce.sh export -t "mfa.Ifrn" 53555 --filter "from:Tyrrrz has:image" +./dce.sh export 53555 -t "mfa.Ifrn" --filter "from:Tyrrrz has:image" ``` Documentation on message filter syntax can be found [here](https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs/Message-filters.md). @@ -281,7 +281,7 @@ dce.bat list channels dm | dce.bat export To list the channels available in a specific server, use the `list channels` command and provide the server ID as an argument: ```console -./dce.sh list channels -t "mfa.Ifrn" 21814 +./dce.sh list channels 21814 -t "mfa.Ifrn" ``` When the output is redirected or piped, the `list channels` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: From 1087173fefaaecfd2b46889d25e92446fc1aea4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:02:36 +0000 Subject: [PATCH 10/23] Rename dce.sh to dce and update docs to use dce/dce.bat without extensions Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/c1f3a4c9-8af4-4b11-a961-442363e2b7be Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .docs/Using-the-CLI.md | 66 +++++++++---------- .../DiscordChatExporter.Cli.csproj | 2 +- DiscordChatExporter.Cli/{dce.sh => dce} | 0 3 files changed, 34 insertions(+), 34 deletions(-) rename DiscordChatExporter.Cli/{dce.sh => dce} (100%) diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index ef98b05da..23f20c47e 100644 --- a/.docs/Using-the-CLI.md +++ b/.docs/Using-the-CLI.md @@ -23,11 +23,11 @@ Now we're ready to run the commands. Type the following command in your terminal of choice, then press ENTER to run it. This will list all available subcommands and options. ```console -./dce.sh +./dce ``` > **Note**: -> On Windows, use `dce.bat` instead of `./dce.sh`. +> On Windows, use `dce` instead of `./dce`. > **Docker** users, please refer to the [Docker usage instructions](Docker.md). @@ -41,18 +41,18 @@ Type the following command in your terminal of choice, then press ENTER to run i | list guilds | Outputs the list of accessible servers | | guide | Explains how to obtain token, server, and channel ID | -To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./dce.sh guide`. +To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./dce guide`. To get help with a specific command, run: ```console -./dce.sh command --help +./dce command --help ``` For example, to figure out how to use the `export` command, run: ```console -./dce.sh export --help +./dce export --help ``` ## Export a specific channel @@ -60,7 +60,7 @@ For example, to figure out how to use the `export` command, run: You can quickly export with DCE's default settings by providing the channel ID as a positional argument and `-t token`. ```console -./dce.sh export 53555 -t "mfa.Ifrn" +./dce export 53555 -t "mfa.Ifrn" ``` #### Changing the format @@ -69,7 +69,7 @@ You can change the export format to `HtmlDark`, `HtmlLight`, `PlainText` `Json` format is `HtmlDark`. ```console -./dce.sh export 53555 -t "mfa.Ifrn" -f Json +./dce export 53555 -t "mfa.Ifrn" -f Json ``` #### Changing the output filename @@ -77,7 +77,7 @@ format is `HtmlDark`. You can change the filename by using `-o name.ext`. e.g. for the `HTML` format: ```console -./dce.sh export 53555 -t "mfa.Ifrn" -o myserver.html +./dce export 53555 -t "mfa.Ifrn" -o myserver.html ``` #### Changing the output directory @@ -87,7 +87,7 @@ extension. If any of the folders in the path have a space in its name, escape them with quotes ("). ```console -./dce.sh export 53555 -t "mfa.Ifrn" -o "C:\Discord Exports" +./dce export 53555 -t "mfa.Ifrn" -o "C:\Discord Exports" ``` #### Changing the filename and output directory @@ -97,7 +97,7 @@ Note that the filename must have an extension, otherwise it will be considered a If any of the folders in the path have a space in its name, escape them with quotes ("). ```console -./dce.sh export 53555 -t "mfa.Ifrn" -o "C:\Discord Exports\myserver.html" +./dce export 53555 -t "mfa.Ifrn" -o "C:\Discord Exports\myserver.html" ``` #### Generating the filename and output directory dynamically @@ -105,7 +105,7 @@ If any of the folders in the path have a space in its name, escape them with quo You can use template tokens to generate the output file path based on the server and channel metadata. ```console -./dce.sh export 53555 -t "mfa.Ifrn" -o "C:\Discord Exports\%G\%T\%C.html" +./dce export 53555 -t "mfa.Ifrn" -o "C:\Discord Exports\%G\%T\%C.html" ``` Assuming you are exporting a channel named `"my-channel"` in the `"Text channels"` category from a server @@ -133,13 +133,13 @@ You can use partitioning to split files after a given number of messages or file For example, a channel with 36 messages set to be partitioned every 10 messages will output 4 files. ```console -./dce.sh export 53555 -t "mfa.Ifrn" -p 10 +./dce export 53555 -t "mfa.Ifrn" -p 10 ``` A 45 MB channel set to be partitioned every 20 MB will output 3 files. ```console -./dce.sh export 53555 -t "mfa.Ifrn" -p 20mb +./dce export 53555 -t "mfa.Ifrn" -p 20mb ``` #### Downloading assets @@ -150,7 +150,7 @@ downloaded when using the plain text (TXT) export format. A folder containing the assets will be created along with the exported chat. They must be kept together. ```console -./dce.sh export 53555 -t "mfa.Ifrn" --media +./dce export 53555 -t "mfa.Ifrn" --media ``` #### Reusing assets @@ -159,7 +159,7 @@ Previously downloaded assets can be reused to skip redundant downloads as long a same folder. Using this option can speed up future exports. This option requires the `--media` option. ```console -./dce.sh export 53555 -t "mfa.Ifrn" --media --reuse-media +./dce export 53555 -t "mfa.Ifrn" --media --reuse-media ``` #### Changing the media directory @@ -168,7 +168,7 @@ By default, the media directory is created alongside the exported chat. You can providing a path that ends with a slash. All of the exported media will be stored in this directory. ```console -./dce.sh export 53555 -t "mfa.Ifrn" --media --media-dir "C:\Discord Media" +./dce export 53555 -t "mfa.Ifrn" --media --media-dir "C:\Discord Media" ``` #### Changing the date format @@ -177,7 +177,7 @@ You can customize how dates are formatted in the exported files by using `--loca locales. The default locale is `en-US`. ```console -./dce.sh export 53555 -t "mfa.Ifrn" --locale "de-DE" +./dce export 53555 -t "mfa.Ifrn" --locale "de-DE" ``` #### Date ranges @@ -186,14 +186,14 @@ locales. The default locale is `en-US`. Use `--before` to export messages sent before the provided date. E.g. messages sent before September 18th, 2019: ```console -./dce.sh export 53555 -t "mfa.Ifrn" --before 2019-09-18 +./dce export 53555 -t "mfa.Ifrn" --before 2019-09-18 ``` **Messages sent after a date** Use `--after` to export messages sent after the provided date. E.g. messages sent after September 17th, 2019 11:34 PM: ```console -./dce.sh export 53555 -t "mfa.Ifrn" --after "2019-09-17 23:34" +./dce export 53555 -t "mfa.Ifrn" --after "2019-09-17 23:34" ``` **Messages sent in a date range** @@ -201,7 +201,7 @@ Use `--before` and `--after` to export messages sent during the provided date ra September 17th, 2019 11:34 PM and September 18th: ```console -./dce.sh export 53555 -t "mfa.Ifrn" --after "2019-09-17 23:34" --before "2019-09-18" +./dce export 53555 -t "mfa.Ifrn" --after "2019-09-17 23:34" --before "2019-09-18" ``` You can try different formats like `17-SEP-2019 11:34 PM` or even refine your ranges down to @@ -215,7 +215,7 @@ formats [here](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custo Use `--filter` to filter what messages are included in the export. ```console -./dce.sh export 53555 -t "mfa.Ifrn" --filter "from:Tyrrrz has:image" +./dce export 53555 -t "mfa.Ifrn" --filter "from:Tyrrrz has:image" ``` Documentation on message filter syntax can be found [here](https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs/Message-filters.md). @@ -227,20 +227,20 @@ To export all channels in a specific server, use `list channels` to list channel **Linux/macOS** (one-liner, token set inline): ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 | ./dce.sh export +DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 | ./dce export ``` **Windows**: ```console set DISCORD_TOKEN=mfa.Ifrn -dce.bat list channels 21814 | dce.bat export +dce list channels 21814 | dce export ``` You can also list channels for multiple guilds at once: ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 35930 | ./dce.sh export +DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 35930 | ./dce export ``` #### Including threads @@ -248,7 +248,7 @@ DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 35930 | ./dce.sh export By default, threads are not included. You can change this behavior by passing `--include-threads` to the `list channels` command. It has possible values of `none`, `active`, or `all`, indicating which threads should be included. To include both active and archived threads, use `--include-threads all`. ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 --include-threads all | ./dce.sh export +DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 --include-threads all | ./dce export ``` #### Including voice channels @@ -256,7 +256,7 @@ DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 --include-threads all | ./ By default, voice channels are included. You can change this behavior by passing `--include-vc false` to the `list channels` command. ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 --include-vc false | ./dce.sh export +DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 --include-vc false | ./dce export ``` ### Export all DMs @@ -266,14 +266,14 @@ To export all DMs: **Linux/macOS**: ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels dm | ./dce.sh export +DISCORD_TOKEN="mfa.Ifrn" ./dce list channels dm | ./dce export ``` **Windows**: ```console set DISCORD_TOKEN=mfa.Ifrn -dce.bat list channels dm | dce.bat export +dce list channels dm | dce export ``` ### List channels in a server @@ -281,13 +281,13 @@ dce.bat list channels dm | dce.bat export To list the channels available in a specific server, use the `list channels` command and provide the server ID as an argument: ```console -./dce.sh list channels 21814 -t "mfa.Ifrn" +./dce list channels 21814 -t "mfa.Ifrn" ``` When the output is redirected or piped, the `list channels` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 | ./dce.sh export +DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 | ./dce export ``` ### List direct message channels @@ -295,13 +295,13 @@ DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels 21814 | ./dce.sh export To list all DM channels accessible to the current account, use the `list channels dm` command: ```console -./dce.sh list channels dm -t "mfa.Ifrn" +./dce list channels dm -t "mfa.Ifrn" ``` When the output is redirected or piped, the `list channels dm` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels dm | ./dce.sh export +DISCORD_TOKEN="mfa.Ifrn" ./dce list channels dm | ./dce export ``` ### List servers @@ -309,5 +309,5 @@ DISCORD_TOKEN="mfa.Ifrn" ./dce.sh list channels dm | ./dce.sh export To list all servers accessible by the current account, use the `list guilds` command: ```console -./dce.sh list guilds -t "mfa.Ifrn" > C:\path\to\output.txt +./dce list guilds -t "mfa.Ifrn" > C:\path\to\output.txt ``` diff --git a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj index f6650ab96..f0358085c 100644 --- a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj +++ b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj @@ -20,7 +20,7 @@ diff --git a/DiscordChatExporter.Cli/dce.sh b/DiscordChatExporter.Cli/dce similarity index 100% rename from DiscordChatExporter.Cli/dce.sh rename to DiscordChatExporter.Cli/dce From 316e8c6ea8abebf02fbbc3e9466b4e2d5d75f9aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:12:47 +0000 Subject: [PATCH 11/23] Simplify stdin reading: use ReadAllLinesAsync extension and Snowflake.Parse Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/59c1113d-3c9c-4900-a9b0-5c6116ce7c75 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Commands/ExportChannelsCommand.cs | 23 +++---------------- .../Utils/Extensions/ConsoleExtensions.cs | 13 +++++++++++ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs index fc5f46289..22e74c254 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs @@ -4,6 +4,7 @@ using CliFx.Binding; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Cli.Utils.Extensions; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Utils.Extensions; @@ -33,26 +34,8 @@ public override async ValueTask ExecuteAsync(IConsole console) var channelIds = new List(ChannelIds); if (channelIds.Count == 0 && console.IsInputRedirected) { - var lineNumber = 0; - string? line; - while ((line = await console.Input.ReadLineAsync()) is not null) - { - lineNumber++; - line = line.Trim(); - if (string.IsNullOrEmpty(line)) - continue; - - var snowflake = Snowflake.TryParse(line); - if (snowflake is null) - { - throw new CommandException( - $"Invalid channel ID on line {lineNumber}: '{line}'. " - + "Each line must contain a valid channel ID." - ); - } - - channelIds.Add(snowflake.Value); - } + await foreach (var line in console.Input.ReadAllLinesAsync()) + channelIds.Add(Snowflake.Parse(line)); } if (channelIds.Count == 0) diff --git a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs index d7d47c8c1..80e402539 100644 --- a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs +++ b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using CliFx.Infrastructure; using Spectre.Console; @@ -61,4 +63,15 @@ Func performOperationAsync progressTask.StopTask(); } } + + public static async IAsyncEnumerable ReadAllLinesAsync(this TextReader reader) + { + string? line; + while ((line = await reader.ReadLineAsync()) is not null) + { + line = line.Trim(); + if (!string.IsNullOrEmpty(line)) + yield return line; + } + } } From f656234c38a2ebc3b75f583a5d0bf690f56f904a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:26:56 +0000 Subject: [PATCH 12/23] Refine ReadLinesAsync: pattern matching, rename, cancellation token Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/e4a61491-645a-418c-91cd-74e835baaf44 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Commands/ExportChannelsCommand.cs | 2 +- .../Utils/Extensions/ConsoleExtensions.cs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs index 22e74c254..f8bed5b10 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs @@ -34,7 +34,7 @@ public override async ValueTask ExecuteAsync(IConsole console) var channelIds = new List(ChannelIds); if (channelIds.Count == 0 && console.IsInputRedirected) { - await foreach (var line in console.Input.ReadAllLinesAsync()) + await foreach (var line in console.Input.ReadLinesAsync(cancellationToken)) channelIds.Add(Snowflake.Parse(line)); } diff --git a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs index 80e402539..25c8c5390 100644 --- a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs +++ b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using CliFx.Infrastructure; using Spectre.Console; @@ -64,10 +65,13 @@ Func performOperationAsync } } - public static async IAsyncEnumerable ReadAllLinesAsync(this TextReader reader) + public static async IAsyncEnumerable ReadLinesAsync( + this TextReader reader, + [System.Runtime.CompilerServices.EnumeratorCancellation] + CancellationToken cancellationToken = default + ) { - string? line; - while ((line = await reader.ReadLineAsync()) is not null) + while (await reader.ReadLineAsync(cancellationToken) is { } line) { line = line.Trim(); if (!string.IsNullOrEmpty(line)) From cbf74ba6a85f7b34fc78cdb90fa3a354cfd5b847 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 08:50:06 +0000 Subject: [PATCH 13/23] Use `using System.Runtime.CompilerServices` instead of fully-qualified attribute Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/a1231ba4-d235-4e2d-98b2-a5bef602a85c Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs index 25c8c5390..5a10e9e85 100644 --- a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs +++ b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using CliFx.Infrastructure; @@ -67,8 +68,7 @@ Func performOperationAsync public static async IAsyncEnumerable ReadLinesAsync( this TextReader reader, - [System.Runtime.CompilerServices.EnumeratorCancellation] - CancellationToken cancellationToken = default + [EnumeratorCancellation] CancellationToken cancellationToken = default ) { while (await reader.ReadLineAsync(cancellationToken) is { } line) From 245b36839660a290738b4697a99dd6cf59e4dbb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 08:52:02 +0000 Subject: [PATCH 14/23] Move Trim() from ReadLinesAsync to call site in ExportChannelsCommand Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/6c8dcb73-6a93-428b-93e5-7bbfcfd2e252 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs | 2 +- DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs index f8bed5b10..0ec06634f 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs @@ -35,7 +35,7 @@ public override async ValueTask ExecuteAsync(IConsole console) if (channelIds.Count == 0 && console.IsInputRedirected) { await foreach (var line in console.Input.ReadLinesAsync(cancellationToken)) - channelIds.Add(Snowflake.Parse(line)); + channelIds.Add(Snowflake.Parse(line.Trim())); } if (channelIds.Count == 0) diff --git a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs index 5a10e9e85..2c19fd872 100644 --- a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs +++ b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs @@ -73,8 +73,7 @@ public static async IAsyncEnumerable ReadLinesAsync( { while (await reader.ReadLineAsync(cancellationToken) is { } line) { - line = line.Trim(); - if (!string.IsNullOrEmpty(line)) + if (!string.IsNullOrWhiteSpace(line)) yield return line; } } From 4ec8faef93e6294a796deec75e05146e2c0fa99f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:10:11 +0000 Subject: [PATCH 15/23] Rename 'list guilds' to 'list servers'; use 'servers' in all user-facing text; simplify token setup in docs Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/688f7e3c-af04-410d-93e7-18d7c0620672 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .docs/Using-the-CLI.md | 54 +++++++++++++------ .../Commands/GetChannelsCommand.cs | 4 +- .../Commands/GetGuildsCommand.cs | 2 +- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index 23f20c47e..35f49c9cd 100644 --- a/.docs/Using-the-CLI.md +++ b/.docs/Using-the-CLI.md @@ -38,11 +38,33 @@ Type the following command in your terminal of choice, then press ENTER to run i | export | Exports one or more channels | | list channels | Outputs the list of channels in the given server(s) | | list channels dm | Outputs the list of direct message channels | -| list guilds | Outputs the list of accessible servers | +| list servers | Outputs the list of accessible servers | | guide | Explains how to obtain token, server, and channel ID | To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./dce guide`. +To pass the token, use the `-t` option: + +```console +./dce export 53555 -t "mfa.Ifrn" +``` + +Alternatively, you can set the `DISCORD_TOKEN` environment variable and omit `-t`: + +**Linux/macOS:** + +```console +export DISCORD_TOKEN="mfa.Ifrn" +``` + +**Windows:** + +```console +set DISCORD_TOKEN=mfa.Ifrn +``` + +The pipeline examples in this guide assume `DISCORD_TOKEN` is already set. + To get help with a specific command, run: ```console @@ -224,23 +246,22 @@ Documentation on message filter syntax can be found [here](https://github.com/Ty To export all channels in a specific server, use `list channels` to list channels and pipe the result to `export`. -**Linux/macOS** (one-liner, token set inline): +**Linux/macOS:** ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 | ./dce export +./dce list channels 21814 | ./dce export ``` -**Windows**: +**Windows:** ```console -set DISCORD_TOKEN=mfa.Ifrn dce list channels 21814 | dce export ``` -You can also list channels for multiple guilds at once: +You can also list channels for multiple servers at once: ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 35930 | ./dce export +./dce list channels 21814 35930 | ./dce export ``` #### Including threads @@ -248,7 +269,7 @@ DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 35930 | ./dce export By default, threads are not included. You can change this behavior by passing `--include-threads` to the `list channels` command. It has possible values of `none`, `active`, or `all`, indicating which threads should be included. To include both active and archived threads, use `--include-threads all`. ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 --include-threads all | ./dce export +./dce list channels 21814 --include-threads all | ./dce export ``` #### Including voice channels @@ -256,23 +277,22 @@ DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 --include-threads all | ./dce By default, voice channels are included. You can change this behavior by passing `--include-vc false` to the `list channels` command. ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 --include-vc false | ./dce export +./dce list channels 21814 --include-vc false | ./dce export ``` ### Export all DMs To export all DMs: -**Linux/macOS**: +**Linux/macOS:** ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce list channels dm | ./dce export +./dce list channels dm | ./dce export ``` -**Windows**: +**Windows:** ```console -set DISCORD_TOKEN=mfa.Ifrn dce list channels dm | dce export ``` @@ -287,7 +307,7 @@ To list the channels available in a specific server, use the `list channels` com When the output is redirected or piped, the `list channels` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce list channels 21814 | ./dce export +./dce list channels 21814 | ./dce export ``` ### List direct message channels @@ -301,13 +321,13 @@ To list all DM channels accessible to the current account, use the `list channel When the output is redirected or piped, the `list channels dm` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: ```console -DISCORD_TOKEN="mfa.Ifrn" ./dce list channels dm | ./dce export +./dce list channels dm | ./dce export ``` ### List servers -To list all servers accessible by the current account, use the `list guilds` command: +To list all servers accessible by the current account, use the `list servers` command: ```console -./dce list guilds -t "mfa.Ifrn" > C:\path\to\output.txt +./dce list servers -t "mfa.Ifrn" > C:\path\to\output.txt ``` diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs index 97559cddc..10eadda37 100644 --- a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -16,7 +16,7 @@ namespace DiscordChatExporter.Cli.Commands; [Command("list channels", Description = "Gets the list of channels in one or more servers.")] public partial class GetChannelsCommand : DiscordCommandBase { - [CommandParameter(0, Name = "guild-ids", Description = "Server ID(s).")] + [CommandParameter(0, Name = "server-ids", Description = "Server ID(s).")] public required IReadOnlyList GuildIds { get; set; } [CommandOption("include-vc", Description = "Include voice channels.")] @@ -71,7 +71,7 @@ await Discord.GetGuildThreadsAsync( } else { - // Show guild header when listing multiple guilds + // Show server header when listing multiple servers if (GuildIds.Count > 1) { var guild = await Discord.GetGuildAsync(guildId, cancellationToken); diff --git a/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs b/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs index c089fe29d..af8303d1c 100644 --- a/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs @@ -9,7 +9,7 @@ namespace DiscordChatExporter.Cli.Commands; -[Command("list guilds", Description = "Gets the list of accessible servers.")] +[Command("list servers", Description = "Gets the list of accessible servers.")] public partial class GetGuildsCommand : DiscordCommandBase { public override async ValueTask ExecuteAsync(IConsole console) From 89407c121f24c479021f1c8794688ca6529bfd03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:14:41 +0000 Subject: [PATCH 16/23] Add .ConfigureAwait(false) to ReadLinesAsync; remove IsNullOrWhiteSpace guard Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/e94c999b-5241-4e2e-9af1-01f6be95e72c Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Utils/Extensions/ConsoleExtensions.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs index 2c19fd872..2cc75c8da 100644 --- a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs +++ b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs @@ -71,10 +71,9 @@ public static async IAsyncEnumerable ReadLinesAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default ) { - while (await reader.ReadLineAsync(cancellationToken) is { } line) + while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line) { - if (!string.IsNullOrWhiteSpace(line)) - yield return line; + yield return line; } } } 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 17/23] 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 0ec06634f..0e7110293 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 override async ValueTask ExecuteAsync(IConsole console) 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 10eadda37..f0c8839e8 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 override async ValueTask ExecuteAsync(IConsole console) var cancellationToken = console.RegisterCancellationHandler(); + var allChannels = new List(); + foreach (var guildId in GuildIds) { var channels = (await Discord.GetGuildChannelsAsync(guildId, cancellationToken)) @@ -59,86 +62,18 @@ await Discord.GetGuildThreadsAsync( .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 e2251570d..95b38a195 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 @@ await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken) .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 af8303d1c..9b77cfd72 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 override async ValueTask ExecuteAsync(IConsole console) .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 000000000..ac8cf31e1 --- /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 000000000..032012b5d --- /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()); +} From 948ceba8cc459ed52f893dfda0f5786a9cf4c89a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 11:14:19 +0000 Subject: [PATCH 18/23] docs: use -short|--long convention for option names in prose text Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/aa0b1344-6eb9-4853-9b48-afd06e94c970 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .docs/Using-the-CLI.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index 35f49c9cd..025452680 100644 --- a/.docs/Using-the-CLI.md +++ b/.docs/Using-the-CLI.md @@ -43,7 +43,7 @@ Type the following command in your terminal of choice, then press ENTER to run i To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./dce guide`. -To pass the token, use the `-t` option: +To pass the token, use the `-t|--token` option: ```console ./dce export 53555 -t "mfa.Ifrn" @@ -79,7 +79,7 @@ For example, to figure out how to use the `export` command, run: ## Export a specific channel -You can quickly export with DCE's default settings by providing the channel ID as a positional argument and `-t token`. +You can quickly export with DCE's default settings by providing the channel ID as a positional argument and `-t|--token`. ```console ./dce export 53555 -t "mfa.Ifrn" @@ -87,7 +87,7 @@ You can quickly export with DCE's default settings by providing the channel ID a #### Changing the format -You can change the export format to `HtmlDark`, `HtmlLight`, `PlainText` `Json` or `Csv` with `-f format`. The default +You can change the export format to `HtmlDark`, `HtmlLight`, `PlainText` `Json` or `Csv` with `-f|--format`. The default format is `HtmlDark`. ```console @@ -96,7 +96,7 @@ format is `HtmlDark`. #### Changing the output filename -You can change the filename by using `-o name.ext`. e.g. for the `HTML` format: +You can change the filename by using `-o|--output`. e.g. for the `HTML` format: ```console ./dce export 53555 -t "mfa.Ifrn" -o myserver.html @@ -104,7 +104,7 @@ You can change the filename by using `-o name.ext`. e.g. for the `HTML` format: #### Changing the output directory -You can change the export directory by using `-o` and providing a path that ends with a slash or does not have a file +You can change the export directory by using `-o|--output` and providing a path that ends with a slash or does not have a file extension. If any of the folders in the path have a space in its name, escape them with quotes ("). @@ -114,7 +114,7 @@ If any of the folders in the path have a space in its name, escape them with quo #### Changing the filename and output directory -You can change both the filename and export directory by using `-o directory\name.ext`. +You can change both the filename and export directory by using `-o|--output`. Note that the filename must have an extension, otherwise it will be considered a directory name. If any of the folders in the path have a space in its name, escape them with quotes ("). From 7f086419743ab051e754c52dce1afff91bf0f500 Mon Sep 17 00:00:00 2001 From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> Date: Sat, 4 Apr 2026 14:16:43 +0300 Subject: [PATCH 19/23] Update error message for missing channel IDs --- DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs index 0e7110293..a6ce1a1f6 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs @@ -59,7 +59,7 @@ public override async ValueTask ExecuteAsync(IConsole console) { throw new CommandException( "No channel IDs provided. " - + "Specify channel IDs as arguments or pipe them from the 'list channels' or 'list channels dm' commands." + + "Specify channel IDs as arguments or pipe them from a newline-separated list." ); } From f8ab9260742b1a546f1a43d5e7eed7745c1e1b2c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 11:23:45 +0000 Subject: [PATCH 20/23] Add 'list unwrap' command; remove category unwrapping from export; add [!IMPORTANT] callouts to pipeline doc sections Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/d2a03a38-0ed4-45c7-b8e7-615ffb35c971 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .docs/Using-the-CLI.md | 33 ++++++--- .../Commands/ExportChannelsCommand.cs | 27 +------ .../Commands/UnwrapChannelsCommand.cs | 71 +++++++++++++++++++ 3 files changed, 97 insertions(+), 34 deletions(-) create mode 100644 DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index 025452680..8626cf93f 100644 --- a/.docs/Using-the-CLI.md +++ b/.docs/Using-the-CLI.md @@ -33,13 +33,14 @@ Type the following command in your terminal of choice, then press ENTER to run i ## CLI commands -| Command | Description | -| ----------------- | ---------------------------------------------------- | -| export | Exports one or more channels | -| list channels | Outputs the list of channels in the given server(s) | -| list channels dm | Outputs the list of direct message channels | -| list servers | Outputs the list of accessible servers | -| guide | Explains how to obtain token, server, and channel ID | +| Command | Description | +| ----------------- | -------------------------------------------------------------------- | +| export | Exports one or more channels | +| list channels | Outputs the list of channels in the given server(s) | +| list channels dm | Outputs the list of direct message channels | +| list servers | Outputs the list of accessible servers | +| list unwrap | Resolves categories in a channel list to their child channels | +| guide | Explains how to obtain token, server, and channel ID | To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./dce guide`. @@ -244,6 +245,9 @@ Documentation on message filter syntax can be found [here](https://github.com/Ty ### Export channels from a specific server +> [!IMPORTANT] +> The following examples assume `DISCORD_TOKEN` is already set. See [CLI commands](#cli-commands) for instructions. + To export all channels in a specific server, use `list channels` to list channels and pipe the result to `export`. **Linux/macOS:** @@ -282,6 +286,9 @@ By default, voice channels are included. You can change this behavior by passing ### Export all DMs +> [!IMPORTANT] +> The following examples assume `DISCORD_TOKEN` is already set. See [CLI commands](#cli-commands) for instructions. + To export all DMs: **Linux/macOS:** @@ -304,7 +311,7 @@ To list the channels available in a specific server, use the `list channels` com ./dce list channels 21814 -t "mfa.Ifrn" ``` -When the output is redirected or piped, the `list channels` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: +The `list channels` command outputs a JSON array of channel objects. You can pipe this directly to the `export` command: ```console ./dce list channels 21814 | ./dce export @@ -318,12 +325,20 @@ To list all DM channels accessible to the current account, use the `list channel ./dce list channels dm -t "mfa.Ifrn" ``` -When the output is redirected or piped, the `list channels dm` command prints only channel IDs (one per line). This allows you to pipe the output directly to the `export` command: +The `list channels dm` command outputs a JSON array of channel objects. You can pipe this directly to the `export` command: ```console ./dce list channels dm | ./dce export ``` +### Unwrap categories + +To resolve category channels in a list to their child channels, use the `list unwrap` command: + +```console +./dce list channels 21814 | ./dce list unwrap | ./dce export +``` + ### List servers To list all servers accessible by the current account, use the `list servers` command: diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs index a6ce1a1f6..d2dc24356 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs @@ -8,7 +8,6 @@ using DiscordChatExporter.Cli.Utils.Extensions; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; -using DiscordChatExporter.Core.Utils.Extensions; namespace DiscordChatExporter.Cli.Commands; @@ -19,8 +18,7 @@ public partial class ExportChannelsCommand : ExportCommandBase 0, Name = "channel-ids", Description = "Channel ID(s). " - + "If provided with category ID(s), all channels inside those categories will be exported. " - + "If not provided, channel IDs are read from standard input (one per line), " + + "If not provided, channel IDs are read from standard input (one per line or as a JSON array), " + "enabling piping from the 'list channels' or 'list channels dm' commands." )] public IReadOnlyList ChannelIds { get; set; } = []; @@ -66,32 +64,11 @@ public override async ValueTask ExecuteAsync(IConsole console) await console.Output.WriteLineAsync("Resolving channel(s)..."); var channels = new List(); - var channelsByGuild = new Dictionary>(); foreach (var channelId in channelIds) { var channel = await Discord.GetChannelAsync(channelId, cancellationToken); - - // Unwrap categories - if (channel.IsCategory) - { - var guildChannels = - channelsByGuild.GetValueOrDefault(channel.GuildId) - ?? await Discord.GetGuildChannelsAsync(channel.GuildId, cancellationToken); - - foreach (var guildChannel in guildChannels) - { - if (guildChannel.Parent?.Id == channel.Id) - channels.Add(guildChannel); - } - - // Cache the guild channels to avoid redundant work - channelsByGuild[channel.GuildId] = guildChannels; - } - else - { - channels.Add(channel); - } + channels.Add(channel); } await ExportAsync(console, channels); diff --git a/DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs new file mode 100644 index 000000000..54db10912 --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using CliFx.Binding; +using CliFx.Infrastructure; +using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Cli.Utils.Extensions; +using DiscordChatExporter.Cli.Utils.Json; +using DiscordChatExporter.Core.Discord; +using DiscordChatExporter.Core.Discord.Data; +using DiscordChatExporter.Core.Utils.Extensions; + +namespace DiscordChatExporter.Cli.Commands; + +[Command( + "list unwrap", + Description = "Resolves categories in a channel list to their child channels." +)] +public partial class UnwrapChannelsCommand : DiscordCommandBase +{ + public override async ValueTask ExecuteAsync(IConsole console) + { + await base.ExecuteAsync(console); + + var cancellationToken = console.RegisterCancellationHandler(); + + // Read all JSON from stdin (produced by 'list channels' or 'list channels dm') + var sb = new StringBuilder(); + await foreach (var line in console.Input.ReadLinesAsync(cancellationToken)) + sb.Append(line); + + var channels = + JsonSerializer.Deserialize( + sb.ToString().Trim(), + CliJsonSerializerContext.Instance.ChannelArray + ) ?? []; + + var result = new List(); + var channelsByGuild = new Dictionary>(); + + foreach (var channel in channels) + { + if (channel.IsCategory) + { + var guildChannels = + channelsByGuild.GetValueOrDefault(channel.GuildId) + ?? await Discord.GetGuildChannelsAsync(channel.GuildId, cancellationToken); + + foreach (var guildChannel in guildChannels) + { + if (guildChannel.Parent?.Id == channel.Id) + result.Add(guildChannel); + } + + channelsByGuild[channel.GuildId] = guildChannels; + } + else + { + result.Add(channel); + } + } + + await console.Output.WriteLineAsync( + JsonSerializer.Serialize( + result.ToArray(), + CliJsonSerializerContext.Instance.ChannelArray + ) + ); + } +} From 1835af12d5c9aecd616087a7b1c3eb96dda26e49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 11:28:04 +0000 Subject: [PATCH 21/23] Fix: catch JsonException in list unwrap; improve export error message Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/d2a03a38-0ed4-45c7-b8e7-615ffb35c971 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Commands/ExportChannelsCommand.cs | 2 +- .../Commands/UnwrapChannelsCommand.cs | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs index d2dc24356..c07fc37bb 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs @@ -57,7 +57,7 @@ public override async ValueTask ExecuteAsync(IConsole console) { throw new CommandException( "No channel IDs provided. " - + "Specify channel IDs as arguments or pipe them from a newline-separated list." + + "Specify channel IDs as arguments or pipe them from the 'list channels' or 'list channels dm' commands." ); } diff --git a/DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs index 54db10912..5616f3d56 100644 --- a/DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using CliFx; using CliFx.Binding; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands.Base; @@ -30,11 +31,22 @@ public override async ValueTask ExecuteAsync(IConsole console) await foreach (var line in console.Input.ReadLinesAsync(cancellationToken)) sb.Append(line); - var channels = - JsonSerializer.Deserialize( - sb.ToString().Trim(), - CliJsonSerializerContext.Instance.ChannelArray - ) ?? []; + Channel[] channels; + try + { + channels = + JsonSerializer.Deserialize( + sb.ToString().Trim(), + CliJsonSerializerContext.Instance.ChannelArray + ) ?? []; + } + catch (JsonException) + { + throw new CommandException( + "Failed to parse input as a JSON channel array. " + + "Pipe the output of 'list channels' or 'list channels dm' to this command." + ); + } var result = new List(); var channelsByGuild = new Dictionary>(); From f6e710909ca2819e45349d0b1b6f3f479404b8e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 11:33:13 +0000 Subject: [PATCH 22/23] Fix export JSON detection to use numeric check; rename GuildIds to ServerIds in GetChannelsCommand Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/ab01ba49-ad07-42ba-b382-27f4d105a92b Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs | 4 ++-- DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs index c07fc37bb..ae2e45593 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelsCommand.cs @@ -39,8 +39,8 @@ public override async ValueTask ExecuteAsync(IConsole console) if (string.IsNullOrEmpty(trimmed)) continue; - // JSON array produced by 'list channels' / 'list channels dm' - if (trimmed.StartsWith('[')) + // Snowflake IDs are numeric; non-numeric input is treated as a JSON array + if (!char.IsAsciiDigit(trimmed[0])) { using var doc = JsonDocument.Parse(trimmed); foreach (var element in doc.RootElement.EnumerateArray()) diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs index f0c8839e8..f0bd5e652 100644 --- a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -18,7 +18,7 @@ namespace DiscordChatExporter.Cli.Commands; public partial class GetChannelsCommand : DiscordCommandBase { [CommandParameter(0, Name = "server-ids", Description = "Server ID(s).")] - public required IReadOnlyList GuildIds { get; set; } + public required IReadOnlyList ServerIds { get; set; } [CommandOption("include-vc", Description = "Include voice channels.")] public bool IncludeVoiceChannels { get; set; } = true; @@ -38,9 +38,9 @@ public override async ValueTask ExecuteAsync(IConsole console) var allChannels = new List(); - foreach (var guildId in GuildIds) + foreach (var serverId in ServerIds) { - var channels = (await Discord.GetGuildChannelsAsync(guildId, cancellationToken)) + var channels = (await Discord.GetGuildChannelsAsync(serverId, cancellationToken)) .Where(c => !c.IsCategory) .Where(c => IncludeVoiceChannels || !c.IsVoice) .OrderBy(c => c.Parent?.Position) @@ -51,7 +51,7 @@ public override async ValueTask ExecuteAsync(IConsole console) ThreadInclusionMode != ThreadInclusionMode.None ? ( await Discord.GetGuildThreadsAsync( - guildId, + serverId, ThreadInclusionMode == ThreadInclusionMode.All, null, null, From ea0b47f26d4d1204e3620fd1c77dff4ee4e75875 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:49:33 +0000 Subject: [PATCH 23/23] Expand list unwrap to also resolve forum channels to thread posts Agent-Logs-Url: https://github.com/Tyrrrz/DiscordChatExporter/sessions/674ba90d-1778-4427-b50a-30bc50195319 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .docs/Using-the-CLI.md | 6 +++--- .../Commands/UnwrapChannelsCommand.cs | 14 +++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index 8626cf93f..591fc2c3f 100644 --- a/.docs/Using-the-CLI.md +++ b/.docs/Using-the-CLI.md @@ -39,7 +39,7 @@ Type the following command in your terminal of choice, then press ENTER to run i | list channels | Outputs the list of channels in the given server(s) | | list channels dm | Outputs the list of direct message channels | | list servers | Outputs the list of accessible servers | -| list unwrap | Resolves categories in a channel list to their child channels | +| list unwrap | Resolves categories and forums in a channel list to their child channels and threads | | guide | Explains how to obtain token, server, and channel ID | To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./dce guide`. @@ -331,9 +331,9 @@ The `list channels dm` command outputs a JSON array of channel objects. You can ./dce list channels dm | ./dce export ``` -### Unwrap categories +### Unwrap categories and forums -To resolve category channels in a list to their child channels, use the `list unwrap` command: +To resolve category and forum channels in a list to their child channels and thread posts, use the `list unwrap` command: ```console ./dce list channels 21814 | ./dce list unwrap | ./dce export diff --git a/DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs index 5616f3d56..7a71621fd 100644 --- a/DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/UnwrapChannelsCommand.cs @@ -16,7 +16,7 @@ namespace DiscordChatExporter.Cli.Commands; [Command( "list unwrap", - Description = "Resolves categories in a channel list to their child channels." + Description = "Resolves categories and forums in a channel list to their child channels and threads." )] public partial class UnwrapChannelsCommand : DiscordCommandBase { @@ -55,6 +55,7 @@ public override async ValueTask ExecuteAsync(IConsole console) { if (channel.IsCategory) { + // Expand category to its child channels var guildChannels = channelsByGuild.GetValueOrDefault(channel.GuildId) ?? await Discord.GetGuildChannelsAsync(channel.GuildId, cancellationToken); @@ -67,6 +68,17 @@ public override async ValueTask ExecuteAsync(IConsole console) channelsByGuild[channel.GuildId] = guildChannels; } + else if (channel.Kind == ChannelKind.GuildForum) + { + // Expand forum to its thread posts + await foreach ( + var thread in Discord.GetChannelThreadsAsync( + [channel], + cancellationToken: cancellationToken + ) + ) + result.Add(thread); + } else { result.Add(channel);