This commit is contained in:
Tyrrrz 2021-12-08 23:50:21 +02:00
parent 8e7baee8a5
commit 880f400e2c
148 changed files with 14241 additions and 14396 deletions

View file

@ -16,141 +16,140 @@ using DiscordChatExporter.Core.Exporting.Filtering;
using DiscordChatExporter.Core.Exporting.Partitioning;
using DiscordChatExporter.Core.Utils.Extensions;
namespace DiscordChatExporter.Cli.Commands.Base
namespace DiscordChatExporter.Cli.Commands.Base;
public abstract class ExportCommandBase : TokenCommandBase
{
public abstract class ExportCommandBase : TokenCommandBase
[CommandOption("output", 'o', Description = "Output file or directory path.")]
public string OutputPath { get; init; } = Directory.GetCurrentDirectory();
[CommandOption("format", 'f', Description = "Export format.")]
public ExportFormat ExportFormat { get; init; } = ExportFormat.HtmlDark;
[CommandOption("after", Description = "Only include messages sent after this date or message ID.")]
public Snowflake? After { get; init; }
[CommandOption("before", Description = "Only include messages sent before this date or message ID.")]
public Snowflake? Before { get; init; }
[CommandOption("partition", 'p', Description = "Split output into partitions, each limited to this number of messages (e.g. '100') or file size (e.g. '10mb').")]
public PartitionLimit PartitionLimit { get; init; } = PartitionLimit.Null;
[CommandOption("filter", Description = "Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image').")]
public MessageFilter MessageFilter { get; init; } = MessageFilter.Null;
[CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")]
public int ParallelLimit { get; init; } = 1;
[CommandOption("media", Description = "Download referenced media content.")]
public bool ShouldDownloadMedia { get; init; }
[CommandOption("reuse-media", Description = "Reuse already existing media content to skip redundant downloads.")]
public bool ShouldReuseMedia { get; init; }
[CommandOption("dateformat", Description = "Format used when writing dates.")]
public string DateFormat { get; init; } = "dd-MMM-yy hh:mm tt";
private ChannelExporter? _channelExporter;
protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord);
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Channel> channels)
{
[CommandOption("output", 'o', Description = "Output file or directory path.")]
public string OutputPath { get; init; } = Directory.GetCurrentDirectory();
var cancellationToken = console.RegisterCancellationHandler();
[CommandOption("format", 'f', Description = "Export format.")]
public ExportFormat ExportFormat { get; init; } = ExportFormat.HtmlDark;
[CommandOption("after", Description = "Only include messages sent after this date or message ID.")]
public Snowflake? After { get; init; }
[CommandOption("before", Description = "Only include messages sent before this date or message ID.")]
public Snowflake? Before { get; init; }
[CommandOption("partition", 'p', Description = "Split output into partitions, each limited to this number of messages (e.g. '100') or file size (e.g. '10mb').")]
public PartitionLimit PartitionLimit { get; init; } = PartitionLimit.Null;
[CommandOption("filter", Description = "Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image').")]
public MessageFilter MessageFilter { get; init; } = MessageFilter.Null;
[CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")]
public int ParallelLimit { get; init; } = 1;
[CommandOption("media", Description = "Download referenced media content.")]
public bool ShouldDownloadMedia { get; init; }
[CommandOption("reuse-media", Description = "Reuse already existing media content to skip redundant downloads.")]
public bool ShouldReuseMedia { get; init; }
[CommandOption("dateformat", Description = "Format used when writing dates.")]
public string DateFormat { get; init; } = "dd-MMM-yy hh:mm tt";
private ChannelExporter? _channelExporter;
protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord);
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Channel> channels)
if (ShouldReuseMedia && !ShouldDownloadMedia)
{
var cancellationToken = console.RegisterCancellationHandler();
throw new CommandException("Option --reuse-media cannot be used without --media.");
}
if (ShouldReuseMedia && !ShouldDownloadMedia)
var errors = new ConcurrentDictionary<Channel, string>();
// Export
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
await console.CreateProgressTicker().StartAsync(async progressContext =>
{
await channels.ParallelForEachAsync(async channel =>
{
throw new CommandException("Option --reuse-media cannot be used without --media.");
}
var errors = new ConcurrentDictionary<Channel, string>();
// Export
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
await console.CreateProgressTicker().StartAsync(async progressContext =>
{
await channels.ParallelForEachAsync(async channel =>
try
{
try
await progressContext.StartTaskAsync($"{channel.Category.Name} / {channel.Name}", async progress =>
{
await progressContext.StartTaskAsync($"{channel.Category.Name} / {channel.Name}", async progress =>
{
var guild = await Discord.GetGuildAsync(channel.GuildId, cancellationToken);
var guild = await Discord.GetGuildAsync(channel.GuildId, cancellationToken);
var request = new ExportRequest(
guild,
channel,
OutputPath,
ExportFormat,
After,
Before,
PartitionLimit,
MessageFilter,
ShouldDownloadMedia,
ShouldReuseMedia,
DateFormat
);
var request = new ExportRequest(
guild,
channel,
OutputPath,
ExportFormat,
After,
Before,
PartitionLimit,
MessageFilter,
ShouldDownloadMedia,
ShouldReuseMedia,
DateFormat
);
await Exporter.ExportChannelAsync(request, progress, cancellationToken);
});
}
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
{
errors[channel] = ex.Message;
}
}, Math.Max(ParallelLimit, 1), cancellationToken);
});
await Exporter.ExportChannelAsync(request, progress, cancellationToken);
});
}
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
{
errors[channel] = ex.Message;
}
}, Math.Max(ParallelLimit, 1), cancellationToken);
});
// Print result
using (console.WithForegroundColor(ConsoleColor.White))
// Print result
using (console.WithForegroundColor(ConsoleColor.White))
{
await console.Output.WriteLineAsync(
$"Successfully exported {channels.Count - errors.Count} channel(s)."
);
}
// Print errors
if (errors.Any())
{
await console.Output.WriteLineAsync();
using (console.WithForegroundColor(ConsoleColor.Red))
{
await console.Output.WriteLineAsync(
$"Successfully exported {channels.Count - errors.Count} channel(s)."
$"Failed to export {errors.Count} channel(s):"
);
}
// Print errors
if (errors.Any())
foreach (var (channel, error) in errors)
{
await console.Output.WriteLineAsync();
await console.Output.WriteAsync($"{channel.Category.Name} / {channel.Name}: ");
using (console.WithForegroundColor(ConsoleColor.Red))
{
await console.Output.WriteLineAsync(
$"Failed to export {errors.Count} channel(s):"
);
}
foreach (var (channel, error) in errors)
{
await console.Output.WriteAsync($"{channel.Category.Name} / {channel.Name}: ");
using (console.WithForegroundColor(ConsoleColor.Red))
await console.Output.WriteLineAsync(error);
}
await console.Output.WriteLineAsync();
await console.Output.WriteLineAsync(error);
}
// Fail the command only if ALL channels failed to export.
// Having some of the channels fail to export is expected.
if (errors.Count >= channels.Count)
{
throw new CommandException("Export failed.");
}
await console.Output.WriteLineAsync();
}
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Snowflake> channelIds)
// Fail the command only if ALL channels failed to export.
// Having some of the channels fail to export is expected.
if (errors.Count >= channels.Count)
{
var cancellationToken = console.RegisterCancellationHandler();
var channels = new List<Channel>();
foreach (var channelId in channelIds)
{
var channel = await Discord.GetChannelAsync(channelId, cancellationToken);
channels.Add(channel);
}
await ExecuteAsync(console, channels);
throw new CommandException("Export failed.");
}
}
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Snowflake> channelIds)
{
var cancellationToken = console.RegisterCancellationHandler();
var channels = new List<Channel>();
foreach (var channelId in channelIds)
{
var channel = await Discord.GetChannelAsync(channelId, cancellationToken);
channels.Add(channel);
}
await ExecuteAsync(console, channels);
}
}

View file

@ -4,27 +4,26 @@ using CliFx.Attributes;
using CliFx.Infrastructure;
using DiscordChatExporter.Core.Discord;
namespace DiscordChatExporter.Cli.Commands.Base
namespace DiscordChatExporter.Cli.Commands.Base;
public abstract class TokenCommandBase : ICommand
{
public abstract class TokenCommandBase : ICommand
{
[CommandOption("token", 't', IsRequired = true, EnvironmentVariable = "DISCORD_TOKEN", Description = "Authentication token.")]
public string TokenValue { get; init; } = "";
[CommandOption("token", 't', IsRequired = true, EnvironmentVariable = "DISCORD_TOKEN", Description = "Authentication token.")]
public string TokenValue { get; init; } = "";
[CommandOption("bot", 'b', EnvironmentVariable = "DISCORD_TOKEN_BOT", Description = "Authenticate as a bot.")]
public bool IsBotToken { get; init; }
[CommandOption("bot", 'b', EnvironmentVariable = "DISCORD_TOKEN_BOT", Description = "Authenticate as a bot.")]
public bool IsBotToken { get; init; }
private AuthToken? _authToken;
private AuthToken AuthToken => _authToken ??= new AuthToken(
IsBotToken
? AuthTokenKind.Bot
: AuthTokenKind.User,
TokenValue
);
private AuthToken? _authToken;
private AuthToken AuthToken => _authToken ??= new AuthToken(
IsBotToken
? AuthTokenKind.Bot
: AuthTokenKind.User,
TokenValue
);
private DiscordClient? _discordClient;
protected DiscordClient Discord => _discordClient ??= new DiscordClient(AuthToken);
private DiscordClient? _discordClient;
protected DiscordClient Discord => _discordClient ??= new DiscordClient(AuthToken);
public abstract ValueTask ExecuteAsync(IConsole console);
}
public abstract ValueTask ExecuteAsync(IConsole console);
}

View file

@ -5,37 +5,36 @@ using CliFx.Infrastructure;
using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Cli.Commands
namespace DiscordChatExporter.Cli.Commands;
[Command("exportall", Description = "Export all accessible channels.")]
public class ExportAllCommand : ExportCommandBase
{
[Command("exportall", Description = "Export all accessible channels.")]
public class ExportAllCommand : ExportCommandBase
[CommandOption("include-dm", Description = "Include direct message channels.")]
public bool IncludeDirectMessages { get; init; } = true;
public override async ValueTask ExecuteAsync(IConsole console)
{
[CommandOption("include-dm", Description = "Include direct message channels.")]
public bool IncludeDirectMessages { get; init; } = true;
var cancellationToken = console.RegisterCancellationHandler();
var channels = new List<Channel>();
public override async ValueTask ExecuteAsync(IConsole console)
await console.Output.WriteLineAsync("Fetching channels...");
await foreach (var guild in Discord.GetUserGuildsAsync(cancellationToken))
{
var cancellationToken = console.RegisterCancellationHandler();
var channels = new List<Channel>();
// Skip DMs if instructed to
if (!IncludeDirectMessages && guild.Id == Guild.DirectMessages.Id)
continue;
await console.Output.WriteLineAsync("Fetching channels...");
await foreach (var guild in Discord.GetUserGuildsAsync(cancellationToken))
await foreach (var channel in Discord.GetGuildChannelsAsync(guild.Id, cancellationToken))
{
// Skip DMs if instructed to
if (!IncludeDirectMessages && guild.Id == Guild.DirectMessages.Id)
// Skip non-text channels
if (!channel.IsTextChannel)
continue;
await foreach (var channel in Discord.GetGuildChannelsAsync(guild.Id, cancellationToken))
{
// Skip non-text channels
if (!channel.IsTextChannel)
continue;
channels.Add(channel);
}
channels.Add(channel);
}
await base.ExecuteAsync(console, channels);
}
await base.ExecuteAsync(console, channels);
}
}

View file

@ -6,16 +6,15 @@ using CliFx.Infrastructure;
using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Core.Discord;
namespace DiscordChatExporter.Cli.Commands
{
[Command("export", Description = "Export one or multiple channels.")]
public class ExportChannelsCommand : ExportCommandBase
{
// TODO: change this to plural (breaking change)
[CommandOption("channel", 'c', IsRequired = true, Description = "Channel ID(s).")]
public IReadOnlyList<Snowflake> ChannelIds { get; init; } = Array.Empty<Snowflake>();
namespace DiscordChatExporter.Cli.Commands;
public override async ValueTask ExecuteAsync(IConsole console) =>
await base.ExecuteAsync(console, ChannelIds);
}
[Command("export", Description = "Export one or multiple channels.")]
public class ExportChannelsCommand : ExportCommandBase
{
// TODO: change this to plural (breaking change)
[CommandOption("channel", 'c', IsRequired = true, Description = "Channel ID(s).")]
public IReadOnlyList<Snowflake> ChannelIds { get; init; } = Array.Empty<Snowflake>();
public override async ValueTask ExecuteAsync(IConsole console) =>
await base.ExecuteAsync(console, ChannelIds);
}

View file

@ -6,20 +6,19 @@ using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Utils.Extensions;
namespace DiscordChatExporter.Cli.Commands
namespace DiscordChatExporter.Cli.Commands;
[Command("exportdm", Description = "Export all direct message channels.")]
public class ExportDirectMessagesCommand : ExportCommandBase
{
[Command("exportdm", Description = "Export all direct message channels.")]
public class ExportDirectMessagesCommand : ExportCommandBase
public override async ValueTask ExecuteAsync(IConsole console)
{
public override async ValueTask ExecuteAsync(IConsole console)
{
var cancellationToken = console.RegisterCancellationHandler();
var cancellationToken = console.RegisterCancellationHandler();
await console.Output.WriteLineAsync("Fetching channels...");
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken);
var textChannels = channels.Where(c => c.IsTextChannel).ToArray();
await console.Output.WriteLineAsync("Fetching channels...");
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken);
var textChannels = channels.Where(c => c.IsTextChannel).ToArray();
await base.ExecuteAsync(console, textChannels);
}
await base.ExecuteAsync(console, textChannels);
}
}

View file

@ -6,23 +6,22 @@ using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Utils.Extensions;
namespace DiscordChatExporter.Cli.Commands
namespace DiscordChatExporter.Cli.Commands;
[Command("exportguild", Description = "Export all channels within specified guild.")]
public class ExportGuildCommand : ExportCommandBase
{
[Command("exportguild", Description = "Export all channels within specified guild.")]
public class ExportGuildCommand : ExportCommandBase
[CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")]
public Snowflake GuildId { get; init; }
public override async ValueTask ExecuteAsync(IConsole console)
{
[CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")]
public Snowflake GuildId { get; init; }
var cancellationToken = console.RegisterCancellationHandler();
public override async ValueTask ExecuteAsync(IConsole console)
{
var cancellationToken = console.RegisterCancellationHandler();
await console.Output.WriteLineAsync("Fetching channels...");
var channels = await Discord.GetGuildChannelsAsync(GuildId, cancellationToken);
var textChannels = channels.Where(c => c.IsTextChannel).ToArray();
await console.Output.WriteLineAsync("Fetching channels...");
var channels = await Discord.GetGuildChannelsAsync(GuildId, cancellationToken);
var textChannels = channels.Where(c => c.IsTextChannel).ToArray();
await base.ExecuteAsync(console, textChannels);
}
await base.ExecuteAsync(console, textChannels);
}
}

View file

@ -7,39 +7,38 @@ using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Utils.Extensions;
namespace DiscordChatExporter.Cli.Commands
namespace DiscordChatExporter.Cli.Commands;
[Command("channels", Description = "Get the list of channels in a guild.")]
public class GetChannelsCommand : TokenCommandBase
{
[Command("channels", Description = "Get the list of channels in a guild.")]
public class GetChannelsCommand : TokenCommandBase
[CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")]
public Snowflake GuildId { get; init; }
public override async ValueTask ExecuteAsync(IConsole console)
{
[CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")]
public Snowflake GuildId { get; init; }
var cancellationToken = console.RegisterCancellationHandler();
public override async ValueTask ExecuteAsync(IConsole console)
var channels = await Discord.GetGuildChannelsAsync(GuildId, cancellationToken);
var textChannels = channels
.Where(c => c.IsTextChannel)
.OrderBy(c => c.Category.Position)
.ThenBy(c => c.Name)
.ToArray();
foreach (var channel in textChannels)
{
var cancellationToken = console.RegisterCancellationHandler();
// Channel ID
await console.Output.WriteAsync(channel.Id.ToString());
var channels = await Discord.GetGuildChannelsAsync(GuildId, cancellationToken);
// Separator
using (console.WithForegroundColor(ConsoleColor.DarkGray))
await console.Output.WriteAsync(" | ");
var textChannels = channels
.Where(c => c.IsTextChannel)
.OrderBy(c => c.Category.Position)
.ThenBy(c => c.Name)
.ToArray();
foreach (var channel in textChannels)
{
// Channel ID
await console.Output.WriteAsync(channel.Id.ToString());
// Separator
using (console.WithForegroundColor(ConsoleColor.DarkGray))
await console.Output.WriteAsync(" | ");
// Channel category / name
using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteLineAsync($"{channel.Category.Name} / {channel.Name}");
}
// Channel category / name
using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteLineAsync($"{channel.Category.Name} / {channel.Name}");
}
}
}

View file

@ -7,36 +7,35 @@ using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Utils.Extensions;
namespace DiscordChatExporter.Cli.Commands
namespace DiscordChatExporter.Cli.Commands;
[Command("dm", Description = "Get the list of direct message channels.")]
public class GetDirectMessageChannelsCommand : TokenCommandBase
{
[Command("dm", Description = "Get the list of direct message channels.")]
public class GetDirectMessageChannelsCommand : TokenCommandBase
public override async ValueTask ExecuteAsync(IConsole console)
{
public override async ValueTask ExecuteAsync(IConsole console)
var cancellationToken = console.RegisterCancellationHandler();
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken);
var textChannels = channels
.Where(c => c.IsTextChannel)
.OrderBy(c => c.Category.Position)
.ThenBy(c => c.Name)
.ToArray();
foreach (var channel in textChannels)
{
var cancellationToken = console.RegisterCancellationHandler();
// Channel ID
await console.Output.WriteAsync(channel.Id.ToString());
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken);
// Separator
using (console.WithForegroundColor(ConsoleColor.DarkGray))
await console.Output.WriteAsync(" | ");
var textChannels = channels
.Where(c => c.IsTextChannel)
.OrderBy(c => c.Category.Position)
.ThenBy(c => c.Name)
.ToArray();
foreach (var channel in textChannels)
{
// Channel ID
await console.Output.WriteAsync(channel.Id.ToString());
// Separator
using (console.WithForegroundColor(ConsoleColor.DarkGray))
await console.Output.WriteAsync(" | ");
// Channel category / name
using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteLineAsync($"{channel.Category.Name} / {channel.Name}");
}
// Channel category / name
using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteLineAsync($"{channel.Category.Name} / {channel.Name}");
}
}
}

View file

@ -6,30 +6,29 @@ using CliFx.Infrastructure;
using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Core.Utils.Extensions;
namespace DiscordChatExporter.Cli.Commands
namespace DiscordChatExporter.Cli.Commands;
[Command("guilds", Description = "Get the list of accessible guilds.")]
public class GetGuildsCommand : TokenCommandBase
{
[Command("guilds", Description = "Get the list of accessible guilds.")]
public class GetGuildsCommand : TokenCommandBase
public override async ValueTask ExecuteAsync(IConsole console)
{
public override async ValueTask ExecuteAsync(IConsole console)
var cancellationToken = console.RegisterCancellationHandler();
var guilds = await Discord.GetUserGuildsAsync(cancellationToken);
foreach (var guild in guilds.OrderBy(g => g.Name))
{
var cancellationToken = console.RegisterCancellationHandler();
// Guild ID
await console.Output.WriteAsync(guild.Id.ToString());
var guilds = await Discord.GetUserGuildsAsync(cancellationToken);
// Separator
using (console.WithForegroundColor(ConsoleColor.DarkGray))
await console.Output.WriteAsync(" | ");
foreach (var guild in guilds.OrderBy(g => g.Name))
{
// Guild ID
await console.Output.WriteAsync(guild.Id.ToString());
// Separator
using (console.WithForegroundColor(ConsoleColor.DarkGray))
await console.Output.WriteAsync(" | ");
// Guild name
using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteLineAsync(guild.Name);
}
// Guild name
using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteLineAsync(guild.Name);
}
}
}

View file

@ -4,68 +4,67 @@ using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
namespace DiscordChatExporter.Cli.Commands
namespace DiscordChatExporter.Cli.Commands;
[Command("guide", Description = "Explains how to obtain token, guild or channel ID.")]
public class GuideCommand : ICommand
{
[Command("guide", Description = "Explains how to obtain token, guild or channel ID.")]
public class GuideCommand : ICommand
public ValueTask ExecuteAsync(IConsole console)
{
public ValueTask ExecuteAsync(IConsole console)
{
// User token
using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get user token:");
// User token
using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get user token:");
console.Output.WriteLine(" 1. Open Discord");
console.Output.WriteLine(" 2. Press Ctrl+Shift+I to show developer tools");
console.Output.WriteLine(" 3. Press Ctrl+Shift+M to toggle device toolbar");
console.Output.WriteLine(" 4. Navigate to the Application tab");
console.Output.WriteLine(" 5. On the left, expand Local Storage and select https://discord.com");
console.Output.WriteLine(" 6. Type \"token\" into the Filter box");
console.Output.WriteLine(" 7. If the token key does not appear, press Ctrl+R to reload");
console.Output.WriteLine(" 8. Copy the value of the token key");
console.Output.WriteLine(" * Automating user accounts is technically against TOS, use at your own risk.");
console.Output.WriteLine();
console.Output.WriteLine(" 1. Open Discord");
console.Output.WriteLine(" 2. Press Ctrl+Shift+I to show developer tools");
console.Output.WriteLine(" 3. Press Ctrl+Shift+M to toggle device toolbar");
console.Output.WriteLine(" 4. Navigate to the Application tab");
console.Output.WriteLine(" 5. On the left, expand Local Storage and select https://discord.com");
console.Output.WriteLine(" 6. Type \"token\" into the Filter box");
console.Output.WriteLine(" 7. If the token key does not appear, press Ctrl+R to reload");
console.Output.WriteLine(" 8. Copy the value of the token key");
console.Output.WriteLine(" * Automating user accounts is technically against TOS, use at your own risk.");
console.Output.WriteLine();
// Bot token
using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get bot token:");
// Bot token
using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get bot token:");
console.Output.WriteLine(" 1. Go to Discord developer portal");
console.Output.WriteLine(" 2. Open your application's settings");
console.Output.WriteLine(" 3. Navigate to the Bot section on the left");
console.Output.WriteLine(" 4. Under Token click Copy");
console.Output.WriteLine();
console.Output.WriteLine(" 1. Go to Discord developer portal");
console.Output.WriteLine(" 2. Open your application's settings");
console.Output.WriteLine(" 3. Navigate to the Bot section on the left");
console.Output.WriteLine(" 4. Under Token click Copy");
console.Output.WriteLine();
// Guild or channel ID
using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get guild ID or guild channel ID:");
// Guild or channel ID
using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get guild ID or guild channel ID:");
console.Output.WriteLine(" 1. Open Discord");
console.Output.WriteLine(" 2. Open Settings");
console.Output.WriteLine(" 3. Go to Appearance section");
console.Output.WriteLine(" 4. Enable Developer Mode");
console.Output.WriteLine(" 5. Right click on the desired guild or channel and click Copy ID");
console.Output.WriteLine();
console.Output.WriteLine(" 1. Open Discord");
console.Output.WriteLine(" 2. Open Settings");
console.Output.WriteLine(" 3. Go to Appearance section");
console.Output.WriteLine(" 4. Enable Developer Mode");
console.Output.WriteLine(" 5. Right click on the desired guild or channel and click Copy ID");
console.Output.WriteLine();
// Direct message channel ID
using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get direct message channel ID:");
// Direct message channel ID
using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get direct message channel ID:");
console.Output.WriteLine(" 1. Open Discord");
console.Output.WriteLine(" 2. Open the desired direct message channel");
console.Output.WriteLine(" 3. Press Ctrl+Shift+I to show developer tools");
console.Output.WriteLine(" 4. Navigate to the Console tab");
console.Output.WriteLine(" 5. Type \"window.location.href\" and press Enter");
console.Output.WriteLine(" 6. Copy the first long sequence of numbers inside the URL");
console.Output.WriteLine();
console.Output.WriteLine(" 1. Open Discord");
console.Output.WriteLine(" 2. Open the desired direct message channel");
console.Output.WriteLine(" 3. Press Ctrl+Shift+I to show developer tools");
console.Output.WriteLine(" 4. Navigate to the Console tab");
console.Output.WriteLine(" 5. Type \"window.location.href\" and press Enter");
console.Output.WriteLine(" 6. Copy the first long sequence of numbers inside the URL");
console.Output.WriteLine();
// Wiki link
using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("For more information, check out the wiki:");
using (console.WithForegroundColor(ConsoleColor.DarkCyan))
console.Output.WriteLine("https://github.com/Tyrrrz/DiscordChatExporter/wiki");
// Wiki link
using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("For more information, check out the wiki:");
using (console.WithForegroundColor(ConsoleColor.DarkCyan))
console.Output.WriteLine("https://github.com/Tyrrrz/DiscordChatExporter/wiki");
return default;
}
return default;
}
}