diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs index 503b8db..368ea61 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs @@ -11,6 +11,7 @@ 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; @@ -136,7 +137,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 @@ -175,9 +176,9 @@ public abstract class ExportCommandBase : DiscordCommandBase ); } - // Export + // Export channels var cancellationToken = console.RegisterCancellationHandler(); - var errors = new ConcurrentDictionary(); + var channelErrors = new ConcurrentDictionary(); await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)..."); await console.CreateProgressTicker().StartAsync(async progressContext => @@ -194,7 +195,7 @@ public abstract class ExportCommandBase : DiscordCommandBase try { await progressContext.StartTaskAsync( - $"{channel.Category.Name} / {channel.Name}", + $"{channel.ParentName} / {channel.Name}", async progress => { var guild = await Discord.GetGuildAsync(channel.GuildId, innerCancellationToken); @@ -225,7 +226,7 @@ public abstract class ExportCommandBase : DiscordCommandBase } catch (DiscordChatExporterException ex) when (!ex.IsFatal) { - errors[channel] = ex.Message; + channelErrors[channel] = ex.Message; } } ); @@ -235,25 +236,25 @@ public abstract class ExportCommandBase : DiscordCommandBase using (console.WithForegroundColor(ConsoleColor.White)) { await console.Output.WriteLineAsync( - $"Successfully exported {channels.Count - errors.Count} channel(s)." + $"Successfully exported {channels.Count - channelErrors.Count} channel(s)." ); } // Print errors - if (errors.Any()) + if (channelErrors.Any()) { await console.Output.WriteLineAsync(); using (console.WithForegroundColor(ConsoleColor.Red)) { await console.Error.WriteLineAsync( - $"Failed to export {errors.Count} channel(s):" + $"Failed to export {channelErrors.Count} channel(s):" ); } - foreach (var (channel, error) in errors) + foreach (var (channel, error) in channelErrors) { - await console.Error.WriteAsync($"{channel.Category.Name} / {channel.Name}: "); + await console.Error.WriteAsync($"{channel.ParentName} / {channel.Name}: "); using (console.WithForegroundColor(ConsoleColor.Red)) await console.Error.WriteLineAsync(error); @@ -264,7 +265,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 (errors.Count >= channels.Count) + if (channelErrors.Count >= channels.Count) throw new CommandException("Export failed."); } diff --git a/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs b/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs index a6525c9..81980f5 100644 --- a/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs @@ -1,3 +1,4 @@ +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 b6f7862..c34e60a 100644 --- a/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +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 ee021ba..50f2aee 100644 --- a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Threading.Tasks; using CliFx.Attributes; using CliFx.Infrastructure; @@ -18,13 +19,14 @@ public class ExportGuildCommand : ExportCommandBase Description = "Guild ID." )] public required Snowflake GuildId { get; init; } - + [CommandOption( "include-vc", Description = "Include voice channels." )] public bool IncludeVoiceChannels { get; init; } = true; + public override async ValueTask ExecuteAsync(IConsole console) { await base.ExecuteAsync(console); diff --git a/DiscordChatExporter.Core/Discord/Data/Channel.cs b/DiscordChatExporter.Core/Discord/Data/Channel.cs index a9eb9af..81b2531 100644 --- a/DiscordChatExporter.Core/Discord/Data/Channel.cs +++ b/DiscordChatExporter.Core/Discord/Data/Channel.cs @@ -11,12 +11,15 @@ public partial record Channel( Snowflake Id, ChannelKind Kind, Snowflake GuildId, + Snowflake ParentId, + string? ParentName, + int? ParentPosition, ChannelCategory Category, string Name, int? Position, string? IconUrl, string? Topic, - Snowflake? LastMessageId) : IHasId + Snowflake? LastMessageId) : IChannel { // Only needed for WPF data binding. Don't use anywhere else. public bool IsVoice => Kind.IsVoice(); @@ -24,7 +27,7 @@ public partial record Channel( public partial record Channel { - public static Channel Parse(JsonElement json, ChannelCategory? categoryHint = null, int? positionHint = null) + public static Channel Parse(JsonElement json, ChannelCategory? categoryHint = null, int? positionHint = null, string? parentName = null, int? parentPosition = null) { var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); var kind = (ChannelKind)json.GetProperty("type").GetInt32(); @@ -33,6 +36,8 @@ public partial record Channel json.GetPropertyOrNull("guild_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse) ?? Guild.DirectMessages.Id; + var parentId = json.GetProperty("parent_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); + var category = categoryHint ?? ChannelCategory.CreateDefault(kind); var name = @@ -70,6 +75,9 @@ public partial record Channel id, kind, guildId, + parentId, + parentName, + parentPosition, category, name, position, diff --git a/DiscordChatExporter.Core/Discord/Data/ChannelThread.cs b/DiscordChatExporter.Core/Discord/Data/ChannelThread.cs index 9f5d6e2..412d923 100644 --- a/DiscordChatExporter.Core/Discord/Data/ChannelThread.cs +++ b/DiscordChatExporter.Core/Discord/Data/ChannelThread.cs @@ -11,11 +11,17 @@ public record ChannelThread( ChannelKind Kind, Snowflake GuildId, Snowflake ParentId, + string? ParentName, string Name, bool IsActive, - Snowflake? LastMessageId) : IHasId + Snowflake? LastMessageId) : IChannel { - public static ChannelThread Parse(JsonElement json) + 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(); @@ -23,6 +29,7 @@ public record ChannelThread( var parentId = json.GetProperty("parent_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); var name = json.GetProperty("name").GetNonWhiteSpaceString(); + var isActive = !json .GetPropertyOrNull("thread_metadata")? .GetPropertyOrNull("archived")? @@ -38,6 +45,7 @@ public record ChannelThread( kind, guildId, parentId, + parentName, name, isActive, lastMessageId diff --git a/DiscordChatExporter.Core/Discord/Data/Common/IChannel.cs b/DiscordChatExporter.Core/Discord/Data/Common/IChannel.cs new file mode 100644 index 0000000..cba97b5 --- /dev/null +++ b/DiscordChatExporter.Core/Discord/Data/Common/IChannel.cs @@ -0,0 +1,15 @@ +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; } +} diff --git a/DiscordChatExporter.Core/Discord/DiscordClient.cs b/DiscordChatExporter.Core/Discord/DiscordClient.cs index d295691..bd0cd2c 100644 --- a/DiscordChatExporter.Core/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Core/Discord/DiscordClient.cs @@ -238,7 +238,7 @@ public class DiscordClient ? categories.GetValueOrDefault(parentId) : null; - yield return Channel.Parse(channelJson, category, position); + yield return Channel.Parse(channelJson, category, position, category?.Name, category?.Position); position++; } } @@ -270,7 +270,7 @@ public class DiscordClient foreach (var threadJson in response.Value.GetProperty("threads").EnumerateArray()) { - yield return ChannelThread.Parse(threadJson); + yield return ChannelThread.Parse(threadJson, channel.Name); currentOffset++; } @@ -286,7 +286,11 @@ public class DiscordClient { var response = await GetJsonResponseAsync($"guilds/{guildId}/threads/active", cancellationToken); foreach (var threadJson in response.GetProperty("threads").EnumerateArray()) - yield return ChannelThread.Parse(threadJson); + { + 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); + } } foreach (var channel in channels) @@ -299,7 +303,7 @@ public class DiscordClient ); foreach (var threadJson in response.GetProperty("threads").EnumerateArray()) - yield return ChannelThread.Parse(threadJson); + yield return ChannelThread.Parse(threadJson, channel.Name); } // Private archived threads @@ -310,7 +314,7 @@ public class DiscordClient ); foreach (var threadJson in response.GetProperty("threads").EnumerateArray()) - yield return ChannelThread.Parse(threadJson); + yield return ChannelThread.Parse(threadJson, channel.Name); } } } @@ -380,7 +384,7 @@ public class DiscordClient ? await GetChannelCategoryAsync(parentId.Value, cancellationToken) : null; - return Channel.Parse(response, category); + return Channel.Parse(response, category, parentName: category?.Name, parentPosition: category?.Position); } private async ValueTask TryGetLastMessageAsync( diff --git a/DiscordChatExporter.Core/Exporting/ExportRequest.cs b/DiscordChatExporter.Core/Exporting/ExportRequest.cs index a917253..982cf5d 100644 --- a/DiscordChatExporter.Core/Exporting/ExportRequest.cs +++ b/DiscordChatExporter.Core/Exporting/ExportRequest.cs @@ -4,6 +4,7 @@ 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; @@ -14,7 +15,7 @@ public partial class ExportRequest { public Guild Guild { get; } - public Channel Channel { get; } + public IChannel Channel { get; } public string OutputFilePath { get; } @@ -42,7 +43,7 @@ public partial class ExportRequest public ExportRequest( Guild guild, - Channel channel, + IChannel channel, string outputPath, string? assetsDirPath, ExportFormat format, @@ -94,7 +95,7 @@ public partial class ExportRequest { public static string GetDefaultOutputFileName( Guild guild, - Channel channel, + IChannel channel, ExportFormat format, Snowflake? after = null, Snowflake? before = null) @@ -102,7 +103,7 @@ public partial class ExportRequest var buffer = new StringBuilder(); // Guild and channel names - buffer.Append($"{guild.Name} - {channel.Category.Name} - {channel.Name} [{channel.Id}]"); + buffer.Append($"{guild.Name} - {channel.ParentName} - {channel.Name} [{channel.Id}]"); // Date range if (after is not null || before is not null) @@ -137,7 +138,7 @@ public partial class ExportRequest private static string FormatPath( string path, Guild guild, - Channel channel, + IChannel channel, Snowflake? after, Snowflake? before) { @@ -148,12 +149,12 @@ public partial class ExportRequest { "%g" => guild.Id.ToString(), "%G" => guild.Name, - "%t" => channel.Category.Id.ToString(), - "%T" => channel.Category.Name, + "%t" => channel.ParentId.ToString(), + "%T" => channel.ParentName ?? "", "%c" => channel.Id.ToString(), "%C" => channel.Name, "%p" => channel.Position?.ToString() ?? "0", - "%P" => channel.Category.Position?.ToString() ?? "0", + "%P" => channel.ParentPosition?.ToString() ?? "0", "%a" => after?.ToDate().ToString("yyyy-MM-dd") ?? "", "%b" => before?.ToDate().ToString("yyyy-MM-dd") ?? "", "%d" => DateTimeOffset.Now.ToString("yyyy-MM-dd"), @@ -165,7 +166,7 @@ public partial class ExportRequest private static string GetOutputBaseFilePath( Guild guild, - Channel channel, + IChannel channel, string outputPath, ExportFormat format, Snowflake? after = null, diff --git a/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs b/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs index 469de22..20484df 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.Category.Id.ToString()); - _writer.WriteString("category", Context.Request.Channel.Category.Name); + _writer.WriteString("categoryId", Context.Request.Channel.ParentId.ToString()); + _writer.WriteString("category", Context.Request.Channel.ParentName); _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 0bfa89e..ad4a6a0 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.Category.Name} / {Context.Request.Channel.Name}"); + await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.ParentName} / {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 a38145e..2e03b79 100644 --- a/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/PreambleTemplate.cshtml @@ -1004,7 +1004,7 @@
@Context.Request.Guild.Name
-
@Context.Request.Channel.Category.Name / @Context.Request.Channel.Name
+
@Context.Request.Channel.ParentName / @Context.Request.Channel.Name
@if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic)) {