From 58ba99e0c6b90db826d25dae6557f8c7c4d64fce Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Tue, 20 Aug 2019 18:33:27 +0300 Subject: [PATCH] Migrate DiscordChatExporter.Cli to CliFx --- .../Commands/ExportChannelCommand.cs | 28 ++++++ .../Commands/ExportCommandBase.cs | 78 +++++++++++++++++ .../Commands/ExportDirectMessagesCommand.cs | 45 ++++++++++ .../Commands/ExportGuildCommand.cs | 49 +++++++++++ .../Commands/GetChannelsCommand.cs | 34 ++++++++ .../GetDirectMessageChannelsCommand.cs | 30 +++++++ .../Commands/GetGuildsCommand.cs | 30 +++++++ .../Commands/GuideCommand.cs | 53 +++++++++++ .../Commands/TokenCommandBase.cs | 29 +++++++ DiscordChatExporter.Cli/Container.cs | 24 ----- .../DiscordChatExporter.Cli.csproj | 2 +- .../Internal/InlineProgress.cs | 37 ++++---- DiscordChatExporter.Cli/Program.cs | 87 +++++-------------- .../Verbs/ExportChannelVerb.cs | 58 ------------- .../Verbs/ExportDirectMessagesVerb.cs | 78 ----------------- .../Verbs/ExportGuildVerb.cs | 79 ----------------- .../Verbs/GetChannelsVerb.cs | 33 ------- .../Verbs/GetDirectMessageChannelsVerb.cs | 32 ------- .../Verbs/GetGuildsVerb.cs | 32 ------- .../Verbs/Options/ExportChannelOptions.cs | 11 --- .../Options/ExportDirectMessagesOptions.cs | 9 -- .../Verbs/Options/ExportGuildOptions.cs | 11 --- .../Verbs/Options/ExportOptions.cs | 29 ------- .../Verbs/Options/GetChannelsOptions.cs | 11 --- .../GetDirectMessageChannelsOptions.cs | 9 -- .../Verbs/Options/GetGuildsOptions.cs | 9 -- .../Verbs/Options/TokenOptions.cs | 16 ---- DiscordChatExporter.Cli/Verbs/Verb.cs | 18 ---- Readme.md | 2 +- 29 files changed, 418 insertions(+), 545 deletions(-) create mode 100644 DiscordChatExporter.Cli/Commands/ExportChannelCommand.cs create mode 100644 DiscordChatExporter.Cli/Commands/ExportCommandBase.cs create mode 100644 DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs create mode 100644 DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs create mode 100644 DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs create mode 100644 DiscordChatExporter.Cli/Commands/GetDirectMessageChannelsCommand.cs create mode 100644 DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs create mode 100644 DiscordChatExporter.Cli/Commands/GuideCommand.cs create mode 100644 DiscordChatExporter.Cli/Commands/TokenCommandBase.cs delete mode 100644 DiscordChatExporter.Cli/Container.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/ExportChannelVerb.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/ExportDirectMessagesVerb.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/ExportGuildVerb.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/GetChannelsVerb.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/GetDirectMessageChannelsVerb.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/GetGuildsVerb.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/Options/ExportChannelOptions.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/Options/ExportDirectMessagesOptions.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/Options/ExportGuildOptions.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/Options/ExportOptions.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/Options/GetChannelsOptions.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/Options/GetDirectMessageChannelsOptions.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/Options/GetGuildsOptions.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/Options/TokenOptions.cs delete mode 100644 DiscordChatExporter.Cli/Verbs/Verb.cs diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelCommand.cs new file mode 100644 index 0000000..a972cf7 --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/ExportChannelCommand.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Services; +using DiscordChatExporter.Core.Services; + +namespace DiscordChatExporter.Cli.Commands +{ + [Command("export", Description = "Export a channel.")] + public class ExportChannelCommand : ExportCommandBase + { + [CommandOption("channel", 'c', IsRequired = true, Description= "Channel ID.")] + public string ChannelId { get; set; } + + public ExportChannelCommand(SettingsService settingsService, DataService dataService, ExportService exportService) + : base(settingsService, dataService, exportService) + { + } + + public override async Task ExecuteAsync(IConsole console) + { + // Get channel + var channel = await DataService.GetChannelAsync(GetToken(), ChannelId); + + // Export + await ExportChannelAsync(console, channel); + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/ExportCommandBase.cs new file mode 100644 index 0000000..b1675d2 --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/ExportCommandBase.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Services; +using DiscordChatExporter.Cli.Internal; +using DiscordChatExporter.Core.Models; +using DiscordChatExporter.Core.Services; +using DiscordChatExporter.Core.Services.Helpers; +using Tyrrrz.Extensions; + +namespace DiscordChatExporter.Cli.Commands +{ + public abstract class ExportCommandBase : TokenCommandBase + { + protected SettingsService SettingsService { get; } + + protected ExportService ExportService { get; } + + + [CommandOption("format", 'f', Description = "Output file format.")] + public ExportFormat ExportFormat { get; set; } = ExportFormat.HtmlDark; + + [CommandOption("output", 'o', Description = "Output file or directory path.")] + public string OutputPath { get; set; } + + [CommandOption("after",Description = "Limit to messages sent after this date.")] + public DateTimeOffset? After { get; set; } + + [CommandOption("before",Description = "Limit to messages sent before this date.")] + public DateTimeOffset? Before { get; set; } + + [CommandOption("partition", 'p', Description = "Split output into partitions limited to this number of messages.")] + public int? PartitionLimit { get; set; } + + [CommandOption("dateformat", Description = "Date format used in output.")] + public string DateFormat { get; set; } + + protected ExportCommandBase(SettingsService settingsService, DataService dataService, ExportService exportService) + : base(dataService) + { + SettingsService = settingsService; + ExportService = exportService; + } + + protected async Task ExportChannelAsync(IConsole console, Channel channel) + { + // Configure settings + if (!DateFormat.IsNullOrWhiteSpace()) + SettingsService.DateFormat = DateFormat; + + console.Output.Write($"Exporting channel [{channel.Name}]... "); + using (var progress = new InlineProgress(console)) + { + // Get chat log + var chatLog = await DataService.GetChatLogAsync(GetToken(), channel, After, Before, progress); + + // Generate file path if not set or is a directory + var filePath = OutputPath; + if (filePath.IsNullOrWhiteSpace() || ExportHelper.IsDirectoryPath(filePath)) + { + // Generate default file name + var fileName = ExportHelper.GetDefaultExportFileName(ExportFormat, chatLog.Guild, + chatLog.Channel, After, Before); + + // Combine paths + filePath = Path.Combine(filePath ?? "", fileName); + } + + // Export + await ExportService.ExportChatLogAsync(chatLog, filePath, ExportFormat, PartitionLimit); + + // Report successful completion + progress.ReportCompletion(); + } + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs b/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs new file mode 100644 index 0000000..a3a2c4a --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Services; +using DiscordChatExporter.Core.Services; +using DiscordChatExporter.Core.Services.Exceptions; + +namespace DiscordChatExporter.Cli.Commands +{ + [Command("exportdm", Description = "Export all direct message channels.")] + public class ExportDirectMessagesCommand : ExportCommandBase + { + public ExportDirectMessagesCommand(SettingsService settingsService, DataService dataService, ExportService exportService) + : base(settingsService, dataService, exportService) + { + } + + public override async Task ExecuteAsync(IConsole console) + { + // Get channels + var channels = await DataService.GetDirectMessageChannelsAsync(GetToken()); + + // Order channels + channels = channels.OrderBy(c => c.Name).ToArray(); + + // Loop through channels + foreach (var channel in channels) + { + try + { + await ExportChannelAsync(console, channel); + } + catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) + { + console.Error.WriteLine("You don't have access to this channel."); + } + catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + console.Error.WriteLine("This channel doesn't exist."); + } + } + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs new file mode 100644 index 0000000..67a33c3 --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs @@ -0,0 +1,49 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Services; +using DiscordChatExporter.Core.Models; +using DiscordChatExporter.Core.Services; +using DiscordChatExporter.Core.Services.Exceptions; + +namespace DiscordChatExporter.Cli.Commands +{ + [Command("exportguild", Description = "Export all channels within specified guild.")] + public class ExportGuildCommand : ExportCommandBase + { + [CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")] + public string GuildId { get; set; } + + public ExportGuildCommand(SettingsService settingsService, DataService dataService, ExportService exportService) + : base(settingsService, dataService, exportService) + { + } + + public override async Task ExecuteAsync(IConsole console) + { + // Get channels + var channels = await DataService.GetGuildChannelsAsync(GetToken(), GuildId); + + // Filter and order channels + channels = channels.Where(c => c.Type == ChannelType.GuildTextChat).OrderBy(c => c.Name).ToArray(); + + // Loop through channels + foreach (var channel in channels) + { + try + { + await ExportChannelAsync(console, channel); + } + catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) + { + console.Error.WriteLine("You don't have access to this channel."); + } + catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + console.Error.WriteLine("This channel doesn't exist."); + } + } + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs new file mode 100644 index 0000000..d3d8826 --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -0,0 +1,34 @@ +using System.Linq; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Services; +using DiscordChatExporter.Core.Models; +using DiscordChatExporter.Core.Services; + +namespace DiscordChatExporter.Cli.Commands +{ + [Command("channels", Description = "Get the list of channels in the given guild.")] + public class GetChannelsCommand : TokenCommandBase + { + [CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")] + public string GuildId { get; set; } + + public GetChannelsCommand(DataService dataService) + : base(dataService) + { + } + + public override async Task ExecuteAsync(IConsole console) + { + // Get channels + var channels = await DataService.GetGuildChannelsAsync(GetToken(), GuildId); + + // Filter and order channels + channels = channels.Where(c => c.Type == ChannelType.GuildTextChat).OrderBy(c => c.Name).ToArray(); + + // Print result + foreach (var channel in channels) + console.Output.WriteLine($"{channel.Id} | {channel.Name}"); + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/GetDirectMessageChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetDirectMessageChannelsCommand.cs new file mode 100644 index 0000000..eeb0a43 --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/GetDirectMessageChannelsCommand.cs @@ -0,0 +1,30 @@ +using System.Linq; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Services; +using DiscordChatExporter.Core.Services; + +namespace DiscordChatExporter.Cli.Commands +{ + [Command("dm", Description = "Get the list of direct message channels.")] + public class GetDirectMessageChannelsCommand : TokenCommandBase + { + public GetDirectMessageChannelsCommand(DataService dataService) + : base(dataService) + { + } + + public override async Task ExecuteAsync(IConsole console) + { + // Get channels + var channels = await DataService.GetDirectMessageChannelsAsync(GetToken()); + + // Order channels + channels = channels.OrderBy(c => c.Name).ToArray(); + + // Print result + foreach (var channel in channels) + console.Output.WriteLine($"{channel.Id} | {channel.Name}"); + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs b/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs new file mode 100644 index 0000000..b8c6a92 --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/GetGuildsCommand.cs @@ -0,0 +1,30 @@ +using System.Linq; +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Services; +using DiscordChatExporter.Core.Services; + +namespace DiscordChatExporter.Cli.Commands +{ + [Command("guilds", Description = "Get the list of accessible guilds.")] + public class GetGuildsCommand : TokenCommandBase + { + public GetGuildsCommand(DataService dataService) + : base(dataService) + { + } + + public override async Task ExecuteAsync(IConsole console) + { + // Get guilds + var guilds = await DataService.GetUserGuildsAsync(GetToken()); + + // Order guilds + guilds = guilds.OrderBy(g => g.Name).ToArray(); + + // Print result + foreach (var guild in guilds) + console.Output.WriteLine($"{guild.Id} | {guild.Name}"); + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/GuideCommand.cs b/DiscordChatExporter.Cli/Commands/GuideCommand.cs new file mode 100644 index 0000000..17e9b6a --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/GuideCommand.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Services; + +namespace DiscordChatExporter.Cli.Commands +{ + [Command("guide", Description = "Explains how to obtain token, guild or channel ID.")] + public class GuideCommand : ICommand + { + public Task ExecuteAsync(IConsole console) + { + console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine("To get user token:")); + console.Output.WriteLine(" 1. Open Discord"); + console.Output.WriteLine(" 2. Press Ctrl+Shift+I to show developer tools"); + console.Output.WriteLine(" 3. Navigate to the Application tab"); + console.Output.WriteLine(" 4. Select \"Local Storage\" > \"https://discordapp.com\" on the left"); + console.Output.WriteLine(" 5. Press Ctrl+R to reload"); + console.Output.WriteLine(" 6. Find \"token\" at the bottom and copy the value"); + console.Output.WriteLine(); + + console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine("To get bot token:")); + console.Output.WriteLine(" 1. Go to Discord developer portal"); + console.Output.WriteLine(" 2. Open your application's settings"); + console.Output.WriteLine(" 3. Navigate to the Bot section on the left"); + console.Output.WriteLine(" 4. Under Token click Copy"); + console.Output.WriteLine(); + + console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine("To get guild ID or guild channel ID:")); + console.Output.WriteLine(" 1. Open Discord"); + console.Output.WriteLine(" 2. Open Settings"); + console.Output.WriteLine(" 3. Go to Appearance section"); + console.Output.WriteLine(" 4. Enable Developer Mode"); + console.Output.WriteLine(" 5. Right click on the desired guild or channel and click Copy ID"); + console.Output.WriteLine(); + + console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine("To get direct message channel ID:")); + console.Output.WriteLine(" 1. Open Discord"); + console.Output.WriteLine(" 2. Open the desired direct message channel"); + console.Output.WriteLine(" 3. Press Ctrl+Shift+I to show developer tools"); + console.Output.WriteLine(" 4. Navigate to the Console tab"); + console.Output.WriteLine(" 5. Type \"window.location.href\" and press Enter"); + console.Output.WriteLine(" 6. Copy the first long sequence of numbers inside the URL"); + console.Output.WriteLine(); + + console.Output.WriteLine("If you still have unanswered questions, check out the wiki:"); + console.Output.WriteLine("https://github.com/Tyrrrz/DiscordChatExporter/wiki"); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/TokenCommandBase.cs b/DiscordChatExporter.Cli/Commands/TokenCommandBase.cs new file mode 100644 index 0000000..d746736 --- /dev/null +++ b/DiscordChatExporter.Cli/Commands/TokenCommandBase.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Services; +using DiscordChatExporter.Core.Models; +using DiscordChatExporter.Core.Services; + +namespace DiscordChatExporter.Cli.Commands +{ + public abstract class TokenCommandBase : ICommand + { + protected DataService DataService { get; } + + [CommandOption("token", 't', IsRequired = true, Description = "Authorization token.")] + public string TokenValue { get; set; } + + [CommandOption("bot", 'b', Description = "Whether this authorization token belongs to a bot.")] + public bool IsBotToken { get; set; } + + protected TokenCommandBase(DataService dataService) + { + DataService = dataService; + } + + protected AuthToken GetToken() => new AuthToken(IsBotToken ? AuthTokenType.Bot : AuthTokenType.User, TokenValue); + + public abstract Task ExecuteAsync(IConsole console); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Container.cs b/DiscordChatExporter.Cli/Container.cs deleted file mode 100644 index d7e44f1..0000000 --- a/DiscordChatExporter.Cli/Container.cs +++ /dev/null @@ -1,24 +0,0 @@ -using DiscordChatExporter.Core.Services; -using StyletIoC; - -namespace DiscordChatExporter.Cli -{ - public static class Container - { - public static IContainer Instance { get; } - - static Container() - { - var builder = new StyletIoCBuilder(); - - // Autobind the .Services assembly - builder.Autobind(typeof(DataService).Assembly); - - // Bind settings as singleton - builder.Bind().ToSelf().InSingletonScope(); - - // Set instance - Instance = builder.BuildContainer(); - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj index 835e903..86fb3e7 100644 --- a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj +++ b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj @@ -12,7 +12,7 @@ - + diff --git a/DiscordChatExporter.Cli/Internal/InlineProgress.cs b/DiscordChatExporter.Cli/Internal/InlineProgress.cs index 4d629d6..fd7c8d7 100644 --- a/DiscordChatExporter.Cli/Internal/InlineProgress.cs +++ b/DiscordChatExporter.Cli/Internal/InlineProgress.cs @@ -1,31 +1,33 @@ using System; +using CliFx.Services; namespace DiscordChatExporter.Cli.Internal { internal class InlineProgress : IProgress, IDisposable { - private readonly int _posX; - private readonly int _posY; + private readonly IConsole _console; + private string _lastOutput = ""; private bool _isCompleted; - public InlineProgress() + public InlineProgress(IConsole console) { - // If output is not redirected - save initial cursor position - if (!Console.IsOutputRedirected) - { - _posX = Console.CursorLeft; - _posY = Console.CursorTop; - } + _console = console; + } + + private void ResetCursorPosition() + { + foreach (var c in _lastOutput) + _console.Output.Write('\b'); } public void Report(double progress) { // If output is not redirected - reset cursor position and write progress - if (!Console.IsOutputRedirected) + if (!_console.IsOutputRedirected) { - Console.SetCursorPosition(_posX, _posY); - Console.WriteLine($"{progress:P1}"); + ResetCursorPosition(); + _console.Output.Write(_lastOutput = $"{progress:P1}"); } } @@ -34,14 +36,13 @@ namespace DiscordChatExporter.Cli.Internal public void Dispose() { // If output is not redirected - reset cursor position - if (!Console.IsOutputRedirected) - Console.SetCursorPosition(_posX, _posY); + if (!_console.IsOutputRedirected) + { + ResetCursorPosition(); + } // Inform about completion - if (_isCompleted) - Console.WriteLine("Completed ✓"); - else - Console.WriteLine("Failed X"); + _console.Output.WriteLine(_isCompleted ? "Completed ✓" : "Failed X"); } } } \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Program.cs b/DiscordChatExporter.Cli/Program.cs index 60a0f9d..a671c3d 100644 --- a/DiscordChatExporter.Cli/Program.cs +++ b/DiscordChatExporter.Cli/Program.cs @@ -1,80 +1,35 @@ -using System; -using System.Linq; -using CommandLine; -using DiscordChatExporter.Cli.Verbs; -using DiscordChatExporter.Cli.Verbs.Options; +using System.Threading.Tasks; +using CliFx; +using DiscordChatExporter.Core.Services; +using StyletIoC; namespace DiscordChatExporter.Cli { public static class Program { - private static void PrintTokenHelp() + private static IContainer BuildContainer() { - Console.WriteLine("# To get user token:"); - Console.WriteLine(" 1. Open Discord"); - Console.WriteLine(" 2. Press Ctrl+Shift+I to show developer tools"); - Console.WriteLine(" 3. Navigate to the Application tab"); - Console.WriteLine(" 4. Select \"Local Storage\" > \"https://discordapp.com\" on the left"); - Console.WriteLine(" 5. Press Ctrl+R to reload"); - Console.WriteLine(" 6. Find \"token\" at the bottom and copy the value"); - Console.WriteLine(); - Console.WriteLine("# To get bot token:"); - Console.WriteLine(" 1. Go to Discord developer portal"); - Console.WriteLine(" 2. Open your application's settings"); - Console.WriteLine(" 3. Navigate to the Bot section on the left"); - Console.WriteLine(" 4. Under Token click Copy"); - Console.WriteLine(); - Console.WriteLine("# To get guild ID or guild channel ID:"); - Console.WriteLine(" 1. Open Discord"); - Console.WriteLine(" 2. Open Settings"); - Console.WriteLine(" 3. Go to Appearance section"); - Console.WriteLine(" 4. Enable Developer Mode"); - Console.WriteLine(" 5. Right click on the desired guild or channel and click Copy ID"); - Console.WriteLine(); - Console.WriteLine("# To get direct message channel ID:"); - Console.WriteLine(" 1. Open Discord"); - Console.WriteLine(" 2. Open the desired direct message channel"); - Console.WriteLine(" 3. Press Ctrl+Shift+I to show developer tools"); - Console.WriteLine(" 4. Navigate to the Console tab"); - Console.WriteLine(" 5. Type \"window.location.href\" and press Enter"); - Console.WriteLine(" 6. Copy the first long sequence of numbers inside the URL"); - } - - public static void Main(string[] args) - { - // Get all verb types - var verbTypes = new[] - { - typeof(ExportChannelOptions), - typeof(ExportDirectMessagesOptions), - typeof(ExportGuildOptions), - typeof(GetChannelsOptions), - typeof(GetDirectMessageChannelsOptions), - typeof(GetGuildsOptions) - }; + var builder = new StyletIoCBuilder(); - // Parse command line arguments - var parsedArgs = Parser.Default.ParseArguments(args, verbTypes); + // Autobind the .Services assembly + builder.Autobind(typeof(DataService).Assembly); - // Execute commands - parsedArgs.WithParsed(o => new ExportChannelVerb(o).Execute()); - parsedArgs.WithParsed(o => new ExportDirectMessagesVerb(o).Execute()); - parsedArgs.WithParsed(o => new ExportGuildVerb(o).Execute()); - parsedArgs.WithParsed(o => new GetChannelsVerb(o).Execute()); - parsedArgs.WithParsed(o => new GetDirectMessageChannelsVerb(o).Execute()); - parsedArgs.WithParsed(o => new GetGuildsVerb(o).Execute()); + // Bind settings as singleton + builder.Bind().ToSelf().InSingletonScope(); - // Show token help if help requested or no verb specified - parsedArgs.WithNotParsed(errs => - { - var err = errs.First(); + // Set instance + return builder.BuildContainer(); + } - if (err.Tag == ErrorType.NoVerbSelectedError) - PrintTokenHelp(); + public static Task Main(string[] args) + { + var container = BuildContainer(); - if (err.Tag == ErrorType.HelpVerbRequestedError && args.Length == 1) - PrintTokenHelp(); - }); + return new CliApplicationBuilder() + .AddCommandsFromThisAssembly() + .UseCommandFactory(schema => (ICommand) container.Get(schema.Type)) + .Build() + .RunAsync(args); } } } \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/ExportChannelVerb.cs b/DiscordChatExporter.Cli/Verbs/ExportChannelVerb.cs deleted file mode 100644 index 1dc7c57..0000000 --- a/DiscordChatExporter.Cli/Verbs/ExportChannelVerb.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using DiscordChatExporter.Cli.Internal; -using DiscordChatExporter.Cli.Verbs.Options; -using DiscordChatExporter.Core.Services; -using DiscordChatExporter.Core.Services.Helpers; -using Tyrrrz.Extensions; - -namespace DiscordChatExporter.Cli.Verbs -{ - public class ExportChannelVerb : Verb - { - public ExportChannelVerb(ExportChannelOptions options) - : base(options) - { - } - - public override async Task ExecuteAsync() - { - // Get services - var settingsService = Container.Instance.Get(); - var dataService = Container.Instance.Get(); - var exportService = Container.Instance.Get(); - - // Configure settings - if (!Options.DateFormat.IsNullOrWhiteSpace()) - settingsService.DateFormat = Options.DateFormat; - - // Track progress - Console.Write($"Exporting channel [{Options.ChannelId}]... "); - using (var progress = new InlineProgress()) - { - // Get chat log - var chatLog = await dataService.GetChatLogAsync(Options.GetToken(), Options.ChannelId, - Options.After, Options.Before, progress); - - // Generate file path if not set or is a directory - var filePath = Options.OutputPath; - if (filePath.IsNullOrWhiteSpace() || ExportHelper.IsDirectoryPath(filePath)) - { - // Generate default file name - var fileName = ExportHelper.GetDefaultExportFileName(Options.ExportFormat, chatLog.Guild, - chatLog.Channel, Options.After, Options.Before); - - // Combine paths - filePath = Path.Combine(filePath ?? "", fileName); - } - - // Export - await exportService.ExportChatLogAsync(chatLog, filePath, Options.ExportFormat, Options.PartitionLimit); - - // Report successful completion - progress.ReportCompletion(); - } - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/ExportDirectMessagesVerb.cs b/DiscordChatExporter.Cli/Verbs/ExportDirectMessagesVerb.cs deleted file mode 100644 index 1dd844f..0000000 --- a/DiscordChatExporter.Cli/Verbs/ExportDirectMessagesVerb.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using DiscordChatExporter.Cli.Internal; -using DiscordChatExporter.Cli.Verbs.Options; -using DiscordChatExporter.Core.Services; -using DiscordChatExporter.Core.Services.Exceptions; -using DiscordChatExporter.Core.Services.Helpers; -using Tyrrrz.Extensions; - -namespace DiscordChatExporter.Cli.Verbs -{ - public class ExportDirectMessagesVerb : Verb - { - public ExportDirectMessagesVerb(ExportDirectMessagesOptions options) - : base(options) - { - } - - public override async Task ExecuteAsync() - { - // Get services - var settingsService = Container.Instance.Get(); - var dataService = Container.Instance.Get(); - var exportService = Container.Instance.Get(); - - // Configure settings - if (!Options.DateFormat.IsNullOrWhiteSpace()) - settingsService.DateFormat = Options.DateFormat; - - // Get channels - var channels = await dataService.GetDirectMessageChannelsAsync(Options.GetToken()); - - // Order channels - channels = channels.OrderBy(c => c.Name).ToArray(); - - // Loop through channels - foreach (var channel in channels) - { - try - { - // Track progress - Console.Write($"Exporting channel [{channel.Name}]... "); - using (var progress = new InlineProgress()) - { - // Get chat log - var chatLog = await dataService.GetChatLogAsync(Options.GetToken(), channel, - Options.After, Options.Before, progress); - - // Generate default file name - var fileName = ExportHelper.GetDefaultExportFileName(Options.ExportFormat, chatLog.Guild, - chatLog.Channel, Options.After, Options.Before); - - // Generate file path - var filePath = Path.Combine(Options.OutputPath ?? "", fileName); - - // Export - await exportService.ExportChatLogAsync(chatLog, filePath, Options.ExportFormat, - Options.PartitionLimit); - - // Report successful completion - progress.ReportCompletion(); - } - } - catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) - { - Console.Error.WriteLine("You don't have access to this channel"); - } - catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.NotFound) - { - Console.Error.WriteLine("This channel doesn't exist"); - } - } - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/ExportGuildVerb.cs b/DiscordChatExporter.Cli/Verbs/ExportGuildVerb.cs deleted file mode 100644 index f271deb..0000000 --- a/DiscordChatExporter.Cli/Verbs/ExportGuildVerb.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using DiscordChatExporter.Cli.Internal; -using DiscordChatExporter.Cli.Verbs.Options; -using DiscordChatExporter.Core.Models; -using DiscordChatExporter.Core.Services; -using DiscordChatExporter.Core.Services.Exceptions; -using DiscordChatExporter.Core.Services.Helpers; -using Tyrrrz.Extensions; - -namespace DiscordChatExporter.Cli.Verbs -{ - public class ExportGuildVerb : Verb - { - public ExportGuildVerb(ExportGuildOptions options) - : base(options) - { - } - - public override async Task ExecuteAsync() - { - // Get services - var settingsService = Container.Instance.Get(); - var dataService = Container.Instance.Get(); - var exportService = Container.Instance.Get(); - - // Configure settings - if (!Options.DateFormat.IsNullOrWhiteSpace()) - settingsService.DateFormat = Options.DateFormat; - - // Get channels - var channels = await dataService.GetGuildChannelsAsync(Options.GetToken(), Options.GuildId); - - // Filter and order channels - channels = channels.Where(c => c.Type == ChannelType.GuildTextChat).OrderBy(c => c.Name).ToArray(); - - // Loop through channels - foreach (var channel in channels) - { - try - { - // Track progress - Console.Write($"Exporting channel [{channel.Name}]... "); - using (var progress = new InlineProgress()) - { - // Get chat log - var chatLog = await dataService.GetChatLogAsync(Options.GetToken(), channel, - Options.After, Options.Before, progress); - - // Generate default file name - var fileName = ExportHelper.GetDefaultExportFileName(Options.ExportFormat, chatLog.Guild, - chatLog.Channel, Options.After, Options.Before); - - // Generate file path - var filePath = Path.Combine(Options.OutputPath ?? "", fileName); - - // Export - await exportService.ExportChatLogAsync(chatLog, filePath, Options.ExportFormat, - Options.PartitionLimit); - - // Report successful completion - progress.ReportCompletion(); - } - } - catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) - { - Console.Error.WriteLine("You don't have access to this channel"); - } - catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.NotFound) - { - Console.Error.WriteLine("This channel doesn't exist"); - } - } - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/GetChannelsVerb.cs b/DiscordChatExporter.Cli/Verbs/GetChannelsVerb.cs deleted file mode 100644 index fff1561..0000000 --- a/DiscordChatExporter.Cli/Verbs/GetChannelsVerb.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DiscordChatExporter.Cli.Verbs.Options; -using DiscordChatExporter.Core.Models; -using DiscordChatExporter.Core.Services; - -namespace DiscordChatExporter.Cli.Verbs -{ - public class GetChannelsVerb : Verb - { - public GetChannelsVerb(GetChannelsOptions options) - : base(options) - { - } - - public override async Task ExecuteAsync() - { - // Get data service - var dataService = Container.Instance.Get(); - - // Get channels - var channels = await dataService.GetGuildChannelsAsync(Options.GetToken(), Options.GuildId); - - // Filter and order channels - channels = channels.Where(c => c.Type == ChannelType.GuildTextChat).OrderBy(c => c.Name).ToArray(); - - // Print result - foreach (var channel in channels) - Console.WriteLine($"{channel.Id} | {channel.Name}"); - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/GetDirectMessageChannelsVerb.cs b/DiscordChatExporter.Cli/Verbs/GetDirectMessageChannelsVerb.cs deleted file mode 100644 index c7174ac..0000000 --- a/DiscordChatExporter.Cli/Verbs/GetDirectMessageChannelsVerb.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DiscordChatExporter.Cli.Verbs.Options; -using DiscordChatExporter.Core.Services; - -namespace DiscordChatExporter.Cli.Verbs -{ - public class GetDirectMessageChannelsVerb : Verb - { - public GetDirectMessageChannelsVerb(GetDirectMessageChannelsOptions options) - : base(options) - { - } - - public override async Task ExecuteAsync() - { - // Get data service - var dataService = Container.Instance.Get(); - - // Get channels - var channels = await dataService.GetDirectMessageChannelsAsync(Options.GetToken()); - - // Order channels - channels = channels.OrderBy(c => c.Name).ToArray(); - - // Print result - foreach (var channel in channels) - Console.WriteLine($"{channel.Id} | {channel.Name}"); - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/GetGuildsVerb.cs b/DiscordChatExporter.Cli/Verbs/GetGuildsVerb.cs deleted file mode 100644 index 1ebb037..0000000 --- a/DiscordChatExporter.Cli/Verbs/GetGuildsVerb.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DiscordChatExporter.Cli.Verbs.Options; -using DiscordChatExporter.Core.Services; - -namespace DiscordChatExporter.Cli.Verbs -{ - public class GetGuildsVerb : Verb - { - public GetGuildsVerb(GetGuildsOptions options) - : base(options) - { - } - - public override async Task ExecuteAsync() - { - // Get data service - var dataService = Container.Instance.Get(); - - // Get guilds - var guilds = await dataService.GetUserGuildsAsync(Options.GetToken()); - - // Order guilds - guilds = guilds.OrderBy(g => g.Name).ToArray(); - - // Print result - foreach (var guild in guilds) - Console.WriteLine($"{guild.Id} | {guild.Name}"); - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/Options/ExportChannelOptions.cs b/DiscordChatExporter.Cli/Verbs/Options/ExportChannelOptions.cs deleted file mode 100644 index 5278a2d..0000000 --- a/DiscordChatExporter.Cli/Verbs/Options/ExportChannelOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using CommandLine; - -namespace DiscordChatExporter.Cli.Verbs.Options -{ - [Verb("export", HelpText = "Export channel.")] - public class ExportChannelOptions : ExportOptions - { - [Option('c', "channel", Required = true, HelpText = "Channel ID.")] - public string ChannelId { get; set; } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/Options/ExportDirectMessagesOptions.cs b/DiscordChatExporter.Cli/Verbs/Options/ExportDirectMessagesOptions.cs deleted file mode 100644 index a29cedf..0000000 --- a/DiscordChatExporter.Cli/Verbs/Options/ExportDirectMessagesOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using CommandLine; - -namespace DiscordChatExporter.Cli.Verbs.Options -{ - [Verb("exportdm", HelpText = "Export all direct message channels.")] - public class ExportDirectMessagesOptions : ExportOptions - { - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/Options/ExportGuildOptions.cs b/DiscordChatExporter.Cli/Verbs/Options/ExportGuildOptions.cs deleted file mode 100644 index 0392184..0000000 --- a/DiscordChatExporter.Cli/Verbs/Options/ExportGuildOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using CommandLine; - -namespace DiscordChatExporter.Cli.Verbs.Options -{ - [Verb("exportguild", HelpText = "Export all channels within a given guild.")] - public class ExportGuildOptions : ExportOptions - { - [Option('g', "guild", Required = true, HelpText = "Guild ID.")] - public string GuildId { get; set; } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/Options/ExportOptions.cs b/DiscordChatExporter.Cli/Verbs/Options/ExportOptions.cs deleted file mode 100644 index a95978a..0000000 --- a/DiscordChatExporter.Cli/Verbs/Options/ExportOptions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using CommandLine; -using DiscordChatExporter.Core.Models; - -namespace DiscordChatExporter.Cli.Verbs.Options -{ - public abstract class ExportOptions : TokenOptions - { - [Option('f', "format", Default = ExportFormat.HtmlDark, HelpText = "Output file format.")] - public ExportFormat ExportFormat { get; set; } - - [Option('o', "output", Default = null, HelpText = "Output file or directory path.")] - public string OutputPath { get; set; } - - // HACK: CommandLineParser doesn't support DateTimeOffset - [Option("after", Default = null, HelpText = "Limit to messages sent after this date.")] - public DateTime? After { get; set; } - - // HACK: CommandLineParser doesn't support DateTimeOffset - [Option("before", Default = null, HelpText = "Limit to messages sent before this date.")] - public DateTime? Before { get; set; } - - [Option('p', "partition", Default = null, HelpText = "Split output into partitions limited to this number of messages.")] - public int? PartitionLimit { get; set; } - - [Option("dateformat", Default = null, HelpText = "Date format used in output.")] - public string DateFormat { get; set; } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/Options/GetChannelsOptions.cs b/DiscordChatExporter.Cli/Verbs/Options/GetChannelsOptions.cs deleted file mode 100644 index 117fc16..0000000 --- a/DiscordChatExporter.Cli/Verbs/Options/GetChannelsOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using CommandLine; - -namespace DiscordChatExporter.Cli.Verbs.Options -{ - [Verb("channels", HelpText = "Get the list of channels in the given guild.")] - public class GetChannelsOptions : TokenOptions - { - [Option('g', "guild", Required = true, HelpText = "Guild ID.")] - public string GuildId { get; set; } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/Options/GetDirectMessageChannelsOptions.cs b/DiscordChatExporter.Cli/Verbs/Options/GetDirectMessageChannelsOptions.cs deleted file mode 100644 index c352e86..0000000 --- a/DiscordChatExporter.Cli/Verbs/Options/GetDirectMessageChannelsOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using CommandLine; - -namespace DiscordChatExporter.Cli.Verbs.Options -{ - [Verb("dm", HelpText = "Get the list of direct message channels.")] - public class GetDirectMessageChannelsOptions : TokenOptions - { - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/Options/GetGuildsOptions.cs b/DiscordChatExporter.Cli/Verbs/Options/GetGuildsOptions.cs deleted file mode 100644 index 4e61acd..0000000 --- a/DiscordChatExporter.Cli/Verbs/Options/GetGuildsOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using CommandLine; - -namespace DiscordChatExporter.Cli.Verbs.Options -{ - [Verb("guilds", HelpText = "Get the list of accessible guilds.")] - public class GetGuildsOptions : TokenOptions - { - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/Options/TokenOptions.cs b/DiscordChatExporter.Cli/Verbs/Options/TokenOptions.cs deleted file mode 100644 index 350417e..0000000 --- a/DiscordChatExporter.Cli/Verbs/Options/TokenOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using CommandLine; -using DiscordChatExporter.Core.Models; - -namespace DiscordChatExporter.Cli.Verbs.Options -{ - public abstract class TokenOptions - { - [Option('t', "token", Required = true, HelpText = "Authorization token.")] - public string TokenValue { get; set; } - - [Option('b', "bot", Default = false, HelpText = "Whether this authorization token belongs to a bot.")] - public bool IsBotToken { get; set; } - - public AuthToken GetToken() => new AuthToken(IsBotToken ? AuthTokenType.Bot : AuthTokenType.User, TokenValue); - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Verbs/Verb.cs b/DiscordChatExporter.Cli/Verbs/Verb.cs deleted file mode 100644 index 82dafe4..0000000 --- a/DiscordChatExporter.Cli/Verbs/Verb.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading.Tasks; - -namespace DiscordChatExporter.Cli.Verbs -{ - public abstract class Verb - { - protected TOptions Options { get; } - - protected Verb(TOptions options) - { - Options = options; - } - - public abstract Task ExecuteAsync(); - - public virtual void Execute() => ExecuteAsync().GetAwaiter().GetResult(); - } -} \ No newline at end of file diff --git a/Readme.md b/Readme.md index ddb8652..4452d7a 100644 --- a/Readme.md +++ b/Readme.md @@ -34,8 +34,8 @@ DiscordChatExporter can be used to export message history from a [Discord](https - [MaterialDesignInXamlToolkit](https://github.com/ButchersBoy/MaterialDesignInXamlToolkit) - [Newtonsoft.Json](http://www.newtonsoft.com/json) - [Scriban](https://github.com/lunet-io/scriban) -- [CommandLineParser](https://github.com/commandlineparser/commandline) - [Ookii.Dialogs](https://github.com/caioproiete/ookii-dialogs-wpf) +- [CliFx](https://github.com/Tyrrrz/CliFx) - [Failsafe](https://github.com/Tyrrrz/Failsafe) - [Gress](https://github.com/Tyrrrz/Gress) - [Onova](https://github.com/Tyrrrz/Onova)