pull/710/head
Tyrrrz 3 years ago
parent af11064a85
commit ea31b1b270

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

@ -54,18 +54,21 @@ namespace DiscordChatExporter.Cli.Commands.Base
private ChannelExporter? _channelExporter;
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>();
// Wrap everything in a progress ticker
// Export
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
await console.CreateProgressTicker().StartAsync(async progressContext =>
{
await channels.ParallelForEachAsync(async channel =>
{
// Export
try
{
await progressContext.StartTaskAsync($"{channel.Category} / {channel.Name}", async progress =>
@ -89,7 +92,7 @@ namespace DiscordChatExporter.Cli.Commands.Base
await Exporter.ExportChannelAsync(request, progress);
});
}
catch (DiscordChatExporterException ex) when (!ex.IsCritical)
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
{
errors[channel] = ex.Message;
}
@ -127,22 +130,25 @@ namespace DiscordChatExporter.Cli.Commands.Base
await console.Output.WriteLineAsync();
}
// Fail the command if ALL channels failed to export.
// Having some of the channels fail to export is fine and expected.
// 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.");
}
}
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);
}
}
}

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

@ -15,14 +15,9 @@ namespace DiscordChatExporter.Cli.Commands
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>();
// Aggregate channels from all guilds
await console.Output.WriteLineAsync("Fetching channels...");
await foreach (var guild in Discord.GetUserGuildsAsync())
{
// Skip DMs if instructed to
@ -39,8 +34,7 @@ namespace DiscordChatExporter.Cli.Commands
}
}
// Export
await ExportAsync(console, channels);
await base.ExecuteAsync(console, channels);
}
}
}

@ -5,7 +5,6 @@ using CliFx.Attributes;
using CliFx.Infrastructure;
using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Cli.Commands
{
@ -16,23 +15,7 @@ namespace DiscordChatExporter.Cli.Commands
[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);
// 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);
}
public override async ValueTask ExecuteAsync(IConsole console) =>
await base.ExecuteAsync(console, ChannelIds);
}
}

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

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

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

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

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

@ -11,6 +11,7 @@ namespace DiscordChatExporter.Cli.Commands
{
public ValueTask ExecuteAsync(IConsole console)
{
// User token
using (console.WithForegroundColor(ConsoleColor.White))
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();
// Bot token
using (console.WithForegroundColor(ConsoleColor.White))
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();
// Guild or channel ID
using (console.WithForegroundColor(ConsoleColor.White))
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();
// Direct message channel ID
using (console.WithForegroundColor(ConsoleColor.White))
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();
// 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");

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

@ -10,20 +10,6 @@ using JsonExtensions.Reading;
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
public partial class Message : IHasId
{

@ -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
}
}

@ -160,13 +160,13 @@ namespace DiscordChatExporter.Core.Discord
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)
return Member.CreateForUser(user);
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)
@ -180,7 +180,7 @@ namespace DiscordChatExporter.Core.Discord
// Instead, we use an empty channel category as a fallback.
catch (DiscordChatExporterException)
{
return ChannelCategory.Empty;
return ChannelCategory.Unknown;
}
}

@ -5,12 +5,12 @@ namespace DiscordChatExporter.Core.Exceptions
{
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)
{
IsCritical = isCritical;
IsFatal = isFatal;
}
}
@ -31,7 +31,7 @@ Failed to perform an HTTP request.
}
internal static DiscordChatExporterException Unauthorized() =>
new("Authentication token is invalid.");
new("Authentication token is invalid.", true);
internal static DiscordChatExporterException Forbidden() =>
new("Access is forbidden.");

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

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

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

Loading…
Cancel
Save