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);
}