diff --git a/.docs/Using-the-CLI.md b/.docs/Using-the-CLI.md index 36de6c95..0a0aa0d3 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: +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 exportall -t "mfa.Ifrn" -``` - -#### Excluding DMs - -To exclude DMs, add the `--include-dm false` option. - -```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 464ecf0a..00000000 --- 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 3e963cad..86d59bb8 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 partial class ExportChannelsCommand : ExportCommandBase 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 39f41f40..00000000 --- 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 037d8c58..00000000 --- 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 b0116ed7..b603d9cd 100644 --- a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -60,54 +60,68 @@ public partial class GetChannelsCommand : DiscordCommandBase .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()); - // Separator - using (console.WithForegroundColor(ConsoleColor.DarkGray)) - await console.Output.WriteAsync(" | "); + var channelThreads = threads.Where(t => t.Parent?.Id == channel.Id).ToArray(); + var channelThreadIdMaxLength = channelThreads + .Select(t => t.Id.ToString().Length) + .OrderDescending() + .FirstOrDefault(); - // Thread status - using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteLineAsync( - channelThread.IsArchived ? "Archived" : "Active" + 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" + ); + } } } } diff --git a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs index 06ea69b1..b395b3cc 100644 --- a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs @@ -30,20 +30,29 @@ public partial class GetDirectChannelsCommand : DiscordCommandBase .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, ' ') - ); + 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(" | "); + // Separator + using (console.WithForegroundColor(ConsoleColor.DarkGray)) + await console.Output.WriteAsync(" | "); - // Channel name - using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteLineAsync(channel.GetHierarchicalName()); + // Channel name + using (console.WithForegroundColor(ConsoleColor.White)) + await console.Output.WriteLineAsync(channel.GetHierarchicalName()); + } } } }