mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-06-05 01:03:44 -04:00
Refactor
This commit is contained in:
parent
03c5c1bc5e
commit
31c7ae9312
50 changed files with 181 additions and 198 deletions
|
@ -51,7 +51,7 @@ public static class ExportWrapper
|
|||
// Lock separately for each channel and format
|
||||
using (await Locker.LockAsync(filePath))
|
||||
{
|
||||
// Perform export only if it hasn't been done before
|
||||
// Perform the export only if it hasn't been done before
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
await new ExportChannelsCommand
|
||||
|
@ -94,14 +94,13 @@ public static class ExportWrapper
|
|||
|
||||
public static async ValueTask<IElement> GetMessageAsHtmlAsync(Snowflake channelId, Snowflake messageId)
|
||||
{
|
||||
var message = (await GetMessagesAsHtmlAsync(channelId))
|
||||
.SingleOrDefault(e =>
|
||||
string.Equals(
|
||||
e.GetAttribute("data-message-id"),
|
||||
messageId.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
);
|
||||
var message = (await GetMessagesAsHtmlAsync(channelId)).SingleOrDefault(e =>
|
||||
string.Equals(
|
||||
e.GetAttribute("data-message-id"),
|
||||
messageId.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
);
|
||||
|
||||
if (message is null)
|
||||
{
|
||||
|
@ -115,14 +114,13 @@ public static class ExportWrapper
|
|||
|
||||
public static async ValueTask<JsonElement> GetMessageAsJsonAsync(Snowflake channelId, Snowflake messageId)
|
||||
{
|
||||
var message = (await GetMessagesAsJsonAsync(channelId))
|
||||
.SingleOrDefault(j =>
|
||||
string.Equals(
|
||||
j.GetProperty("id").GetString(),
|
||||
messageId.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
);
|
||||
var message = (await GetMessagesAsJsonAsync(channelId)).SingleOrDefault(j =>
|
||||
string.Equals(
|
||||
j.GetProperty("id").GetString(),
|
||||
messageId.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
);
|
||||
|
||||
if (message.ValueKind == JsonValueKind.Undefined)
|
||||
{
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class CsvContentSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Messages_are_exported_correctly()
|
||||
public async Task I_can_export_a_channel_in_the_CSV_format()
|
||||
{
|
||||
// Act
|
||||
var document = await ExportWrapper.ExportAsCsvAsync(ChannelIds.DateRangeTestCases);
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class DateRangeSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Messages_filtered_after_specific_date_only_include_messages_sent_after_that_date()
|
||||
public async Task I_can_filter_the_export_to_only_include_messages_sent_after_the_specified_date()
|
||||
{
|
||||
// Arrange
|
||||
var after = new DateTimeOffset(2021, 07, 24, 0, 0, 0, TimeSpan.Zero);
|
||||
|
@ -61,7 +61,7 @@ public class DateRangeSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Messages_filtered_before_specific_date_only_include_messages_sent_before_that_date()
|
||||
public async Task I_can_filter_the_export_to_only_include_messages_sent_before_the_specified_date()
|
||||
{
|
||||
// Arrange
|
||||
var before = new DateTimeOffset(2021, 07, 24, 0, 0, 0, TimeSpan.Zero);
|
||||
|
@ -103,7 +103,7 @@ public class DateRangeSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Messages_filtered_between_specific_dates_only_include_messages_sent_between_those_dates()
|
||||
public async Task I_can_filter_the_export_to_only_include_messages_sent_between_the_specified_dates()
|
||||
{
|
||||
// Arrange
|
||||
var after = new DateTimeOffset(2021, 07, 24, 0, 0, 0, TimeSpan.Zero);
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class FilterSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Messages_filtered_by_text_only_include_messages_that_contain_that_text()
|
||||
public async Task I_can_filter_the_export_to_only_include_messages_that_contain_the_specified_text()
|
||||
{
|
||||
// Arrange
|
||||
using var file = TempFile.Create();
|
||||
|
@ -42,7 +42,7 @@ public class FilterSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Messages_filtered_by_author_only_include_messages_sent_by_that_author()
|
||||
public async Task I_can_filter_the_export_to_only_include_messages_that_were_sent_by_the_specified_author()
|
||||
{
|
||||
// Arrange
|
||||
using var file = TempFile.Create();
|
||||
|
@ -68,7 +68,7 @@ public class FilterSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Messages_filtered_by_content_only_include_messages_that_have_that_content()
|
||||
public async Task I_can_filter_the_export_to_only_include_messages_that_contain_the_specified_content()
|
||||
{
|
||||
// Arrange
|
||||
using var file = TempFile.Create();
|
||||
|
@ -94,7 +94,7 @@ public class FilterSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Messages_filtered_by_pin_only_include_messages_that_have_been_pinned()
|
||||
public async Task I_can_filter_the_export_to_only_include_messages_that_have_been_pinned()
|
||||
{
|
||||
// Arrange
|
||||
using var file = TempFile.Create();
|
||||
|
@ -120,7 +120,7 @@ public class FilterSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Messages_filtered_by_mention_only_include_messages_that_have_that_mention()
|
||||
public async Task I_can_filter_the_export_to_only_include_messages_that_contain_the_specified_mention()
|
||||
{
|
||||
// Arrange
|
||||
using var file = TempFile.Create();
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class HtmlAttachmentSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Message_with_a_generic_attachment_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_generic_attachment()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
@ -36,7 +36,7 @@ public class HtmlAttachmentSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_an_image_attachment_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_an_image_attachment()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
@ -57,7 +57,7 @@ public class HtmlAttachmentSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_video_attachment_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_video_attachment()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/333
|
||||
|
||||
|
@ -77,7 +77,7 @@ public class HtmlAttachmentSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_an_audio_attachment_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_an_audio_attachment()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/333
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class HtmlContentSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Messages_are_exported_correctly()
|
||||
public async Task I_can_export_a_channel_in_the_HTML_format()
|
||||
{
|
||||
// Act
|
||||
var messages = await ExportWrapper.GetMessagesAsHtmlAsync(ChannelIds.DateRangeTestCases);
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class HtmlEmbedSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Message_with_an_embed_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_rich_embed()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
@ -33,7 +33,7 @@ public class HtmlEmbedSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_an_image_link_is_rendered_with_an_image_embed()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_an_image_embed()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/537
|
||||
|
||||
|
@ -54,7 +54,7 @@ public class HtmlEmbedSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_an_image_link_and_nothing_else_is_rendered_without_text_content()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_an_image_embed_and_the_text_is_hidden_if_it_only_contains_the_image_link()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/682
|
||||
|
||||
|
@ -70,7 +70,7 @@ public class HtmlEmbedSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_video_link_is_rendered_with_a_video_embed()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_video_embed()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
@ -89,7 +89,7 @@ public class HtmlEmbedSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_GIFV_link_is_rendered_with_a_video_embed()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_GIFV_embed()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
@ -108,7 +108,7 @@ public class HtmlEmbedSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_GIFV_link_and_nothing_else_is_rendered_without_text_content()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_GIFV_embed_and_the_text_is_hidden_if_it_only_contains_the_video_link()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
@ -122,7 +122,7 @@ public class HtmlEmbedSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_Spotify_track_link_is_rendered_with_a_track_embed()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_Spotify_track_embed()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/657
|
||||
|
||||
|
@ -138,7 +138,7 @@ public class HtmlEmbedSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_YouTube_video_link_is_rendered_with_a_video_embed()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_YouTube_video_embed()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/570
|
||||
|
||||
|
@ -154,7 +154,7 @@ public class HtmlEmbedSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_Twitter_post_link_with_multiple_images_is_rendered_as_a_single_embed()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_Twitter_post_embed_that_includes_multiple_images()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/695
|
||||
|
||||
|
@ -180,7 +180,7 @@ public class HtmlEmbedSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_guild_invite_link_is_rendered_with_a_widget()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_guild_invite()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/649
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class HtmlGroupingSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Messages_are_grouped_correctly()
|
||||
public async Task I_can_export_a_channel_and_the_messages_are_grouped_according_to_their_author_and_timestamps()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/152
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class HtmlMarkdownSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Message_with_a_timestamp_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker()
|
||||
{
|
||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
||||
|
@ -36,7 +36,7 @@ public class HtmlMarkdownSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_short_time_timestamp_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_short_timestamp_marker()
|
||||
{
|
||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
||||
|
@ -60,7 +60,7 @@ public class HtmlMarkdownSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_long_time_timestamp_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_long_timestamp_marker()
|
||||
{
|
||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
||||
|
@ -84,7 +84,7 @@ public class HtmlMarkdownSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_short_date_timestamp_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_short_date_timestamp_marker()
|
||||
{
|
||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
||||
|
@ -108,7 +108,7 @@ public class HtmlMarkdownSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_long_date_timestamp_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_long_date_timestamp_marker()
|
||||
{
|
||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
||||
|
@ -132,7 +132,7 @@ public class HtmlMarkdownSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_full_timestamp_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_full_timestamp_marker()
|
||||
{
|
||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
||||
|
@ -156,7 +156,7 @@ public class HtmlMarkdownSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_full_long_timestamp_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_full_long_timestamp_marker()
|
||||
{
|
||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
||||
|
@ -180,7 +180,7 @@ public class HtmlMarkdownSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_relative_timestamp_is_rendered_as_the_default_timestamp()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_relative_timestamp_marker()
|
||||
{
|
||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
||||
|
@ -204,7 +204,7 @@ public class HtmlMarkdownSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_an_invalid_timestamp_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_an_invalid_timestamp_marker()
|
||||
{
|
||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class HtmlMentionSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Message_with_a_user_mention_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_user_mention()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
@ -24,7 +24,7 @@ public class HtmlMentionSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_text_channel_mention_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_text_channel_mention()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
@ -37,7 +37,7 @@ public class HtmlMentionSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_voice_channel_mention_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_voice_channel_mention()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
@ -50,7 +50,7 @@ public class HtmlMentionSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_role_mention_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_role_mention()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class HtmlReplySpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Message_with_a_reply_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_that_replies_to_another_message()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
@ -24,7 +24,7 @@ public class HtmlReplySpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_reply_to_a_deleted_message_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_that_replies_to_a_deleted_message()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/645
|
||||
|
||||
|
@ -42,7 +42,7 @@ public class HtmlReplySpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_reply_to_an_empty_message_with_attachment_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_that_replies_to_an_empty_message_with_an_attachment()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/634
|
||||
|
||||
|
@ -58,7 +58,7 @@ public class HtmlReplySpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_reply_to_an_interaction_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_that_replies_to_an_interaction()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/569
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class HtmlStickerSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Message_with_a_PNG_based_sticker_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_PNG_sticker()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
@ -23,7 +23,7 @@ public class HtmlStickerSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_Lottie_based_sticker_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_Lottie_sticker()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class JsonAttachmentSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Message_with_a_generic_attachment_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_generic_attachment()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||
|
@ -23,15 +23,16 @@ public class JsonAttachmentSpecs
|
|||
|
||||
var attachments = message.GetProperty("attachments").EnumerateArray().ToArray();
|
||||
attachments.Should().HaveCount(1);
|
||||
attachments.Single().GetProperty("url").GetString().Should().Be(
|
||||
|
||||
attachments[0].GetProperty("url").GetString().Should().Be(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885587844964417596/Test.txt"
|
||||
);
|
||||
attachments.Single().GetProperty("fileName").GetString().Should().Be("Test.txt");
|
||||
attachments.Single().GetProperty("fileSizeBytes").GetInt64().Should().Be(11);
|
||||
attachments[0].GetProperty("fileName").GetString().Should().Be("Test.txt");
|
||||
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(11);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_an_image_attachment_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_an_image_attachment()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||
|
@ -44,15 +45,16 @@ public class JsonAttachmentSpecs
|
|||
|
||||
var attachments = message.GetProperty("attachments").EnumerateArray().ToArray();
|
||||
attachments.Should().HaveCount(1);
|
||||
attachments.Single().GetProperty("url").GetString().Should().Be(
|
||||
|
||||
attachments[0].GetProperty("url").GetString().Should().Be(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885654862430359613/bird-thumbnail.png"
|
||||
);
|
||||
attachments.Single().GetProperty("fileName").GetString().Should().Be("bird-thumbnail.png");
|
||||
attachments.Single().GetProperty("fileSizeBytes").GetInt64().Should().Be(466335);
|
||||
attachments[0].GetProperty("fileName").GetString().Should().Be("bird-thumbnail.png");
|
||||
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(466335);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_video_attachment_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_video_attachment()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||
|
@ -65,15 +67,16 @@ public class JsonAttachmentSpecs
|
|||
|
||||
var attachments = message.GetProperty("attachments").EnumerateArray().ToArray();
|
||||
attachments.Should().HaveCount(1);
|
||||
attachments.Single().GetProperty("url").GetString().Should().Be(
|
||||
|
||||
attachments[0].GetProperty("url").GetString().Should().Be(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885655761512968233/file_example_MP4_640_3MG.mp4"
|
||||
);
|
||||
attachments.Single().GetProperty("fileName").GetString().Should().Be("file_example_MP4_640_3MG.mp4");
|
||||
attachments.Single().GetProperty("fileSizeBytes").GetInt64().Should().Be(3114374);
|
||||
attachments[0].GetProperty("fileName").GetString().Should().Be("file_example_MP4_640_3MG.mp4");
|
||||
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(3114374);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_an_audio_attachment_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_an_audio_attachment()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||
|
@ -86,10 +89,11 @@ public class JsonAttachmentSpecs
|
|||
|
||||
var attachments = message.GetProperty("attachments").EnumerateArray().ToArray();
|
||||
attachments.Should().HaveCount(1);
|
||||
attachments.Single().GetProperty("url").GetString().Should().Be(
|
||||
|
||||
attachments[0].GetProperty("url").GetString().Should().Be(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885656175348187146/file_example_MP3_1MG.mp3"
|
||||
);
|
||||
attachments.Single().GetProperty("fileName").GetString().Should().Be("file_example_MP3_1MG.mp3");
|
||||
attachments.Single().GetProperty("fileSizeBytes").GetInt64().Should().Be(1087849);
|
||||
attachments[0].GetProperty("fileName").GetString().Should().Be("file_example_MP3_1MG.mp3");
|
||||
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(1087849);
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class JsonContentSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Messages_are_exported_correctly()
|
||||
public async Task I_can_export_a_channel_in_the_JSON_format()
|
||||
{
|
||||
// Act
|
||||
var messages = await ExportWrapper.GetMessagesAsJsonAsync(ChannelIds.DateRangeTestCases);
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class JsonEmbedSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Message_with_an_embed_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_rich_embed()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class JsonMentionSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Message_with_a_user_mention_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_user_mention()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||
|
@ -30,7 +30,7 @@ public class JsonMentionSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_text_channel_mention_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_text_channel_mention()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||
|
@ -43,7 +43,7 @@ public class JsonMentionSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_voice_channel_mention_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_voice_channel_mention()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||
|
@ -56,7 +56,7 @@ public class JsonMentionSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_role_mention_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_role_mention()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class JsonStickerSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Message_with_a_PNG_based_sticker_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_PNG_sticker()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||
|
@ -31,7 +31,7 @@ public class JsonStickerSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_Lottie_based_sticker_is_rendered_correctly()
|
||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_Lottie_sticker()
|
||||
{
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||
|
|
|
@ -14,7 +14,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class PartitioningSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Messages_partitioned_by_count_are_split_into_a_corresponding_number_of_files()
|
||||
public async Task I_can_export_a_channel_with_partitioning_based_on_message_count()
|
||||
{
|
||||
// Arrange
|
||||
using var dir = TempDir.Create();
|
||||
|
@ -37,7 +37,7 @@ public class PartitioningSpecs
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Messages_partitioned_by_file_size_are_split_into_a_corresponding_number_of_files()
|
||||
public async Task I_can_export_a_channel_with_partitioning_based_on_file_size()
|
||||
{
|
||||
// Arrange
|
||||
using var dir = TempDir.Create();
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class PlainTextContentSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Messages_are_exported_correctly()
|
||||
public async Task I_can_export_a_channel_in_the_TXT_format()
|
||||
{
|
||||
// Act
|
||||
var document = await ExportWrapper.ExportAsPlainTextAsync(ChannelIds.DateRangeTestCases);
|
||||
|
|
|
@ -14,7 +14,7 @@ namespace DiscordChatExporter.Cli.Tests.Specs;
|
|||
public class SelfContainedSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Messages_in_self_contained_export_only_reference_local_file_resources()
|
||||
public async Task I_can_export_a_channel_and_download_all_referenced_assets()
|
||||
{
|
||||
// Arrange
|
||||
using var dir = TempDir.Create();
|
||||
|
|
|
@ -30,8 +30,8 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
'o',
|
||||
Description =
|
||||
"Output file or directory path. " +
|
||||
"If a directory is specified, file names will be generated automatically based on the channel names and other parameters. " +
|
||||
"Directory path should end with a slash to avoid ambiguity. " +
|
||||
"Directory path must end with a slash to avoid ambiguity. " +
|
||||
"If a directory is specified, file names will be generated automatically. " +
|
||||
"Supports template tokens, see the documentation for more info."
|
||||
)]
|
||||
public string OutputPath
|
||||
|
@ -65,13 +65,16 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
"partition",
|
||||
'p',
|
||||
Description =
|
||||
"Split output into partitions, each limited to this number of messages (e.g. '100') or file size (e.g. '10mb')."
|
||||
"Split the output into partitions, each limited to the specified " +
|
||||
"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')."
|
||||
Description =
|
||||
"Only include messages that satisfy this filter. " +
|
||||
"See the documentation for more info."
|
||||
)]
|
||||
public MessageFilter MessageFilter { get; init; } = MessageFilter.Null;
|
||||
|
||||
|
@ -103,7 +106,9 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
|
||||
[CommandOption(
|
||||
"media-dir",
|
||||
Description = "Download assets to this directory. If not specified, the asset directory path will be derived from the output path."
|
||||
Description =
|
||||
"Download assets to this directory. " +
|
||||
"If not specified, the asset directory path will be derived from the output path."
|
||||
)]
|
||||
public string? AssetsDirPath
|
||||
{
|
||||
|
@ -123,6 +128,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
"fuck-russia",
|
||||
EnvironmentVariable = "FUCK_RUSSIA",
|
||||
Description = "Don't print the Support Ukraine message to the console.",
|
||||
// Use a converter to accept '1' as 'true' to reuse the existing environment variable
|
||||
Converter = typeof(TruthyBooleanBindingConverter)
|
||||
)]
|
||||
public bool IsUkraineSupportMessageDisabled { get; init; }
|
||||
|
@ -132,7 +138,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
|
||||
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Channel> channels)
|
||||
{
|
||||
// Reuse assets option should only be used when the download assets option is set.
|
||||
// Asset reuse can only be enabled if the download assets option is set
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/425
|
||||
if (ShouldReuseAssets && !ShouldDownloadAssets)
|
||||
{
|
||||
|
@ -141,7 +147,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
);
|
||||
}
|
||||
|
||||
// Assets directory should only be specified when the download assets option is set
|
||||
// Assets directory can only be specified if the download assets option is set
|
||||
if (!string.IsNullOrWhiteSpace(AssetsDirPath) && !ShouldDownloadAssets)
|
||||
{
|
||||
throw new CommandException(
|
||||
|
@ -149,8 +155,8 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
);
|
||||
}
|
||||
|
||||
// Make sure the user does not try to export all channels into a single file.
|
||||
// Output path must either be a directory, or contain template tokens.
|
||||
// Make sure the user does not try to export multiple channels into one file.
|
||||
// Output path must either be a directory or contain template tokens for this to work.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/799
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/917
|
||||
var isValidOutputPath =
|
||||
|
@ -225,7 +231,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
);
|
||||
});
|
||||
|
||||
// Print result
|
||||
// Print the result
|
||||
using (console.WithForegroundColor(ConsoleColor.White))
|
||||
{
|
||||
await console.Output.WriteLineAsync(
|
||||
|
@ -240,28 +246,26 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
|
||||
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||
{
|
||||
await console.Output.WriteLineAsync(
|
||||
await console.Error.WriteLineAsync(
|
||||
$"Failed to export {errors.Count} channel(s):"
|
||||
);
|
||||
}
|
||||
|
||||
foreach (var (channel, error) in errors)
|
||||
{
|
||||
await console.Output.WriteAsync($"{channel.Category.Name} / {channel.Name}: ");
|
||||
await console.Error.WriteAsync($"{channel.Category.Name} / {channel.Name}: ");
|
||||
|
||||
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||
await console.Output.WriteLineAsync(error);
|
||||
await console.Error.WriteLineAsync(error);
|
||||
}
|
||||
|
||||
await console.Output.WriteLineAsync();
|
||||
await console.Error.WriteLineAsync();
|
||||
}
|
||||
|
||||
// Fail the command only if ALL channels failed to export.
|
||||
// Having some of the channels fail to export is expected.
|
||||
// If only some channels failed to export, it's okay.
|
||||
if (errors.Count >= channels.Count)
|
||||
{
|
||||
throw new CommandException("Export failed.");
|
||||
}
|
||||
}
|
||||
|
||||
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Snowflake> channelIds)
|
||||
|
|
|
@ -13,7 +13,7 @@ using JsonExtensions.Reading;
|
|||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
|
||||
[Command("exportall", Description = "Export all accessible channels.")]
|
||||
[Command("exportall", Description = "Exports all accessible channels.")]
|
||||
public class ExportAllCommand : ExportCommandBase
|
||||
{
|
||||
[CommandOption(
|
||||
|
@ -30,7 +30,9 @@ public class ExportAllCommand : ExportCommandBase
|
|||
|
||||
[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."
|
||||
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; init; }
|
||||
|
||||
|
@ -62,7 +64,7 @@ public class ExportAllCommand : ExportCommandBase
|
|||
|
||||
var entry = archive.GetEntry("messages/index.json");
|
||||
if (entry is null)
|
||||
throw new CommandException("Cannot find channel index inside the data package.");
|
||||
throw new CommandException("Could not find channel index inside the data package.");
|
||||
|
||||
await using var stream = entry.Open();
|
||||
using var document = await JsonDocument.ParseAsync(stream, default, cancellationToken);
|
||||
|
@ -85,7 +87,7 @@ public class ExportAllCommand : ExportCommandBase
|
|||
}
|
||||
catch (DiscordChatExporterException)
|
||||
{
|
||||
await console.Output.WriteLineAsync($"Channel '{channelName}' ({channelId}) is inaccessible.");
|
||||
await console.Error.WriteLineAsync($"Channel '{channelName}' ({channelId}) is inaccessible.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,16 @@ using DiscordChatExporter.Core.Discord;
|
|||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
|
||||
[Command("export", Description = "Export one or multiple channels.")]
|
||||
[Command("export", Description = "Exports one or multiple channels.")]
|
||||
public class ExportChannelsCommand : ExportCommandBase
|
||||
{
|
||||
// TODO: change this to plural (breaking change)
|
||||
[CommandOption(
|
||||
"channel",
|
||||
'c',
|
||||
Description = "Channel ID(s). If provided with category IDs, all channels inside those categories will be exported."
|
||||
Description =
|
||||
"Channel ID(s). " +
|
||||
"If provided with category ID(s), all channels inside those categories will be exported."
|
||||
)]
|
||||
public required IReadOnlyList<Snowflake> ChannelIds { get; init; }
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ using DiscordChatExporter.Core.Utils.Extensions;
|
|||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
|
||||
[Command("exportdm", Description = "Export all direct message channels.")]
|
||||
[Command("exportdm", Description = "Exports all direct message channels.")]
|
||||
public class ExportDirectMessagesCommand : ExportCommandBase
|
||||
{
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
|
|
|
@ -9,7 +9,7 @@ using DiscordChatExporter.Core.Utils.Extensions;
|
|||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
|
||||
[Command("exportguild", Description = "Export all channels within specified guild.")]
|
||||
[Command("exportguild", Description = "Exports all channels within the specified guild.")]
|
||||
public class ExportGuildCommand : ExportCommandBase
|
||||
{
|
||||
[CommandOption(
|
||||
|
|
|
@ -22,7 +22,7 @@ public class GetChannelsCommand : DiscordCommandBase
|
|||
|
||||
[CommandOption(
|
||||
"include-threads",
|
||||
Description = "Display threads alongside channels."
|
||||
Description = "Include threads in the output."
|
||||
)]
|
||||
public bool IncludeThreads { get; init; }
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ using DiscordChatExporter.Core.Utils.Extensions;
|
|||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
|
||||
[Command("dm", Description = "Get the list of direct message channels.")]
|
||||
[Command("dm", Description = "Gets the list of all direct message channels.")]
|
||||
public class GetDirectChannelsCommand : DiscordCommandBase
|
||||
{
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
|
|
|
@ -9,7 +9,7 @@ using DiscordChatExporter.Core.Utils.Extensions;
|
|||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
|
||||
[Command("guilds", Description = "Get the list of accessible guilds.")]
|
||||
[Command("guilds", Description = "Gets the list of accessible guilds.")]
|
||||
public class GetGuildsCommand : DiscordCommandBase
|
||||
{
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
|
|
|
@ -6,7 +6,7 @@ using CliFx.Infrastructure;
|
|||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
|
||||
[Command("guide", Description = "Explains how to obtain token, guild or channel ID.")]
|
||||
[Command("guide", Description = "Explains how to obtain the token, guild or channel ID.")]
|
||||
public class GuideCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
namespace DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
// Order of enum fields needs to match the order in the docs.
|
||||
public enum ChannelKind
|
||||
{
|
||||
GuildTextChat = 0,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using JsonExtensions.Reading;
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord.Data.Common;
|
||||
|
||||
public class IdBasedEqualityComparer : IEqualityComparer<IHasId>
|
||||
{
|
||||
public static IdBasedEqualityComparer Instance { get; } = new();
|
||||
|
||||
public bool Equals(IHasId? x, IHasId? y) => x?.Id == y?.Id;
|
||||
|
||||
public int GetHashCode(IHasId obj) => obj.Id.GetHashCode();
|
||||
}
|
|
@ -34,8 +34,7 @@ public partial record Emoji
|
|||
if (!string.IsNullOrWhiteSpace(name))
|
||||
return ImageCdn.GetStandardEmojiUrl(name);
|
||||
|
||||
// Either ID or name should be set
|
||||
throw new ApplicationException("Emoji has neither ID nor name set.");
|
||||
throw new InvalidOperationException("Either the emoji ID or name should be provided.");
|
||||
}
|
||||
|
||||
public static Emoji Parse(JsonElement json)
|
||||
|
|
|
@ -17,5 +17,5 @@ public enum MessageKind
|
|||
|
||||
public static class MessageKindExtensions
|
||||
{
|
||||
public static bool IsSystemNotification(this MessageKind c) => (int)c is >= 1 and <= 18;
|
||||
public static bool IsSystemNotification(this MessageKind kind) => (int)kind is >= 1 and <= 18;
|
||||
}
|
|
@ -238,11 +238,8 @@ public class DiscordClient
|
|||
? categories.GetValueOrDefault(parentId)
|
||||
: null;
|
||||
|
||||
var channel = Channel.Parse(channelJson, category, position);
|
||||
|
||||
yield return Channel.Parse(channelJson, category, position);
|
||||
position++;
|
||||
|
||||
yield return channel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -288,8 +285,8 @@ public class DiscordClient
|
|||
var response = await GetJsonResponseAsync($"channels/{channelId}", cancellationToken);
|
||||
return ChannelCategory.Parse(response);
|
||||
}
|
||||
// In some cases, the Discord API returns an empty body when requesting a channel.
|
||||
// Return an empty channel category as fallback in these cases.
|
||||
// In some cases, Discord API returns an empty body when requesting a channel.
|
||||
// Use an empty channel category as fallback for these cases.
|
||||
catch (DiscordChatExporterException)
|
||||
{
|
||||
return new ChannelCategory(channelId, "Unknown Category", 0);
|
||||
|
@ -371,8 +368,9 @@ public class DiscordClient
|
|||
if (lastMessage is null || lastMessage.Timestamp < after?.ToDate())
|
||||
yield break;
|
||||
|
||||
// Keep track of the first message in range in order to calculate progress
|
||||
// Keep track of the first message in range in order to calculate the progress
|
||||
var firstMessage = default(Message);
|
||||
|
||||
var currentAfter = after ?? Snowflake.Zero;
|
||||
while (true)
|
||||
{
|
||||
|
|
|
@ -29,15 +29,11 @@ public partial record struct Snowflake
|
|||
|
||||
// As number
|
||||
if (ulong.TryParse(str, NumberStyles.None, formatProvider, out var value))
|
||||
{
|
||||
return new Snowflake(value);
|
||||
}
|
||||
|
||||
// As date
|
||||
if (DateTimeOffset.TryParse(str, formatProvider, DateTimeStyles.None, out var instant))
|
||||
{
|
||||
return FromDate(instant);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -89,11 +89,15 @@ internal partial class CsvMessageWriter : MessageWriter
|
|||
// Message content
|
||||
if (message.Kind.IsSystemNotification())
|
||||
{
|
||||
await _writer.WriteAsync(CsvEncode(message.GetFallbackContent()));
|
||||
await _writer.WriteAsync(CsvEncode(
|
||||
message.GetFallbackContent()
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
await _writer.WriteAsync(CsvEncode(await FormatMarkdownAsync(message.Content, cancellationToken)));
|
||||
await _writer.WriteAsync(CsvEncode(
|
||||
await FormatMarkdownAsync(message.Content, cancellationToken)
|
||||
));
|
||||
}
|
||||
|
||||
await _writer.WriteAsync(',');
|
||||
|
|
|
@ -75,9 +75,8 @@ internal partial class ExportAssetDownloader
|
|||
catch
|
||||
{
|
||||
// This can apparently fail for some reason.
|
||||
// Updating the file date is not a critical task, so we'll just ignore exceptions thrown here.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/585
|
||||
// Updating file dates is not a critical task, so we'll just
|
||||
// ignore exceptions thrown here.
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -98,7 +97,7 @@ internal partial class ExportAssetDownloader
|
|||
{
|
||||
var urlHash = GetUrlHash(url);
|
||||
|
||||
// Try to extract file name from URL
|
||||
// Try to extract the file name from URL
|
||||
var fileName = Regex.Match(url, @".+/([^?]*)").Groups[1].Value;
|
||||
|
||||
// If it's not there, just use the URL hash as the file name
|
||||
|
@ -110,7 +109,7 @@ internal partial class ExportAssetDownloader
|
|||
var fileExtension = Path.GetExtension(fileName);
|
||||
|
||||
// Probably not a file extension, just a dot in a long file name
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/708
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/pull/812
|
||||
if (fileExtension.Length > 41)
|
||||
{
|
||||
fileNameWithoutExtension = fileName;
|
||||
|
|
|
@ -55,10 +55,13 @@ internal class ExportContext
|
|||
|
||||
var member = await Discord.TryGetGuildMemberAsync(Request.Guild.Id, id, cancellationToken);
|
||||
|
||||
// User may have left the guild since they were mentioned
|
||||
// User may have left the guild since they were mentioned.
|
||||
// Create a dummy member object based on the user info.
|
||||
if (member is null)
|
||||
{
|
||||
var user = fallbackUser ?? await Discord.TryGetUserAsync(id, cancellationToken);
|
||||
|
||||
// User may have been deleted since they were mentioned
|
||||
if (user is not null)
|
||||
member = Member.CreateDefault(user);
|
||||
}
|
||||
|
@ -114,7 +117,7 @@ internal class ExportContext
|
|||
var relativeFilePath = Path.GetRelativePath(Request.OutputDirPath, filePath);
|
||||
|
||||
// Prefer relative paths so that the output files can be copied around without breaking references.
|
||||
// If the assets path is outside of the export directory, use the absolute path instead.
|
||||
// If the assets path is outside of the export directory, use an absolute path instead.
|
||||
var optimalFilePath =
|
||||
relativeFilePath.StartsWith(".." + Path.DirectorySeparatorChar, StringComparison.Ordinal) ||
|
||||
relativeFilePath.StartsWith(".." + Path.AltDirectorySeparatorChar, StringComparison.Ordinal)
|
||||
|
@ -135,8 +138,8 @@ internal class ExportContext
|
|||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/372
|
||||
catch (Exception ex) when (ex is HttpRequestException or OperationCanceledException)
|
||||
{
|
||||
// TODO: add logging so we can be more liberal with catching exceptions
|
||||
// We don't want this to crash the exporting process in case of failure
|
||||
// We don't want this to crash the exporting process in case of failure.
|
||||
// TODO: add logging so we can be more liberal with catching exceptions.
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,12 +52,12 @@ internal class HtmlMessageWriter : MessageWriter
|
|||
if ((message.Timestamp - lastMessage.Timestamp).Duration().TotalMinutes > 7)
|
||||
return false;
|
||||
|
||||
// Messages must be from the same author
|
||||
// Messages must be sent by the same author
|
||||
if (message.Author.Id != lastMessage.Author.Id)
|
||||
return false;
|
||||
|
||||
// If the user changed their name after the last message, their new messages
|
||||
// cannot join an existing group.
|
||||
// If the author changed their name after the last message, their new messages
|
||||
// cannot join the existing group.
|
||||
if (!string.Equals(message.Author.FullName, lastMessage.Author.FullName, StringComparison.Ordinal))
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ internal partial class MessageExporter : IAsyncDisposable
|
|||
|
||||
private async ValueTask<MessageWriter> GetWriterAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Ensure partition limit has not been reached
|
||||
// Ensure that the partition limit has not been reached
|
||||
if (_writer is not null &&
|
||||
_context.Request.PartitionLimit.IsReached(_writer.MessagesWritten, _writer.BytesWritten))
|
||||
{
|
||||
|
@ -74,11 +74,11 @@ internal partial class MessageExporter
|
|||
{
|
||||
private static string GetPartitionFilePath(string baseFilePath, int partitionIndex)
|
||||
{
|
||||
// First partition, don't change file name
|
||||
// First partition, don't change the file name
|
||||
if (partitionIndex <= 0)
|
||||
return baseFilePath;
|
||||
|
||||
// Inject partition index into file name
|
||||
// Inject partition index into the file name
|
||||
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(baseFilePath);
|
||||
var fileExt = Path.GetExtension(baseFilePath);
|
||||
var fileName = $"{fileNameWithoutExt} [part {partitionIndex + 1}]{fileExt}";
|
||||
|
|
|
@ -16,7 +16,7 @@ public static class Http
|
|||
|
||||
private static bool IsRetryableStatusCode(HttpStatusCode statusCode) =>
|
||||
statusCode is HttpStatusCode.TooManyRequests or HttpStatusCode.RequestTimeout ||
|
||||
// Treat all server-side errors as retryable.
|
||||
// Treat all server-side errors as retryable
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/908
|
||||
(int)statusCode >= 500;
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@ public class Bootstrapper : Bootstrapper<RootViewModel>
|
|||
{
|
||||
base.OnStart();
|
||||
|
||||
// Set default theme
|
||||
// (preferred theme will be set later, once the settings are loaded)
|
||||
// Set the default theme.
|
||||
// Preferred theme will be set later, once the settings are loaded.
|
||||
App.SetLightTheme();
|
||||
}
|
||||
|
||||
|
@ -26,10 +26,7 @@ public class Bootstrapper : Bootstrapper<RootViewModel>
|
|||
{
|
||||
base.ConfigureIoC(builder);
|
||||
|
||||
// Bind settings as singleton
|
||||
builder.Bind<SettingsService>().ToSelf().InSingletonScope();
|
||||
|
||||
// Bind view model factory
|
||||
builder.Bind<IViewModelFactory>().ToAbstractFactory();
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ public partial class SettingsService : SettingsBase
|
|||
|
||||
public override void Save()
|
||||
{
|
||||
// Clear token if it's not supposed to be persisted
|
||||
// Clear the token if it's not supposed to be persisted
|
||||
if (!IsTokenPersisted)
|
||||
LastToken = null;
|
||||
|
||||
|
|
|
@ -6,13 +6,15 @@ internal static class ProcessEx
|
|||
{
|
||||
public static void StartShellExecute(string path)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo(path)
|
||||
using var process = new Process
|
||||
{
|
||||
UseShellExecute = true
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = path,
|
||||
UseShellExecute = true
|
||||
}
|
||||
};
|
||||
|
||||
using (Process.Start(startInfo))
|
||||
{
|
||||
}
|
||||
process.Start();
|
||||
}
|
||||
}
|
|
@ -156,14 +156,11 @@ public class DashboardViewModel : PropertyChangedBase
|
|||
|
||||
var exporter = new ChannelExporter(_discord);
|
||||
|
||||
var channelProgressPairs = dialog
|
||||
.Channels!
|
||||
.Select(c => new
|
||||
{
|
||||
Channel = c,
|
||||
Progress = _progressMuxer.CreateInput()
|
||||
})
|
||||
.ToArray();
|
||||
var channelProgressPairs = dialog.Channels!.Select(c => new
|
||||
{
|
||||
Channel = c,
|
||||
Progress = _progressMuxer.CreateInput()
|
||||
}).ToArray();
|
||||
|
||||
var successfulExportCount = 0;
|
||||
|
||||
|
@ -213,7 +210,7 @@ public class DashboardViewModel : PropertyChangedBase
|
|||
}
|
||||
);
|
||||
|
||||
// Notify of overall completion
|
||||
// Notify of the overall completion
|
||||
if (successfulExportCount > 0)
|
||||
{
|
||||
_eventAggregator.Publish(
|
||||
|
|
|
@ -25,7 +25,7 @@ public class DialogManager : IDisposable
|
|||
|
||||
void OnDialogOpened(object? openSender, DialogOpenedEventArgs openArgs)
|
||||
{
|
||||
void OnScreenClosed(object? closeSender, EventArgs args)
|
||||
void OnScreenClosed(object? closeSender, EventArgs closeArgs)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
namespace DiscordChatExporter.Gui.ViewModels.Messages;
|
||||
|
||||
public class NotificationMessage
|
||||
{
|
||||
public string Text { get; }
|
||||
|
||||
public NotificationMessage(string text) => Text = text;
|
||||
}
|
||||
public record NotificationMessage(string Text);
|
|
@ -62,9 +62,7 @@ public class RootViewModel : Screen, IHandle<NotificationMessage>, IDisposable
|
|||
_settingsService.Save();
|
||||
|
||||
if (await _dialogManager.ShowDialogAsync(dialog) == true)
|
||||
{
|
||||
ProcessEx.StartShellExecute("https://tyrrrz.me/ukraine?source=discordchatexporter");
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask CheckForUpdatesAsync()
|
||||
|
@ -106,7 +104,7 @@ public class RootViewModel : Screen, IHandle<NotificationMessage>, IDisposable
|
|||
|
||||
_settingsService.Load();
|
||||
|
||||
// Sync theme with settings
|
||||
// Sync the theme with settings
|
||||
if (_settingsService.IsDarkModeEnabled)
|
||||
{
|
||||
App.SetDarkTheme();
|
||||
|
@ -116,7 +114,7 @@ public class RootViewModel : Screen, IHandle<NotificationMessage>, IDisposable
|
|||
App.SetLightTheme();
|
||||
}
|
||||
|
||||
// App has just been updated, display changelog
|
||||
// App has just been updated, display the changelog
|
||||
if (_settingsService.LastAppVersion is not null && _settingsService.LastAppVersion != App.Version)
|
||||
{
|
||||
Notifications.Enqueue(
|
||||
|
@ -137,8 +135,7 @@ public class RootViewModel : Screen, IHandle<NotificationMessage>, IDisposable
|
|||
_updateService.FinalizeUpdate(false);
|
||||
}
|
||||
|
||||
public void Handle(NotificationMessage message) =>
|
||||
Notifications.Enqueue(message.Text);
|
||||
public void Handle(NotificationMessage message) => Notifications.Enqueue(message.Text);
|
||||
|
||||
public void Dispose() => Notifications.Dispose();
|
||||
}
|
|
@ -221,7 +221,7 @@
|
|||
materialDesign:HintAssist.IsFloating="True"
|
||||
Style="{DynamicResource MaterialDesignOutlinedTextBox}"
|
||||
Text="{Binding PartitionLimitValue}"
|
||||
ToolTip="Split output into partitions, each limited to this number of messages (e.g. '100') or file size (e.g. '10mb')" />
|
||||
ToolTip="Split the output into partitions, each limited to the specified number of messages (e.g. '100') or file size (e.g. '10mb')" />
|
||||
|
||||
<!-- Filtering -->
|
||||
<TextBox
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue