Rework architecture

This commit is contained in:
Alexey Golub
2020-04-21 21:30:42 +03:00
parent 130c0b6fe2
commit 8685a3d7e3
119 changed files with 1520 additions and 1560 deletions

View File

@@ -4,17 +4,13 @@ using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Utilities;
using DiscordChatExporter.Core.Models;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Domain.Discord.Models;
using DiscordChatExporter.Domain.Exporting;
namespace DiscordChatExporter.Cli.Commands
namespace DiscordChatExporter.Cli.Commands.Base
{
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;
@@ -31,25 +27,17 @@ namespace DiscordChatExporter.Cli.Commands
public int? PartitionLimit { get; set; }
[CommandOption("dateformat", Description = "Date format used in output.")]
public string? DateFormat { get; set; }
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
protected ExportCommandBase(SettingsService settingsService, DataService dataService, ExportService exportService)
: base(dataService)
{
SettingsService = settingsService;
ExportService = exportService;
}
protected Exporter GetExporter() => new Exporter(GetDiscordClient());
protected async ValueTask ExportAsync(IConsole console, Guild guild, Channel channel)
{
if (!string.IsNullOrWhiteSpace(DateFormat))
SettingsService.DateFormat = DateFormat;
console.Output.Write($"Exporting channel [{channel.Name}]... ");
console.Output.Write($"Exporting channel '{channel.Name}'... ");
var progress = console.CreateProgressTicker();
await ExportService.ExportChatLogAsync(Token, guild, channel,
OutputPath, ExportFormat, PartitionLimit,
await GetExporter().ExportChatLogAsync(guild, channel,
OutputPath, ExportFormat, DateFormat, PartitionLimit,
After, Before, progress);
console.Output.WriteLine();
@@ -58,13 +46,13 @@ namespace DiscordChatExporter.Cli.Commands
protected async ValueTask ExportAsync(IConsole console, Channel channel)
{
var guild = await DataService.GetGuildAsync(Token, channel.GuildId);
var guild = await GetDiscordClient().GetGuildAsync(channel.GuildId);
await ExportAsync(console, guild, channel);
}
protected async ValueTask ExportAsync(IConsole console, string channelId)
{
var channel = await DataService.GetChannelAsync(Token, channelId);
var channel = await GetDiscordClient().GetChannelAsync(channelId);
await ExportAsync(console, channel);
}
}

View File

@@ -1,38 +1,28 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Utilities;
using DiscordChatExporter.Core.Models;
using DiscordChatExporter.Core.Models.Exceptions;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Core.Services.Exceptions;
using DiscordChatExporter.Domain.Discord.Models;
using DiscordChatExporter.Domain.Exceptions;
using DiscordChatExporter.Domain.Utilities;
using Gress;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Cli.Commands
namespace DiscordChatExporter.Cli.Commands.Base
{
public abstract class ExportMultipleCommandBase : ExportCommandBase
{
[CommandOption("parallel", Description = "Export this number of separate channels in parallel.")]
public int ParallelLimit { get; set; } = 1;
protected ExportMultipleCommandBase(SettingsService settingsService, DataService dataService, ExportService exportService)
: base(settingsService, dataService, exportService)
{
}
protected async ValueTask ExportMultipleAsync(IConsole console, IReadOnlyList<Channel> channels)
{
// This uses a separate route from ExportCommandBase because the progress ticker is not thread-safe
// Ugly code ahead. Will need to refactor.
if (!string.IsNullOrWhiteSpace(DateFormat))
SettingsService.DateFormat = DateFormat;
// Progress
console.Output.Write($"Exporting {channels.Count} channels... ");
var ticker = console.CreateProgressTicker();
@@ -44,41 +34,33 @@ namespace DiscordChatExporter.Cli.Commands
var operations = progressManager.CreateOperations(channels.Count);
// Export channels
using var semaphore = new SemaphoreSlim(ParallelLimit.ClampMin(1));
var errors = new List<string>();
await Task.WhenAll(channels.Select(async (channel, i) =>
var successfulExportCount = 0;
await channels.Zip(operations).ParallelForEachAsync(async tuple =>
{
var operation = operations[i];
await semaphore.WaitAsync();
var guild = await DataService.GetGuildAsync(Token, channel.GuildId);
var (channel, operation) = tuple;
try
{
await ExportService.ExportChatLogAsync(Token, guild, channel,
OutputPath, ExportFormat, PartitionLimit,
var guild = await GetDiscordClient().GetGuildAsync(channel.GuildId);
await GetExporter().ExportChatLogAsync(guild, channel,
OutputPath, ExportFormat, DateFormat, PartitionLimit,
After, Before, operation);
Interlocked.Increment(ref successfulExportCount);
}
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
{
errors.Add("You don't have access to this channel.");
}
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
errors.Add("This channel doesn't exist.");
}
catch (DomainException ex)
catch (DiscordChatExporterException ex) when (!ex.IsCritical)
{
errors.Add(ex.Message);
}
finally
{
semaphore.Release();
operation.Dispose();
}
}));
}, ParallelLimit.ClampMin(1));
ticker.Report(1);
console.Output.WriteLine();
@@ -86,7 +68,7 @@ namespace DiscordChatExporter.Cli.Commands
foreach (var error in errors)
console.Error.WriteLine(error);
console.Output.WriteLine("Done.");
console.Output.WriteLine($"Successfully exported {successfulExportCount} channel(s).");
}
}
}

View File

@@ -1,15 +1,12 @@
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using DiscordChatExporter.Core.Models;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Domain.Discord;
namespace DiscordChatExporter.Cli.Commands
namespace DiscordChatExporter.Cli.Commands.Base
{
public abstract class TokenCommandBase : ICommand
{
protected DataService DataService { get; }
[CommandOption("token", 't', IsRequired = true, EnvironmentVariableName = "DISCORD_TOKEN",
Description = "Authorization token.")]
public string TokenValue { get; set; } = "";
@@ -18,12 +15,9 @@ namespace DiscordChatExporter.Cli.Commands
Description = "Whether this authorization token belongs to a bot.")]
public bool IsBotToken { get; set; }
protected AuthToken Token => new AuthToken(IsBotToken ? AuthTokenType.Bot : AuthTokenType.User, TokenValue);
protected AuthToken GetAuthToken() => new AuthToken(IsBotToken ? AuthTokenType.Bot : AuthTokenType.User, TokenValue);
protected TokenCommandBase(DataService dataService)
{
DataService = dataService;
}
protected DiscordClient GetDiscordClient() => new DiscordClient(GetAuthToken());
public abstract ValueTask ExecuteAsync(IConsole console);
}

View File

@@ -1,7 +1,7 @@
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Cli.Commands.Base;
namespace DiscordChatExporter.Cli.Commands
{
@@ -11,11 +11,6 @@ namespace DiscordChatExporter.Cli.Commands
[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 ValueTask ExecuteAsync(IConsole console) => await ExportAsync(console, ChannelId);
}
}

View File

@@ -2,21 +2,16 @@
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Cli.Commands.Base;
namespace DiscordChatExporter.Cli.Commands
{
[Command("exportdm", Description = "Export all direct message channels.")]
public class ExportDirectMessagesCommand : ExportMultipleCommandBase
{
public ExportDirectMessagesCommand(SettingsService settingsService, DataService dataService, ExportService exportService)
: base(settingsService, dataService, exportService)
{
}
public override async ValueTask ExecuteAsync(IConsole console)
{
var directMessageChannels = await DataService.GetDirectMessageChannelsAsync(Token);
var directMessageChannels = await GetDiscordClient().GetDirectMessageChannelsAsync();
var channels = directMessageChannels.OrderBy(c => c.Name).ToArray();
await ExportMultipleAsync(console, channels);

View File

@@ -2,8 +2,7 @@
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using DiscordChatExporter.Core.Models;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Cli.Commands.Base;
namespace DiscordChatExporter.Cli.Commands
{
@@ -13,17 +12,12 @@ namespace DiscordChatExporter.Cli.Commands
[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 ValueTask ExecuteAsync(IConsole console)
{
var guildChannels = await DataService.GetGuildChannelsAsync(Token, GuildId);
var guildChannels = await GetDiscordClient().GetGuildChannelsAsync(GuildId);
var channels = guildChannels
.Where(c => c.Type.IsExportable())
.Where(c => c.IsTextChannel)
.OrderBy(c => c.Name)
.ToArray();

View File

@@ -2,8 +2,7 @@
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using DiscordChatExporter.Core.Models;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Cli.Commands.Base;
namespace DiscordChatExporter.Cli.Commands
{
@@ -13,17 +12,12 @@ namespace DiscordChatExporter.Cli.Commands
[CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")]
public string GuildId { get; set; } = "";
public GetChannelsCommand(DataService dataService)
: base(dataService)
{
}
public override async ValueTask ExecuteAsync(IConsole console)
{
var guildChannels = await DataService.GetGuildChannelsAsync(Token, GuildId);
var guildChannels = await GetDiscordClient().GetGuildChannelsAsync(GuildId);
var channels = guildChannels
.Where(c => c.Type.IsExportable())
.Where(c => c.IsTextChannel)
.OrderBy(c => c.Name)
.ToArray();

View File

@@ -2,21 +2,16 @@
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Cli.Commands.Base;
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 ValueTask ExecuteAsync(IConsole console)
{
var directMessageChannels = await DataService.GetDirectMessageChannelsAsync(Token);
var directMessageChannels = await GetDiscordClient().GetDirectMessageChannelsAsync();
var channels = directMessageChannels.OrderBy(c => c.Name).ToArray();
foreach (var channel in channels)

View File

@@ -2,21 +2,17 @@
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Cli.Commands.Base;
using DiscordChatExporter.Domain.Discord;
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 ValueTask ExecuteAsync(IConsole console)
{
var guilds = await DataService.GetUserGuildsAsync(Token);
var guilds = await GetDiscordClient().GetUserGuildsAsync();
foreach (var guild in guilds.OrderBy(g => g.Name))
console.Output.WriteLine($"{guild.Id} | {guild.Name}");

View File

@@ -9,13 +9,11 @@
<ItemGroup>
<PackageReference Include="CliFx" Version="1.0.0" />
<PackageReference Include="Gress" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" />
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DiscordChatExporter.Core.Models\DiscordChatExporter.Core.Models.csproj" />
<ProjectReference Include="..\DiscordChatExporter.Core.Services\DiscordChatExporter.Core.Services.csproj" />
<ProjectReference Include="..\DiscordChatExporter.Domain\DiscordChatExporter.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,44 +1,14 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using CliFx;
using DiscordChatExporter.Cli.Commands;
using DiscordChatExporter.Core.Services;
using Microsoft.Extensions.DependencyInjection;
namespace DiscordChatExporter.Cli
{
public static class Program
{
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
// Register services
services.AddSingleton<DataService>();
services.AddSingleton<ExportService>();
services.AddSingleton<SettingsService>();
// Register commands
services.AddTransient<ExportChannelCommand>();
services.AddTransient<ExportDirectMessagesCommand>();
services.AddTransient<ExportGuildCommand>();
services.AddTransient<GetChannelsCommand>();
services.AddTransient<GetDirectMessageChannelsCommand>();
services.AddTransient<GetGuildsCommand>();
services.AddTransient<GuideCommand>();
return services.BuildServiceProvider();
}
public static async Task<int> Main(string[] args)
{
var serviceProvider = ConfigureServices();
return await new CliApplicationBuilder()
public static async Task<int> Main(string[] args) =>
await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.UseTypeActivator(serviceProvider.GetService)
.Build()
.RunAsync(args);
}
}
}