This commit is contained in:
Tyrrrz 2021-09-16 01:08:14 +03:00
parent af11064a85
commit ea31b1b270
19 changed files with 67 additions and 98 deletions

View file

@ -40,12 +40,10 @@ namespace DiscordChatExporter.Cli.Tests.Specs.HtmlWriting
Snowflake.Parse("867886632203976775") Snowflake.Parse("867886632203976775")
); );
var iframe = message.QuerySelector("iframe"); var iframeSrc = message.QuerySelector("iframe")?.GetAttribute("src");
// Assert // Assert
iframe.Should().NotBeNull(); iframeSrc.Should().StartWithEquivalentOf("https://open.spotify.com/embed/track/1LHZMWefF9502NPfArRfvP");
iframe?.GetAttribute("src").Should()
.StartWithEquivalentOf("https://open.spotify.com/embed/track/1LHZMWefF9502NPfArRfvP");
} }
[Fact] [Fact]
@ -57,12 +55,10 @@ namespace DiscordChatExporter.Cli.Tests.Specs.HtmlWriting
Snowflake.Parse("866472508588294165") Snowflake.Parse("866472508588294165")
); );
var iframe = message.QuerySelector("iframe"); var iframeSrc = message.QuerySelector("iframe")?.GetAttribute("src");
// Assert // Assert
iframe.Should().NotBeNull(); iframeSrc.Should().StartWithEquivalentOf("https://www.youtube.com/embed/qOWW4OlgbvE");
iframe?.GetAttribute("src").Should()
.StartWithEquivalentOf("https://www.youtube.com/embed/qOWW4OlgbvE");
} }
} }
} }

View file

@ -54,18 +54,21 @@ namespace DiscordChatExporter.Cli.Commands.Base
private ChannelExporter? _channelExporter; private ChannelExporter? _channelExporter;
protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord); protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord);
protected async ValueTask ExportAsync(IConsole console, IReadOnlyList<Channel> channels) protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Channel> channels)
{ {
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)..."); if (ShouldReuseMedia && !ShouldDownloadMedia)
{
throw new CommandException("Option --reuse-media cannot be used without --media.");
}
var errors = new ConcurrentDictionary<Channel, string>(); var errors = new ConcurrentDictionary<Channel, string>();
// Wrap everything in a progress ticker // Export
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
await console.CreateProgressTicker().StartAsync(async progressContext => await console.CreateProgressTicker().StartAsync(async progressContext =>
{ {
await channels.ParallelForEachAsync(async channel => await channels.ParallelForEachAsync(async channel =>
{ {
// Export
try try
{ {
await progressContext.StartTaskAsync($"{channel.Category} / {channel.Name}", async progress => await progressContext.StartTaskAsync($"{channel.Category} / {channel.Name}", async progress =>
@ -89,7 +92,7 @@ namespace DiscordChatExporter.Cli.Commands.Base
await Exporter.ExportChannelAsync(request, progress); await Exporter.ExportChannelAsync(request, progress);
}); });
} }
catch (DiscordChatExporterException ex) when (!ex.IsCritical) catch (DiscordChatExporterException ex) when (!ex.IsFatal)
{ {
errors[channel] = ex.Message; errors[channel] = ex.Message;
} }
@ -127,22 +130,25 @@ namespace DiscordChatExporter.Cli.Commands.Base
await console.Output.WriteLineAsync(); await console.Output.WriteLineAsync();
} }
// Fail the command if ALL channels failed to export. // Fail the command only if ALL channels failed to export.
// Having some of the channels fail to export is fine and expected. // Having some of the channels fail to export is expected.
if (errors.Count >= channels.Count) if (errors.Count >= channels.Count)
{ {
throw new CommandException("Export failed."); throw new CommandException("Export failed.");
} }
} }
public override ValueTask ExecuteAsync(IConsole console) protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Snowflake> channelIds)
{ {
if (ShouldReuseMedia && !ShouldDownloadMedia) var channels = new List<Channel>();
foreach (var channelId in channelIds)
{ {
throw new CommandException("Option --reuse-media cannot be used without --media."); var channel = await Discord.GetChannelAsync(channelId);
channels.Add(channel);
} }
return default; await ExecuteAsync(console, channels);
} }
} }
} }

View file

@ -14,7 +14,8 @@ namespace DiscordChatExporter.Cli.Commands.Base
[CommandOption("bot", 'b', EnvironmentVariable = "DISCORD_TOKEN_BOT", Description = "Authenticate as a bot.")] [CommandOption("bot", 'b', EnvironmentVariable = "DISCORD_TOKEN_BOT", Description = "Authenticate as a bot.")]
public bool IsBotToken { get; init; } public bool IsBotToken { get; init; }
private AuthToken GetAuthToken() => new( private AuthToken? _authToken;
private AuthToken AuthToken => _authToken ??= new AuthToken(
IsBotToken IsBotToken
? AuthTokenKind.Bot ? AuthTokenKind.Bot
: AuthTokenKind.User, : AuthTokenKind.User,
@ -22,7 +23,7 @@ namespace DiscordChatExporter.Cli.Commands.Base
); );
private DiscordClient? _discordClient; private DiscordClient? _discordClient;
protected DiscordClient Discord => _discordClient ??= new DiscordClient(GetAuthToken()); protected DiscordClient Discord => _discordClient ??= new DiscordClient(AuthToken);
public abstract ValueTask ExecuteAsync(IConsole console); public abstract ValueTask ExecuteAsync(IConsole console);
} }

View file

@ -15,14 +15,9 @@ namespace DiscordChatExporter.Cli.Commands
public override async ValueTask ExecuteAsync(IConsole console) public override async ValueTask ExecuteAsync(IConsole console)
{ {
await base.ExecuteAsync(console);
// Get channel metadata
await console.Output.WriteLineAsync("Fetching channels...");
var channels = new List<Channel>(); var channels = new List<Channel>();
// Aggregate channels from all guilds await console.Output.WriteLineAsync("Fetching channels...");
await foreach (var guild in Discord.GetUserGuildsAsync()) await foreach (var guild in Discord.GetUserGuildsAsync())
{ {
// Skip DMs if instructed to // Skip DMs if instructed to
@ -39,8 +34,7 @@ namespace DiscordChatExporter.Cli.Commands
} }
} }
// Export await base.ExecuteAsync(console, channels);
await ExportAsync(console, channels);
} }
} }
} }

View file

@ -5,7 +5,6 @@ using CliFx.Attributes;
using CliFx.Infrastructure; using CliFx.Infrastructure;
using DiscordChatExporter.Cli.Commands.Base; using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Cli.Commands namespace DiscordChatExporter.Cli.Commands
{ {
@ -16,23 +15,7 @@ namespace DiscordChatExporter.Cli.Commands
[CommandOption("channel", 'c', IsRequired = true, Description = "Channel ID(s).")] [CommandOption("channel", 'c', IsRequired = true, Description = "Channel ID(s).")]
public IReadOnlyList<Snowflake> ChannelIds { get; init; } = Array.Empty<Snowflake>(); public IReadOnlyList<Snowflake> ChannelIds { get; init; } = Array.Empty<Snowflake>();
public override async ValueTask ExecuteAsync(IConsole console) public override async ValueTask ExecuteAsync(IConsole console) =>
{ await base.ExecuteAsync(console, ChannelIds);
await base.ExecuteAsync(console);
// Get channel metadata
await console.Output.WriteLineAsync("Fetching channel(s)...");
var channels = new List<Channel>();
foreach (var channelId in ChannelIds)
{
var channel = await Discord.GetChannelAsync(channelId);
channels.Add(channel);
}
// Export
await ExportAsync(console, channels);
}
} }
} }

View file

@ -13,15 +13,11 @@ namespace DiscordChatExporter.Cli.Commands
{ {
public override async ValueTask ExecuteAsync(IConsole console) public override async ValueTask ExecuteAsync(IConsole console)
{ {
await base.ExecuteAsync(console);
// Get channel metadata
await console.Output.WriteLineAsync("Fetching channels..."); await console.Output.WriteLineAsync("Fetching channels...");
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id); var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id);
var textChannels = channels.Where(c => c.IsTextChannel).ToArray(); var textChannels = channels.Where(c => c.IsTextChannel).ToArray();
// Export await base.ExecuteAsync(console, textChannels);
await ExportAsync(console, textChannels);
} }
} }
} }

View file

@ -16,15 +16,11 @@ namespace DiscordChatExporter.Cli.Commands
public override async ValueTask ExecuteAsync(IConsole console) public override async ValueTask ExecuteAsync(IConsole console)
{ {
await base.ExecuteAsync(console);
// Get channel metadata
await console.Output.WriteLineAsync("Fetching channels..."); await console.Output.WriteLineAsync("Fetching channels...");
var channels = await Discord.GetGuildChannelsAsync(GuildId); var channels = await Discord.GetGuildChannelsAsync(GuildId);
var textChannels = channels.Where(c => c.IsTextChannel).ToArray(); var textChannels = channels.Where(c => c.IsTextChannel).ToArray();
// Export await base.ExecuteAsync(console, textChannels);
await ExportAsync(console, textChannels);
} }
} }
} }

View file

@ -36,9 +36,7 @@ namespace DiscordChatExporter.Cli.Commands
// Channel category / name // Channel category / name
using (console.WithForegroundColor(ConsoleColor.White)) using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteAsync($"{channel.Category} / {channel.Name}"); await console.Output.WriteLineAsync($"{channel.Category} / {channel.Name}");
await console.Output.WriteLineAsync();
} }
} }
} }

View file

@ -33,9 +33,7 @@ namespace DiscordChatExporter.Cli.Commands
// Channel category / name // Channel category / name
using (console.WithForegroundColor(ConsoleColor.White)) using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteAsync($"{channel.Category} / {channel.Name}"); await console.Output.WriteLineAsync($"{channel.Category} / {channel.Name}");
await console.Output.WriteLineAsync();
} }
} }
} }

View file

@ -26,9 +26,7 @@ namespace DiscordChatExporter.Cli.Commands
// Guild name // Guild name
using (console.WithForegroundColor(ConsoleColor.White)) using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteAsync(guild.Name); await console.Output.WriteLineAsync(guild.Name);
await console.Output.WriteLineAsync();
} }
} }
} }

View file

@ -11,6 +11,7 @@ namespace DiscordChatExporter.Cli.Commands
{ {
public ValueTask ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
// User token
using (console.WithForegroundColor(ConsoleColor.White)) using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get user token:"); console.Output.WriteLine("To get user token:");
@ -25,6 +26,7 @@ namespace DiscordChatExporter.Cli.Commands
console.Output.WriteLine(" * Automating user accounts is technically against TOS, use at your own risk."); console.Output.WriteLine(" * Automating user accounts is technically against TOS, use at your own risk.");
console.Output.WriteLine(); console.Output.WriteLine();
// Bot token
using (console.WithForegroundColor(ConsoleColor.White)) using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get bot token:"); console.Output.WriteLine("To get bot token:");
@ -34,6 +36,7 @@ namespace DiscordChatExporter.Cli.Commands
console.Output.WriteLine(" 4. Under Token click Copy"); console.Output.WriteLine(" 4. Under Token click Copy");
console.Output.WriteLine(); console.Output.WriteLine();
// Guild or channel ID
using (console.WithForegroundColor(ConsoleColor.White)) using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get guild ID or guild channel ID:"); console.Output.WriteLine("To get guild ID or guild channel ID:");
@ -44,6 +47,7 @@ namespace DiscordChatExporter.Cli.Commands
console.Output.WriteLine(" 5. Right click on the desired guild or channel and click Copy ID"); console.Output.WriteLine(" 5. Right click on the desired guild or channel and click Copy ID");
console.Output.WriteLine(); console.Output.WriteLine();
// Direct message channel ID
using (console.WithForegroundColor(ConsoleColor.White)) using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("To get direct message channel ID:"); console.Output.WriteLine("To get direct message channel ID:");
@ -55,9 +59,9 @@ namespace DiscordChatExporter.Cli.Commands
console.Output.WriteLine(" 6. Copy the first long sequence of numbers inside the URL"); console.Output.WriteLine(" 6. Copy the first long sequence of numbers inside the URL");
console.Output.WriteLine(); console.Output.WriteLine();
// Wiki link
using (console.WithForegroundColor(ConsoleColor.White)) using (console.WithForegroundColor(ConsoleColor.White))
console.Output.WriteLine("For more information, check out the wiki:"); console.Output.WriteLine("For more information, check out the wiki:");
using (console.WithForegroundColor(ConsoleColor.DarkCyan)) using (console.WithForegroundColor(ConsoleColor.DarkCyan))
console.Output.WriteLine("https://github.com/Tyrrrz/DiscordChatExporter/wiki"); console.Output.WriteLine("https://github.com/Tyrrrz/DiscordChatExporter/wiki");

View file

@ -27,6 +27,8 @@ namespace DiscordChatExporter.Core.Discord.Data
public partial class ChannelCategory public partial class ChannelCategory
{ {
public static ChannelCategory Unknown { get; } = new(Snowflake.Zero, "<unknown category>", 0);
public static ChannelCategory Parse(JsonElement json, int? position = null) public static ChannelCategory Parse(JsonElement json, int? position = null)
{ {
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse); var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
@ -41,7 +43,5 @@ namespace DiscordChatExporter.Core.Discord.Data
position ?? json.GetPropertyOrNull("position")?.GetInt32() position ?? json.GetPropertyOrNull("position")?.GetInt32()
); );
} }
public static ChannelCategory Empty { get; } = new(Snowflake.Zero, "<unknown category>", 0);
} }
} }

View file

@ -10,20 +10,6 @@ using JsonExtensions.Reading;
namespace DiscordChatExporter.Core.Discord.Data namespace DiscordChatExporter.Core.Discord.Data
{ {
// https://discord.com/developers/docs/resources/channel#message-object-message-types
public enum MessageKind
{
Default = 0,
RecipientAdd = 1,
RecipientRemove = 2,
Call = 3,
ChannelNameChange = 4,
ChannelIconChange = 5,
ChannelPinnedMessage = 6,
GuildMemberJoin = 7,
Reply = 19
}
// https://discord.com/developers/docs/resources/channel#message-object // https://discord.com/developers/docs/resources/channel#message-object
public partial class Message : IHasId public partial class Message : IHasId
{ {

View file

@ -0,0 +1,16 @@
namespace DiscordChatExporter.Core.Discord.Data
{
// https://discord.com/developers/docs/resources/channel#message-object-message-types
public enum MessageKind
{
Default = 0,
RecipientAdd = 1,
RecipientRemove = 2,
Call = 3,
ChannelNameChange = 4,
ChannelIconChange = 5,
ChannelPinnedMessage = 6,
GuildMemberJoin = 7,
Reply = 19
}
}

View file

@ -160,13 +160,13 @@ namespace DiscordChatExporter.Core.Discord
yield return Role.Parse(roleJson); yield return Role.Parse(roleJson);
} }
public async ValueTask<Member?> TryGetGuildMemberAsync(Snowflake guildId, User user) public async ValueTask<Member> GetGuildMemberAsync(Snowflake guildId, User user)
{ {
if (guildId == Guild.DirectMessages.Id) if (guildId == Guild.DirectMessages.Id)
return Member.CreateForUser(user); return Member.CreateForUser(user);
var response = await TryGetJsonResponseAsync($"guilds/{guildId}/members/{user.Id}"); var response = await TryGetJsonResponseAsync($"guilds/{guildId}/members/{user.Id}");
return response?.Pipe(Member.Parse); return response?.Pipe(Member.Parse) ?? Member.CreateForUser(user);
} }
public async ValueTask<ChannelCategory> GetChannelCategoryAsync(Snowflake channelId) public async ValueTask<ChannelCategory> GetChannelCategoryAsync(Snowflake channelId)
@ -180,7 +180,7 @@ namespace DiscordChatExporter.Core.Discord
// Instead, we use an empty channel category as a fallback. // Instead, we use an empty channel category as a fallback.
catch (DiscordChatExporterException) catch (DiscordChatExporterException)
{ {
return ChannelCategory.Empty; return ChannelCategory.Unknown;
} }
} }

View file

@ -5,12 +5,12 @@ namespace DiscordChatExporter.Core.Exceptions
{ {
public partial class DiscordChatExporterException : Exception public partial class DiscordChatExporterException : Exception
{ {
public bool IsCritical { get; } public bool IsFatal { get; }
public DiscordChatExporterException(string message, bool isCritical = false) public DiscordChatExporterException(string message, bool isFatal = false)
: base(message) : base(message)
{ {
IsCritical = isCritical; IsFatal = isFatal;
} }
} }
@ -31,7 +31,7 @@ Failed to perform an HTTP request.
} }
internal static DiscordChatExporterException Unauthorized() => internal static DiscordChatExporterException Unauthorized() =>
new("Authentication token is invalid."); new("Authentication token is invalid.", true);
internal static DiscordChatExporterException Forbidden() => internal static DiscordChatExporterException Forbidden() =>
new("Access is forbidden."); new("Access is forbidden.");

View file

@ -49,10 +49,7 @@ namespace DiscordChatExporter.Core.Exporting
if (!encounteredUsers.Add(referencedUser)) if (!encounteredUsers.Add(referencedUser))
continue; continue;
var member = var member = await _discord.GetGuildMemberAsync(request.Guild.Id, referencedUser);
await _discord.TryGetGuildMemberAsync(request.Guild.Id, referencedUser) ??
Member.CreateForUser(referencedUser);
contextMembers.Add(member); contextMembers.Add(member);
} }

View file

@ -30,7 +30,7 @@ namespace DiscordChatExporter.Core.Exporting
private async ValueTask<MessageWriter> GetWriterAsync() private async ValueTask<MessageWriter> GetWriterAsync()
{ {
// Ensure partition limit has not been exceeded // Ensure partition limit has not been reached
if (_writer is not null && if (_writer is not null &&
_context.Request.PartitionLimit.IsReached(_writer.MessagesWritten, _writer.BytesWritten)) _context.Request.PartitionLimit.IsReached(_writer.MessagesWritten, _writer.BytesWritten))
{ {

View file

@ -175,7 +175,7 @@ namespace DiscordChatExporter.Gui.ViewModels
GuildChannelMap = guildChannelMap; GuildChannelMap = guildChannelMap;
SelectedGuild = guildChannelMap.Keys.FirstOrDefault(); SelectedGuild = guildChannelMap.Keys.FirstOrDefault();
} }
catch (DiscordChatExporterException ex) when (!ex.IsCritical) catch (DiscordChatExporterException ex) when (!ex.IsFatal)
{ {
Notifications.Enqueue(ex.Message.TrimEnd('.')); Notifications.Enqueue(ex.Message.TrimEnd('.'));
} }
@ -234,7 +234,7 @@ namespace DiscordChatExporter.Gui.ViewModels
Interlocked.Increment(ref successfulExportCount); Interlocked.Increment(ref successfulExportCount);
} }
catch (DiscordChatExporterException ex) when (!ex.IsCritical) catch (DiscordChatExporterException ex) when (!ex.IsFatal)
{ {
Notifications.Enqueue(ex.Message.TrimEnd('.')); Notifications.Enqueue(ex.Message.TrimEnd('.'));
} }