From e175c9303827420d39e07d0368806a9c927e6821 Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Sun, 16 Jul 2023 22:55:36 +0300 Subject: [PATCH] Refactor --- .../Commands/Base/ExportCommandBase.cs | 25 +++--- .../TruthyBooleanBindingConverter.cs | 3 +- .../Commands/ExportAllCommand.cs | 1 - .../Commands/ExportDirectMessagesCommand.cs | 3 +- .../Commands/ExportGuildCommand.cs | 5 +- .../Commands/GetChannelsCommand.cs | 16 ++-- .../Commands/GetDirectChannelsCommand.cs | 2 +- .../Discord/Data/Channel.cs | 41 ++++++---- .../Discord/Data/ChannelCategory.cs | 37 --------- .../Discord/Data/ChannelThread.cs | 53 ------------- .../Discord/Data/Common/IChannel.cs | 15 ---- .../Discord/Data/Member.cs | 3 +- .../Discord/DiscordClient.cs | 77 +++++++++---------- .../Exporting/ExportContext.cs | 2 +- .../Exporting/ExportRequest.cs | 19 +++-- .../Exporting/JsonMessageWriter.cs | 4 +- .../Exporting/PlainTextMessageWriter.cs | 2 +- .../Exporting/PreambleTemplate.cshtml | 2 +- .../Views/Dialogs/ExportSetupView.xaml | 2 +- 19 files changed, 106 insertions(+), 206 deletions(-) delete mode 100644 DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs delete mode 100644 DiscordChatExporter.Core/Discord/Data/ChannelThread.cs delete mode 100644 DiscordChatExporter.Core/Discord/Data/Common/IChannel.cs diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs index 368ea61..80850a3 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs @@ -11,7 +11,6 @@ using DiscordChatExporter.Cli.Commands.Converters; using DiscordChatExporter.Cli.Utils.Extensions; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; -using DiscordChatExporter.Core.Discord.Data.Common; using DiscordChatExporter.Core.Exceptions; using DiscordChatExporter.Core.Exporting; using DiscordChatExporter.Core.Exporting.Filtering; @@ -137,7 +136,7 @@ public abstract class ExportCommandBase : DiscordCommandBase private ChannelExporter? _channelExporter; protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord); - protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList channels) + protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList channels) { // Asset reuse can only be enabled if the download assets option is set // https://github.com/Tyrrrz/DiscordChatExporter/issues/425 @@ -176,9 +175,9 @@ public abstract class ExportCommandBase : DiscordCommandBase ); } - // Export channels + // Export var cancellationToken = console.RegisterCancellationHandler(); - var channelErrors = new ConcurrentDictionary(); + var errors = new ConcurrentDictionary(); await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)..."); await console.CreateProgressTicker().StartAsync(async progressContext => @@ -195,7 +194,7 @@ public abstract class ExportCommandBase : DiscordCommandBase try { await progressContext.StartTaskAsync( - $"{channel.ParentName} / {channel.Name}", + $"{channel.Category} / {channel.Name}", async progress => { var guild = await Discord.GetGuildAsync(channel.GuildId, innerCancellationToken); @@ -226,7 +225,7 @@ public abstract class ExportCommandBase : DiscordCommandBase } catch (DiscordChatExporterException ex) when (!ex.IsFatal) { - channelErrors[channel] = ex.Message; + errors[channel] = ex.Message; } } ); @@ -236,25 +235,25 @@ public abstract class ExportCommandBase : DiscordCommandBase using (console.WithForegroundColor(ConsoleColor.White)) { await console.Output.WriteLineAsync( - $"Successfully exported {channels.Count - channelErrors.Count} channel(s)." + $"Successfully exported {channels.Count - errors.Count} channel(s)." ); } // Print errors - if (channelErrors.Any()) + if (errors.Any()) { await console.Output.WriteLineAsync(); using (console.WithForegroundColor(ConsoleColor.Red)) { await console.Error.WriteLineAsync( - $"Failed to export {channelErrors.Count} channel(s):" + $"Failed to export {errors.Count} channel(s):" ); } - foreach (var (channel, error) in channelErrors) + foreach (var (channel, error) in errors) { - await console.Error.WriteAsync($"{channel.ParentName} / {channel.Name}: "); + await console.Error.WriteAsync($"{channel.Category} / {channel.Name}: "); using (console.WithForegroundColor(ConsoleColor.Red)) await console.Error.WriteLineAsync(error); @@ -265,7 +264,7 @@ public abstract class ExportCommandBase : DiscordCommandBase // Fail the command only if ALL channels failed to export. // If only some channels failed to export, it's okay. - if (channelErrors.Count >= channels.Count) + if (errors.Count >= channels.Count) throw new CommandException("Export failed."); } @@ -291,7 +290,7 @@ public abstract class ExportCommandBase : DiscordCommandBase foreach (var guildChannel in guildChannels) { - if (guildChannel.Category.Id == channel.Id) + if (guildChannel.Parent?.Id == channel.Id) channels.Add(guildChannel); } diff --git a/DiscordChatExporter.Cli/Commands/Converters/TruthyBooleanBindingConverter.cs b/DiscordChatExporter.Cli/Commands/Converters/TruthyBooleanBindingConverter.cs index c813876..e5bd3bd 100644 --- a/DiscordChatExporter.Cli/Commands/Converters/TruthyBooleanBindingConverter.cs +++ b/DiscordChatExporter.Cli/Commands/Converters/TruthyBooleanBindingConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.Globalization; +using System.Globalization; using CliFx.Extensibility; namespace DiscordChatExporter.Cli.Commands.Converters; diff --git a/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs b/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs index 81980f5..a6525c9 100644 --- a/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.IO.Compression; using System.Text.Json; diff --git a/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs b/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs index c34e60a..b6f7862 100644 --- a/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using CliFx.Attributes; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands.Base; diff --git a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs index 50f2aee..b43d190 100644 --- a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using CliFx.Attributes; using CliFx.Infrastructure; @@ -19,7 +18,7 @@ public class ExportGuildCommand : ExportCommandBase Description = "Guild ID." )] public required Snowflake GuildId { get; init; } - + [CommandOption( "include-vc", Description = "Include voice channels." diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs index ac3bf48..d1f6311 100644 --- a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -32,7 +32,7 @@ public class GetChannelsCommand : DiscordCommandBase var channels = (await Discord.GetGuildChannelsAsync(GuildId, cancellationToken)) .Where(c => c.Kind != ChannelKind.GuildCategory) - .OrderBy(c => c.Category.Position) + .OrderBy(c => c.Parent?.Position) .ThenBy(c => c.Name) .ToArray(); @@ -43,7 +43,7 @@ public class GetChannelsCommand : DiscordCommandBase var threads = IncludeThreads ? (await Discord.GetGuildThreadsAsync(GuildId, cancellationToken)).OrderBy(c => c.Name).ToArray() - : Array.Empty(); + : Array.Empty(); foreach (var channel in channels) { @@ -58,22 +58,22 @@ public class GetChannelsCommand : DiscordCommandBase // Channel category / name using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteLineAsync($"{channel.Category.Name} / {channel.Name}"); + await console.Output.WriteLineAsync($"{channel.Category} / {channel.Name}"); - var channelThreads = threads.Where(t => t.ParentId == channel.Id).ToArray(); + var channelThreads = threads.Where(t => t.Parent?.Id == channel.Id).ToArray(); var channelThreadIdMaxLength = channelThreads .Select(t => t.Id.ToString().Length) .OrderDescending() .FirstOrDefault(); - foreach (var thread in channelThreads) + foreach (var channelThread in channelThreads) { // Indent await console.Output.WriteAsync(" * "); // Thread ID await console.Output.WriteAsync( - thread.Id.ToString().PadRight(channelThreadIdMaxLength, ' ') + channelThread.Id.ToString().PadRight(channelThreadIdMaxLength, ' ') ); // Separator @@ -82,7 +82,7 @@ public class GetChannelsCommand : DiscordCommandBase // Thread name using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteAsync($"Thread / {thread.Name}"); + await console.Output.WriteAsync($"Thread / {channelThread.Name}"); // Separator using (console.WithForegroundColor(ConsoleColor.DarkGray)) @@ -90,7 +90,7 @@ public class GetChannelsCommand : DiscordCommandBase // Thread status using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteLineAsync(thread.IsActive ? "Active" : "Archived"); + await console.Output.WriteLineAsync(channelThread.IsActive ? "Active" : "Archived"); } } } diff --git a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs index ed73153..8a17ac0 100644 --- a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs @@ -40,7 +40,7 @@ public class GetDirectChannelsCommand : DiscordCommandBase // Channel category / name using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteLineAsync($"{channel.Category.Name} / {channel.Name}"); + await console.Output.WriteLineAsync($"{channel.Category} / {channel.Name}"); } } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Data/Channel.cs b/DiscordChatExporter.Core/Discord/Data/Channel.cs index d55f8fc..fc0e092 100644 --- a/DiscordChatExporter.Core/Discord/Data/Channel.cs +++ b/DiscordChatExporter.Core/Discord/Data/Channel.cs @@ -11,23 +11,37 @@ public partial record Channel( Snowflake Id, ChannelKind Kind, Snowflake GuildId, - Snowflake? ParentId, - string? ParentName, - int? ParentPosition, - ChannelCategory Category, + Channel? Parent, string Name, int? Position, string? IconUrl, string? Topic, - Snowflake? LastMessageId) : IChannel + bool IsActive, + Snowflake? LastMessageId) { + // Used for visual backwards-compatibility with old exports, where + // channels without a parent (i.e. mostly DM channels) had a fallback + // category created for them. + public string Category => Parent?.Name ?? Kind switch + { + ChannelKind.GuildCategory => "Category", + ChannelKind.GuildTextChat => "Text", + ChannelKind.DirectTextChat => "Private", + ChannelKind.DirectGroupTextChat => "Group", + ChannelKind.GuildPrivateThread => "Private Thread", + ChannelKind.GuildPublicThread => "Public Thread", + ChannelKind.GuildNews => "News", + ChannelKind.GuildNewsThread => "News Thread", + _ => "Default" + }; + // Only needed for WPF data binding. Don't use anywhere else. public bool IsVoice => Kind.IsVoice(); } public partial record Channel { - public static Channel Parse(JsonElement json, ChannelCategory? categoryHint = null, int? positionHint = null, string? parentName = null, int? parentPosition = null) + public static Channel Parse(JsonElement json, Channel? parent = null, int? positionHint = null) { var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); var kind = (ChannelKind)json.GetProperty("type").GetInt32(); @@ -36,10 +50,6 @@ public partial record Channel json.GetPropertyOrNull("guild_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse) ?? Guild.DirectMessages.Id; - var parentId = json.GetPropertyOrNull("parent_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse); - - var category = categoryHint ?? ChannelCategory.CreateDefault(kind); - var name = // Guild channel json.GetPropertyOrNull("name")?.GetNonWhiteSpaceStringOrNull() ?? @@ -66,6 +76,11 @@ public partial record Channel var topic = json.GetPropertyOrNull("topic")?.GetStringOrNull(); + var isActive = !json + .GetPropertyOrNull("thread_metadata")? + .GetPropertyOrNull("archived")? + .GetBooleanOrNull() ?? true; + var lastMessageId = json .GetPropertyOrNull("last_message_id")? .GetNonWhiteSpaceStringOrNull()? @@ -75,14 +90,12 @@ public partial record Channel id, kind, guildId, - parentId, - parentName, - parentPosition, - category, + parent, name, position, iconUrl, topic, + isActive, lastMessageId ); } diff --git a/DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs b/DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs deleted file mode 100644 index 56bfe65..0000000 --- a/DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text.Json; -using DiscordChatExporter.Core.Discord.Data.Common; -using DiscordChatExporter.Core.Utils.Extensions; -using JsonExtensions.Reading; - -namespace DiscordChatExporter.Core.Discord.Data; - -public record ChannelCategory(Snowflake Id, string Name, int? Position) : IHasId -{ - public static ChannelCategory CreateDefault(ChannelKind channelKind) => new( - Snowflake.Zero, - channelKind switch - { - ChannelKind.GuildTextChat => "Text", - ChannelKind.DirectTextChat => "Private", - ChannelKind.DirectGroupTextChat => "Group", - ChannelKind.GuildNews => "News", - _ => "Default" - }, - null - ); - - public static ChannelCategory Parse(JsonElement json, int? positionHint = null) - { - var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); - - var name = - json.GetPropertyOrNull("name")?.GetNonWhiteSpaceStringOrNull() ?? - id.ToString(); - - var position = - positionHint ?? - json.GetPropertyOrNull("position")?.GetInt32OrNull(); - - return new ChannelCategory(id, name, position); - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Data/ChannelThread.cs b/DiscordChatExporter.Core/Discord/Data/ChannelThread.cs deleted file mode 100644 index 5107023..0000000 --- a/DiscordChatExporter.Core/Discord/Data/ChannelThread.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Text.Json; -using DiscordChatExporter.Core.Discord.Data.Common; -using DiscordChatExporter.Core.Utils.Extensions; -using JsonExtensions.Reading; - -namespace DiscordChatExporter.Core.Discord.Data; - -// https://discord.com/developers/docs/resources/channel#channel-object-example-thread-channel -public record ChannelThread( - Snowflake Id, - ChannelKind Kind, - Snowflake GuildId, - Snowflake? ParentId, - string? ParentName, - string Name, - bool IsActive, - Snowflake? LastMessageId) : IChannel -{ - public int? ParentPosition => null; - public int? Position => null; - public string? IconUrl => null; - public string? Topic => null; - - public static ChannelThread Parse(JsonElement json, string parentName) - { - var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); - var kind = (ChannelKind)json.GetProperty("type").GetInt32(); - var guildId = json.GetProperty("guild_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); - var parentId = json.GetProperty("parent_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); - var name = json.GetProperty("name").GetNonWhiteSpaceString(); - - var isActive = !json - .GetPropertyOrNull("thread_metadata")? - .GetPropertyOrNull("archived")? - .GetBooleanOrNull() ?? true; - - var lastMessageId = json - .GetPropertyOrNull("last_message_id")? - .GetNonWhiteSpaceStringOrNull()? - .Pipe(Snowflake.Parse); - - return new ChannelThread( - id, - kind, - guildId, - parentId, - parentName, - name, - isActive, - lastMessageId - ); - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Data/Common/IChannel.cs b/DiscordChatExporter.Core/Discord/Data/Common/IChannel.cs deleted file mode 100644 index 2aa36bd..0000000 --- a/DiscordChatExporter.Core/Discord/Data/Common/IChannel.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace DiscordChatExporter.Core.Discord.Data.Common; - -public interface IChannel : IHasId -{ - ChannelKind Kind { get; } - Snowflake GuildId { get; } - Snowflake? ParentId { get; } - string? ParentName { get; } - int? ParentPosition { get; } - string Name { get; } - int? Position { get; } - string? IconUrl { get; } - string? Topic { get; } - Snowflake? LastMessageId { get; } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Data/Member.cs b/DiscordChatExporter.Core/Discord/Data/Member.cs index 5ebaf1d..d7393e4 100644 --- a/DiscordChatExporter.Core/Discord/Data/Member.cs +++ b/DiscordChatExporter.Core/Discord/Data/Member.cs @@ -20,7 +20,8 @@ public partial record Member( public partial record Member { - public static Member CreateDefault(User user) => new(user, null, null, Array.Empty()); + public static Member CreateFallback(User user) => + new(user, null, null, Array.Empty()); public static Member Parse(JsonElement json, Snowflake? guildId = null) { diff --git a/DiscordChatExporter.Core/Discord/DiscordClient.cs b/DiscordChatExporter.Core/Discord/DiscordClient.cs index bd0cd2c..46c856a 100644 --- a/DiscordChatExporter.Core/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Core/Discord/DiscordClient.cs @@ -215,14 +215,14 @@ public class DiscordClient var channelsJson = response .EnumerateArray() - .OrderBy(j => j.GetProperty("position").GetInt32()) + .OrderBy(c => c.GetProperty("position").GetInt32()) .ThenBy(j => j.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse)) .ToArray(); - var categories = channelsJson + var parentsById = channelsJson .Where(j => j.GetProperty("type").GetInt32() == (int)ChannelKind.GuildCategory) - .Select((j, index) => ChannelCategory.Parse(j, index + 1)) - .ToDictionary(j => j.Id.ToString(), StringComparer.Ordinal); + .Select((j, i) => Channel.Parse(j, null, i + 1)) + .ToDictionary(j => j.Id); // Discord channel positions are relative, so we need to normalize them // so that the user may refer to them more easily in file name templates. @@ -230,21 +230,19 @@ public class DiscordClient foreach (var channelJson in channelsJson) { - var parentId = channelJson + var parent = channelJson .GetPropertyOrNull("parent_id")? - .GetNonWhiteSpaceStringOrNull(); + .GetNonWhiteSpaceStringOrNull()? + .Pipe(Snowflake.Parse) + .Pipe(parentsById.GetValueOrDefault); - var category = !string.IsNullOrWhiteSpace(parentId) - ? categories.GetValueOrDefault(parentId) - : null; - - yield return Channel.Parse(channelJson, category, position, category?.Name, category?.Position); + yield return Channel.Parse(channelJson, parent, position); position++; } } } - public async IAsyncEnumerable GetGuildThreadsAsync( + public async IAsyncEnumerable GetGuildThreadsAsync( Snowflake guildId, [EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -264,13 +262,14 @@ public class DiscordClient .SetQueryParameter("offset", currentOffset.ToString()) .Build(); + // Can be null on channels that the user cannot access var response = await TryGetJsonResponseAsync(url, cancellationToken); if (response is null) break; foreach (var threadJson in response.Value.GetProperty("threads").EnumerateArray()) { - yield return ChannelThread.Parse(threadJson, channel.Name); + yield return Channel.Parse(threadJson, channel); currentOffset++; } @@ -284,12 +283,18 @@ public class DiscordClient { // Active threads { + var parentsById = channels.ToDictionary(c => c.Id); + var response = await GetJsonResponseAsync($"guilds/{guildId}/threads/active", cancellationToken); foreach (var threadJson in response.GetProperty("threads").EnumerateArray()) { - var parentId = threadJson.GetProperty("parent_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); - var parentChannel = channels.First(t => t.Id == parentId); - yield return ChannelThread.Parse(threadJson, parentChannel.Name); + var parent = threadJson + .GetPropertyOrNull("parent_id")? + .GetNonWhiteSpaceStringOrNull()? + .Pipe(Snowflake.Parse) + .Pipe(parentsById.GetValueOrDefault); + + yield return Channel.Parse(threadJson, parent); } } @@ -303,7 +308,7 @@ public class DiscordClient ); foreach (var threadJson in response.GetProperty("threads").EnumerateArray()) - yield return ChannelThread.Parse(threadJson, channel.Name); + yield return Channel.Parse(threadJson, channel); } // Private archived threads @@ -314,7 +319,7 @@ public class DiscordClient ); foreach (var threadJson in response.GetProperty("threads").EnumerateArray()) - yield return ChannelThread.Parse(threadJson, channel.Name); + yield return Channel.Parse(threadJson, channel); } } } @@ -352,41 +357,33 @@ public class DiscordClient return response?.Pipe(Invite.Parse); } - public async ValueTask GetChannelCategoryAsync( + public async ValueTask TryGetChannelAsync( Snowflake channelId, CancellationToken cancellationToken = default) { - try - { - var response = await GetJsonResponseAsync($"channels/{channelId}", cancellationToken); - return ChannelCategory.Parse(response); - } - // 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); - } - } - - public async ValueTask GetChannelAsync( - Snowflake channelId, - CancellationToken cancellationToken = default) - { - var response = await GetJsonResponseAsync($"channels/{channelId}", cancellationToken); + var response = await TryGetJsonResponseAsync($"channels/{channelId}", cancellationToken); + if (response is null) + return null; var parentId = response + .Value .GetPropertyOrNull("parent_id")? .GetNonWhiteSpaceStringOrNull()? .Pipe(Snowflake.Parse); - var category = parentId is not null - ? await GetChannelCategoryAsync(parentId.Value, cancellationToken) + var parent = parentId is not null + ? await TryGetChannelAsync(parentId.Value, cancellationToken) : null; - return Channel.Parse(response, category, parentName: category?.Name, parentPosition: category?.Position); + return Channel.Parse(response.Value, parent); } + public async ValueTask GetChannelAsync( + Snowflake channelId, + CancellationToken cancellationToken = default) => + await TryGetChannelAsync(channelId, cancellationToken) ?? + throw new InvalidOperationException($"Channel {channelId} not found."); + private async ValueTask TryGetLastMessageAsync( Snowflake channelId, Snowflake? before = null, diff --git a/DiscordChatExporter.Core/Exporting/ExportContext.cs b/DiscordChatExporter.Core/Exporting/ExportContext.cs index 1700992..20f36ed 100644 --- a/DiscordChatExporter.Core/Exporting/ExportContext.cs +++ b/DiscordChatExporter.Core/Exporting/ExportContext.cs @@ -63,7 +63,7 @@ internal class ExportContext // User may have been deleted since they were mentioned if (user is not null) - member = Member.CreateDefault(user); + member = Member.CreateFallback(user); } // Store the result even if it's null, to avoid re-fetching non-existing members diff --git a/DiscordChatExporter.Core/Exporting/ExportRequest.cs b/DiscordChatExporter.Core/Exporting/ExportRequest.cs index 1fd7dcb..ce3ad9b 100644 --- a/DiscordChatExporter.Core/Exporting/ExportRequest.cs +++ b/DiscordChatExporter.Core/Exporting/ExportRequest.cs @@ -4,7 +4,6 @@ using System.Text; using System.Text.RegularExpressions; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; -using DiscordChatExporter.Core.Discord.Data.Common; using DiscordChatExporter.Core.Exporting.Filtering; using DiscordChatExporter.Core.Exporting.Partitioning; using DiscordChatExporter.Core.Utils; @@ -15,7 +14,7 @@ public partial class ExportRequest { public Guild Guild { get; } - public IChannel Channel { get; } + public Channel Channel { get; } public string OutputFilePath { get; } @@ -43,7 +42,7 @@ public partial class ExportRequest public ExportRequest( Guild guild, - IChannel channel, + Channel channel, string outputPath, string? assetsDirPath, ExportFormat format, @@ -95,7 +94,7 @@ public partial class ExportRequest { public static string GetDefaultOutputFileName( Guild guild, - IChannel channel, + Channel channel, ExportFormat format, Snowflake? after = null, Snowflake? before = null) @@ -103,7 +102,7 @@ public partial class ExportRequest var buffer = new StringBuilder(); // Guild and channel names - buffer.Append($"{guild.Name} - {channel.ParentName} - {channel.Name} [{channel.Id}]"); + buffer.Append($"{guild.Name} - {channel.Category} - {channel.Name} [{channel.Id}]"); // Date range if (after is not null || before is not null) @@ -138,7 +137,7 @@ public partial class ExportRequest private static string FormatPath( string path, Guild guild, - IChannel channel, + Channel channel, Snowflake? after, Snowflake? before) { @@ -149,12 +148,12 @@ public partial class ExportRequest { "%g" => guild.Id.ToString(), "%G" => guild.Name, - "%t" => channel.ParentId.ToString() ?? "", - "%T" => channel.ParentName ?? "", + "%t" => channel.Parent?.Id.ToString() ?? "", + "%T" => channel.Parent?.Name ?? "", "%c" => channel.Id.ToString(), "%C" => channel.Name, "%p" => channel.Position?.ToString() ?? "0", - "%P" => channel.ParentPosition?.ToString() ?? "0", + "%P" => channel.Parent?.Position?.ToString() ?? "0", "%a" => after?.ToDate().ToString("yyyy-MM-dd") ?? "", "%b" => before?.ToDate().ToString("yyyy-MM-dd") ?? "", "%d" => DateTimeOffset.Now.ToString("yyyy-MM-dd"), @@ -166,7 +165,7 @@ public partial class ExportRequest private static string GetOutputBaseFilePath( Guild guild, - IChannel channel, + Channel channel, string outputPath, ExportFormat format, Snowflake? after = null, diff --git a/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs b/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs index 1f85917..903e005 100644 --- a/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs @@ -241,8 +241,8 @@ internal class JsonMessageWriter : MessageWriter _writer.WriteStartObject("channel"); _writer.WriteString("id", Context.Request.Channel.Id.ToString()); _writer.WriteString("type", Context.Request.Channel.Kind.ToString()); - _writer.WriteString("categoryId", Context.Request.Channel.ParentId.ToString()); - _writer.WriteString("category", Context.Request.Channel.ParentName); + _writer.WriteString("categoryId", Context.Request.Channel.Parent?.Id.ToString()); + _writer.WriteString("category", Context.Request.Channel.Category); _writer.WriteString("name", Context.Request.Channel.Name); _writer.WriteString("topic", Context.Request.Channel.Topic); diff --git a/DiscordChatExporter.Core/Exporting/PlainTextMessageWriter.cs b/DiscordChatExporter.Core/Exporting/PlainTextMessageWriter.cs index ad4a6a0..076b72b 100644 --- a/DiscordChatExporter.Core/Exporting/PlainTextMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/PlainTextMessageWriter.cs @@ -193,7 +193,7 @@ internal class PlainTextMessageWriter : MessageWriter { await _writer.WriteLineAsync(new string('=', 62)); await _writer.WriteLineAsync($"Guild: {Context.Request.Guild.Name}"); - await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.ParentName} / {Context.Request.Channel.Name}"); + await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.Category} / {Context.Request.Channel.Name}"); if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic)) { diff --git a/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml b/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml index 2e03b79..b7b2353 100644 --- a/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml @@ -1004,7 +1004,7 @@
@Context.Request.Guild.Name
-
@Context.Request.Channel.ParentName / @Context.Request.Channel.Name
+
@Context.Request.Channel.Category / @Context.Request.Channel.Name
@if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic)) { diff --git a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml index 9c9708a..38241f6 100644 --- a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml +++ b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml @@ -60,7 +60,7 @@ FontWeight="Light" TextTrimming="CharacterEllipsis" Visibility="{Binding IsSingleChannel, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"> - +