diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs index bbd69619..54167ffe 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs @@ -246,7 +246,9 @@ public abstract class ExportCommandBase : DiscordCommandBase foreach (var (channel, error) in errorsByChannel) { - await console.Error.WriteAsync($"{channel.ParentNameWithFallback} / {channel.Name}: "); + await console.Error.WriteAsync( + $"{channel.ParentNameWithFallback} / {channel.Name}: " + ); using (console.WithForegroundColor(ConsoleColor.Red)) await console.Error.WriteLineAsync(error); diff --git a/DiscordChatExporter.Cli/Commands/Converters/ThreadInclusionBindingConverter.cs b/DiscordChatExporter.Cli/Commands/Converters/ThreadInclusionBindingConverter.cs new file mode 100644 index 00000000..0466e655 --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/Converters/ThreadInclusionBindingConverter.cs @@ -0,0 +1,22 @@ +using System; +using CliFx.Extensibility; +using DiscordChatExporter.Cli.Commands.Shared; + +namespace DiscordChatExporter.Cli.Commands.Converters; + +internal class ThreadInclusionBindingConverter : BindingConverter +{ + public override ThreadInclusion Convert(string? rawValue) + { + // Empty or unset value is treated as 'active' to match the previous behavior + if (string.IsNullOrWhiteSpace(rawValue)) + return ThreadInclusion.Active; + + // Boolean 'true' is treated as 'active', boolean 'false' is treated as 'none' + if (bool.TryParse(rawValue, out var boolValue)) + return boolValue ? ThreadInclusion.Active : ThreadInclusion.None; + + // Otherwise, fall back to regular enum parsing + return Enum.Parse(rawValue, true); + } +} diff --git a/DiscordChatExporter.Cli/Commands/Converters/TruthyBooleanBindingConverter.cs b/DiscordChatExporter.Cli/Commands/Converters/TruthyBooleanBindingConverter.cs index 48bc125c..5f4abd20 100644 --- a/DiscordChatExporter.Cli/Commands/Converters/TruthyBooleanBindingConverter.cs +++ b/DiscordChatExporter.Cli/Commands/Converters/TruthyBooleanBindingConverter.cs @@ -7,19 +7,15 @@ internal class TruthyBooleanBindingConverter : BindingConverter { public override bool Convert(string? rawValue) { - // Null is still considered true, to match the base behavior - if (rawValue is null) + // Empty or unset value is treated as 'true', to match the regular boolean behavior + if (string.IsNullOrWhiteSpace(rawValue)) return true; - if (string.IsNullOrWhiteSpace(rawValue)) - return false; + // Number '1' is treated as 'true', other numbers are treated as 'false' + if (int.TryParse(rawValue, CultureInfo.InvariantCulture, out var intValue)) + return intValue == 1; - if (bool.TryParse(rawValue, out var boolValue)) - return boolValue; - - if (int.TryParse(rawValue, CultureInfo.InvariantCulture, out var intValue) && intValue == 0) - return false; - - return true; + // Otherwise, fall back to regular boolean parsing + return bool.Parse(rawValue); } } diff --git a/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs b/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs index 349b971e..c8549358 100644 --- a/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs @@ -6,6 +6,8 @@ using CliFx.Attributes; using CliFx.Exceptions; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Cli.Commands.Converters; +using DiscordChatExporter.Cli.Commands.Shared; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Exceptions; @@ -25,11 +27,16 @@ public class ExportAllCommand : ExportCommandBase [CommandOption("include-vc", Description = "Include voice channels.")] public bool IncludeVoiceChannels { get; init; } = true; - [CommandOption("include-threads", Description = "Include threads.")] - public bool IncludeThreads { get; init; } = false; + [CommandOption( + "include-threads", + Description = "Specifies which types of threads should be included.", + Converter = typeof(ThreadInclusionBindingConverter) + )] + public ThreadInclusion ThreadInclusion { get; init; } = ThreadInclusion.None; - [CommandOption("include-archived-threads", Description = "Include archived threads.")] - public bool IncludeArchivedThreads { get; init; } = false; + private bool IncludeThreads => ThreadInclusion != ThreadInclusion.None; + + private bool IncludeArchivedThreads => ThreadInclusion.HasFlag(ThreadInclusion.Archived); [CommandOption( "data-package", @@ -42,14 +49,6 @@ public class ExportAllCommand : ExportCommandBase { await base.ExecuteAsync(console); - // Cannot include archived threads without including active threads as well - if (IncludeArchivedThreads && !IncludeThreads) - { - throw new CommandException( - "Option --include-archived-threads can only be used when --include-threads is also specified." - ); - } - var cancellationToken = console.RegisterCancellationHandler(); var channels = new List(); diff --git a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs index 6e86a744..c338ebc0 100644 --- a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using CliFx.Attributes; -using CliFx.Exceptions; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Cli.Commands.Converters; +using DiscordChatExporter.Cli.Commands.Shared; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; @@ -18,24 +19,21 @@ public class ExportGuildCommand : ExportCommandBase [CommandOption("include-vc", Description = "Include voice channels.")] public bool IncludeVoiceChannels { get; init; } = true; - [CommandOption("include-threads", Description = "Include threads.")] - public bool IncludeThreads { get; init; } = false; + [CommandOption( + "include-threads", + Description = "Specifies which types of threads should be included.", + Converter = typeof(ThreadInclusionBindingConverter) + )] + public ThreadInclusion ThreadInclusion { get; init; } = ThreadInclusion.None; - [CommandOption("include-archived-threads", Description = "Include archived threads.")] - public bool IncludeArchivedThreads { get; init; } = false; + private bool IncludeThreads => ThreadInclusion != ThreadInclusion.None; + + private bool IncludeArchivedThreads => ThreadInclusion.HasFlag(ThreadInclusion.Archived); public override async ValueTask ExecuteAsync(IConsole console) { await base.ExecuteAsync(console); - // Cannot include archived threads without including active threads as well - if (IncludeArchivedThreads && !IncludeThreads) - { - throw new CommandException( - "Option --include-archived-threads can only be used when --include-threads is also specified." - ); - } - var cancellationToken = console.RegisterCancellationHandler(); var channels = new List(); diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs index 917efdb9..aca0ed5d 100644 --- a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -2,9 +2,10 @@ using System.Linq; using System.Threading.Tasks; using CliFx.Attributes; -using CliFx.Exceptions; using CliFx.Infrastructure; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Cli.Commands.Converters; +using DiscordChatExporter.Cli.Commands.Shared; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Utils.Extensions; @@ -20,24 +21,21 @@ public class GetChannelsCommand : DiscordCommandBase [CommandOption("include-vc", Description = "Include voice channels.")] public bool IncludeVoiceChannels { get; init; } = true; - [CommandOption("include-threads", Description = "Include threads.")] - public bool IncludeThreads { get; init; } = false; + [CommandOption( + "include-threads", + Description = "Specifies which types of threads should be included.", + Converter = typeof(ThreadInclusionBindingConverter) + )] + public ThreadInclusion ThreadInclusion { get; init; } = ThreadInclusion.None; - [CommandOption("include-archived-threads", Description = "Include archived threads.")] - public bool IncludeArchivedThreads { get; init; } = false; + private bool IncludeThreads => ThreadInclusion != ThreadInclusion.None; + + private bool IncludeArchivedThreads => ThreadInclusion.HasFlag(ThreadInclusion.Archived); public override async ValueTask ExecuteAsync(IConsole console) { await base.ExecuteAsync(console); - // Cannot include archived threads without including active threads as well - if (IncludeArchivedThreads && !IncludeThreads) - { - throw new CommandException( - "Option --include-archived-threads can only be used when --include-threads is also specified." - ); - } - var cancellationToken = console.RegisterCancellationHandler(); var channels = (await Discord.GetGuildChannelsAsync(GuildId, cancellationToken)) @@ -77,7 +75,9 @@ public class GetChannelsCommand : DiscordCommandBase // Channel category / name using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteLineAsync($"{channel.ParentNameWithFallback} / {channel.Name}"); + await console.Output.WriteLineAsync( + $"{channel.ParentNameWithFallback} / {channel.Name}" + ); var channelThreads = threads.Where(t => t.Parent?.Id == channel.Id).ToArray(); var channelThreadIdMaxLength = channelThreads diff --git a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs index 8a118b77..4ceb1bdd 100644 --- a/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetDirectChannelsCommand.cs @@ -44,7 +44,9 @@ public class GetDirectChannelsCommand : DiscordCommandBase // Channel category / name using (console.WithForegroundColor(ConsoleColor.White)) - await console.Output.WriteLineAsync($"{channel.ParentNameWithFallback} / {channel.Name}"); + await console.Output.WriteLineAsync( + $"{channel.ParentNameWithFallback} / {channel.Name}" + ); } } } diff --git a/DiscordChatExporter.Cli/Commands/Shared/ThreadInclusion.cs b/DiscordChatExporter.Cli/Commands/Shared/ThreadInclusion.cs new file mode 100644 index 00000000..dfdf3bbb --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/Shared/ThreadInclusion.cs @@ -0,0 +1,12 @@ +using System; + +namespace DiscordChatExporter.Cli.Commands.Shared; + +[Flags] +public enum ThreadInclusion +{ + None = 0, + Active = 1, + Archived = 2, + All = Active | Archived +}