More refactoring

This commit is contained in:
Alexey Golub 2020-04-23 00:32:48 +03:00
parent b2a48d338a
commit 9d0d7cd5dd
8 changed files with 197 additions and 127 deletions

View file

@ -16,7 +16,7 @@ namespace DiscordChatExporter.Domain.Discord
Value = value; Value = value;
} }
public AuthenticationHeaderValue GetAuthenticationHeader() => Type == AuthTokenType.User public AuthenticationHeaderValue GetAuthorizationHeader() => Type == AuthTokenType.User
? new AuthenticationHeaderValue(Value) ? new AuthenticationHeaderValue(Value)
: new AuthenticationHeaderValue("Bot", Value); : new AuthenticationHeaderValue("Bot", Value);

View file

@ -29,10 +29,12 @@ namespace DiscordChatExporter.Domain.Discord
{ {
var userId = json.GetProperty("user").Pipe(ParseId); var userId = json.GetProperty("user").Pipe(ParseId);
var nick = json.GetPropertyOrNull("nick")?.GetString(); var nick = json.GetPropertyOrNull("nick")?.GetString();
var roles = json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString()).ToArray() ??
Array.Empty<string>();
return new Member(userId, nick, roles); var roleIds =
json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString()).ToArray() ??
Array.Empty<string>();
return new Member(userId, nick, roleIds);
} }
private Guild ParseGuild(JsonElement json) private Guild ParseGuild(JsonElement json)
@ -40,8 +42,10 @@ namespace DiscordChatExporter.Domain.Discord
var id = ParseId(json); var id = ParseId(json);
var name = json.GetProperty("name").GetString(); var name = json.GetProperty("name").GetString();
var iconHash = json.GetProperty("icon").GetString(); var iconHash = json.GetProperty("icon").GetString();
var roles = json.GetPropertyOrNull("roles")?.EnumerateArray().Select(ParseRole).ToArray() ??
Array.Empty<Role>(); var roles =
json.GetPropertyOrNull("roles")?.EnumerateArray().Select(ParseRole).ToArray() ??
Array.Empty<Role>();
return new Guild(id, name, iconHash, roles); return new Guild(id, name, iconHash, roles);
} }
@ -53,8 +57,9 @@ namespace DiscordChatExporter.Domain.Discord
var type = (ChannelType) json.GetProperty("type").GetInt32(); var type = (ChannelType) json.GetProperty("type").GetInt32();
var topic = json.GetPropertyOrNull("topic")?.GetString(); var topic = json.GetPropertyOrNull("topic")?.GetString();
var guildId = json.GetPropertyOrNull("guild_id")?.GetString() ?? var guildId =
Guild.DirectMessages.Id; json.GetPropertyOrNull("guild_id")?.GetString() ??
Guild.DirectMessages.Id;
var name = var name =
json.GetPropertyOrNull("name")?.GetString() ?? json.GetPropertyOrNull("name")?.GetString() ??
@ -134,10 +139,22 @@ namespace DiscordChatExporter.Domain.Discord
var image = json.GetPropertyOrNull("image")?.Pipe(ParseEmbedImage); var image = json.GetPropertyOrNull("image")?.Pipe(ParseEmbedImage);
var footer = json.GetPropertyOrNull("footer")?.Pipe(ParseEmbedFooter); var footer = json.GetPropertyOrNull("footer")?.Pipe(ParseEmbedFooter);
var fields = json.GetPropertyOrNull("fields")?.EnumerateArray().Select(ParseEmbedField).ToArray() ?? var fields =
Array.Empty<EmbedField>(); json.GetPropertyOrNull("fields")?.EnumerateArray().Select(ParseEmbedField).ToArray() ??
Array.Empty<EmbedField>();
return new Embed(title, url, timestamp, color, author, description, fields, thumbnail, image, footer); return new Embed(
title,
url,
timestamp,
color,
author,
description,
fields,
thumbnail,
image,
footer
);
} }
private Emoji ParseEmoji(JsonElement json) private Emoji ParseEmoji(JsonElement json)
@ -180,20 +197,36 @@ namespace DiscordChatExporter.Domain.Discord
var author = json.GetProperty("author").Pipe(ParseUser); var author = json.GetProperty("author").Pipe(ParseUser);
var attachments = json.GetPropertyOrNull("attachments")?.EnumerateArray().Select(ParseAttachment).ToArray() ?? var attachments =
Array.Empty<Attachment>(); json.GetPropertyOrNull("attachments")?.EnumerateArray().Select(ParseAttachment).ToArray() ??
Array.Empty<Attachment>();
var embeds = json.GetPropertyOrNull("embeds")?.EnumerateArray().Select(ParseEmbed).ToArray() ?? var embeds =
Array.Empty<Embed>(); json.GetPropertyOrNull("embeds")?.EnumerateArray().Select(ParseEmbed).ToArray() ??
Array.Empty<Embed>();
var reactions = json.GetPropertyOrNull("reactions")?.EnumerateArray().Select(ParseReaction).ToArray() ?? var reactions =
Array.Empty<Reaction>(); json.GetPropertyOrNull("reactions")?.EnumerateArray().Select(ParseReaction).ToArray() ??
Array.Empty<Reaction>();
var mentionedUsers = json.GetPropertyOrNull("mentions")?.EnumerateArray().Select(ParseUser).ToArray() ?? var mentionedUsers =
Array.Empty<User>(); json.GetPropertyOrNull("mentions")?.EnumerateArray().Select(ParseUser).ToArray() ??
Array.Empty<User>();
return new Message(id, channelId, type, author, timestamp, editedTimestamp, isPinned, content, attachments, embeds, return new Message(
reactions, mentionedUsers); id,
channelId,
type,
author,
timestamp,
editedTimestamp,
isPinned,
content,
attachments,
embeds,
reactions,
mentionedUsers
);
} }
} }
} }

View file

@ -18,6 +18,8 @@ namespace DiscordChatExporter.Domain.Discord
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly IAsyncPolicy<HttpResponseMessage> _httpRequestPolicy; private readonly IAsyncPolicy<HttpResponseMessage> _httpRequestPolicy;
private readonly Uri _baseUri = new Uri("https://discordapp.com/api/v6/", UriKind.Absolute);
public DiscordClient(AuthToken token, HttpClient httpClient) public DiscordClient(AuthToken token, HttpClient httpClient)
{ {
_token = token; _token = token;
@ -51,10 +53,8 @@ namespace DiscordChatExporter.Domain.Discord
{ {
using var response = await _httpRequestPolicy.ExecuteAsync(async () => using var response = await _httpRequestPolicy.ExecuteAsync(async () =>
{ {
var uri = new Uri(new Uri("https://discordapp.com/api/v6"), url); using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
request.Headers.Authorization = _token.GetAuthorizationHeader();
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Authorization = _token.GetAuthenticationHeader();
return await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); return await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
}); });
@ -113,11 +113,13 @@ namespace DiscordChatExporter.Domain.Discord
while (true) while (true)
{ {
var route = "users/@me/guilds?limit=100"; var url = new UrlBuilder()
if (!string.IsNullOrWhiteSpace(afterId)) .SetPath("users/@me/guilds")
route += $"&after={afterId}"; .SetQueryParameter("limit", "100")
.SetQueryParameterIfNotNullOrWhiteSpace("after", afterId)
.Build();
var response = await GetApiResponseAsync(route); var response = await GetApiResponseAsync(url);
var isEmpty = true; var isEmpty = true;
@ -147,7 +149,7 @@ namespace DiscordChatExporter.Domain.Discord
public async Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string guildId) public async Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string guildId)
{ {
// Special case for direct messages pseudo-guild // Direct messages pseudo-guild
if (guildId == Guild.DirectMessages.Id) if (guildId == Guild.DirectMessages.Id)
return Array.Empty<Channel>(); return Array.Empty<Channel>();
@ -159,38 +161,42 @@ namespace DiscordChatExporter.Domain.Discord
private async Task<Message> GetLastMessageAsync(string channelId, DateTimeOffset? before = null) private async Task<Message> GetLastMessageAsync(string channelId, DateTimeOffset? before = null)
{ {
var route = $"channels/{channelId}/messages?limit=1"; var url = new UrlBuilder()
if (before != null) .SetPath($"channels/{channelId}/messages")
route += $"&before={before.Value.ToSnowflake()}"; .SetQueryParameter("limit", "1")
.SetQueryParameterIfNotNullOrWhiteSpace("before", before?.ToSnowflake())
.Build();
var response = await GetApiResponseAsync(route); var response = await GetApiResponseAsync(url);
return response.EnumerateArray().Select(ParseMessage).FirstOrDefault(); return response.EnumerateArray().Select(ParseMessage).FirstOrDefault();
} }
public async IAsyncEnumerable<Message> GetMessagesAsync(string channelId, public async IAsyncEnumerable<Message> GetMessagesAsync(
DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress<double>? progress = null) string channelId,
DateTimeOffset? after = null,
DateTimeOffset? before = null,
IProgress<double>? progress = null)
{ {
// Get the last message
var lastMessage = await GetLastMessageAsync(channelId, before); var lastMessage = await GetLastMessageAsync(channelId, before);
// If the last message doesn't exist or it's outside of range - return // If the last message doesn't exist or it's outside of range - return
if (lastMessage == null || lastMessage.Timestamp < after) if (lastMessage == null || lastMessage.Timestamp < after)
{
progress?.Report(1);
yield break; yield break;
}
// Get other messages
var firstMessage = default(Message); var firstMessage = default(Message);
var afterId = after?.ToSnowflake() ?? "0"; var afterId = after?.ToSnowflake() ?? "0";
while (true) while (true)
{ {
// Get message batch var url = new UrlBuilder()
var route = $"channels/{channelId}/messages?limit=100&after={afterId}"; .SetPath($"channels/{channelId}/messages")
var response = await GetApiResponseAsync(route); .SetQueryParameter("limit", "100")
.SetQueryParameter("after", afterId)
.Build();
var response = await GetApiResponseAsync(url);
// Parse
var messages = response var messages = response
.EnumerateArray() .EnumerateArray()
.Select(ParseMessage) .Select(ParseMessage)
@ -201,33 +207,28 @@ namespace DiscordChatExporter.Domain.Discord
if (!messages.Any()) if (!messages.Any())
break; break;
// Trim messages to range (until last message) foreach (var message in messages)
var messagesInRange = messages
.TakeWhile(m => m.Id != lastMessage.Id && m.Timestamp < lastMessage.Timestamp)
.ToArray();
// Yield messages
foreach (var message in messagesInRange)
{ {
// Set first message if it's not set
firstMessage ??= message; firstMessage ??= message;
// Report progress (based on the time range of parsed messages compared to total) // Ensure messages are in range (take into account that last message could have been deleted)
progress?.Report((message.Timestamp - firstMessage.Timestamp).TotalSeconds / if (message.Timestamp > lastMessage.Timestamp)
(lastMessage.Timestamp - firstMessage.Timestamp).TotalSeconds); yield break;
// Report progress based on the duration of parsed messages divided by total
progress?.Report(
(message.Timestamp - firstMessage.Timestamp) /
(lastMessage.Timestamp - firstMessage.Timestamp)
);
yield return message; yield return message;
afterId = message.Id; afterId = message.Id;
// Yielded last message - break loop
if (message.Id == lastMessage.Id)
yield break;
} }
// Break if messages were trimmed (which means the last message was encountered)
if (messagesInRange.Length != messages.Length)
break;
} }
// Yield last message
yield return lastMessage;
progress?.Report(1);
} }
} }

View file

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
namespace DiscordChatExporter.Domain.Discord
{
internal class UrlBuilder
{
private string _path = "";
private readonly Dictionary<string, string?> _queryParameters =
new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
public UrlBuilder SetPath(string path)
{
_path = path;
return this;
}
public UrlBuilder SetQueryParameter(string key, string? value)
{
var keyEncoded = WebUtility.UrlEncode(key);
var valueEncoded = WebUtility.UrlEncode(value);
_queryParameters[keyEncoded] = valueEncoded;
return this;
}
public UrlBuilder SetQueryParameterIfNotNullOrWhiteSpace(string key, string? value) =>
!string.IsNullOrWhiteSpace(value)
? SetQueryParameter(key, value)
: this;
public string Build()
{
var buffer = new StringBuilder();
buffer.Append(_path);
if (_queryParameters.Any())
buffer.Append('?');
buffer.AppendJoin('&', _queryParameters.Select(kvp => $"{kvp.Key}={kvp.Value}"));
return buffer.ToString();
}
}
}

View file

@ -17,6 +17,8 @@ namespace DiscordChatExporter.Domain.Exporting
public ChannelExporter(DiscordClient discord) => _discord = discord; public ChannelExporter(DiscordClient discord) => _discord = discord;
public ChannelExporter(AuthToken token) : this(new DiscordClient(token)) {}
public async Task ExportAsync( public async Task ExportAsync(
Guild guild, Guild guild,
Channel channel, Channel channel,
@ -28,13 +30,12 @@ namespace DiscordChatExporter.Domain.Exporting
DateTimeOffset? before = null, DateTimeOffset? before = null,
IProgress<double>? progress = null) IProgress<double>? progress = null)
{ {
// Get base file path from output path
var baseFilePath = GetFilePathFromOutputPath(guild, channel, outputPath, format, after, before); var baseFilePath = GetFilePathFromOutputPath(guild, channel, outputPath, format, after, before);
// Create options // Options
var options = new ExportOptions(baseFilePath, format, partitionLimit); var options = new ExportOptions(baseFilePath, format, partitionLimit);
// Create context // Context
var mentionableUsers = new HashSet<User>(IdBasedEqualityComparer.Instance); var mentionableUsers = new HashSet<User>(IdBasedEqualityComparer.Instance);
var mentionableChannels = await _discord.GetGuildChannelsAsync(guild.Id); var mentionableChannels = await _discord.GetGuildChannelsAsync(guild.Id);
var mentionableRoles = guild.Roles; var mentionableRoles = guild.Roles;
@ -44,11 +45,9 @@ namespace DiscordChatExporter.Domain.Exporting
mentionableUsers, mentionableChannels, mentionableRoles mentionableUsers, mentionableChannels, mentionableRoles
); );
// Create renderer await using var messageExporter = new MessageExporter(options, context);
await using var renderer = new MessageExporter(options, context);
// Render messages var exportedAnything = false;
var renderedAnything = false;
await foreach (var message in _discord.GetMessagesAsync(channel.Id, after, before, progress)) await foreach (var message in _discord.GetMessagesAsync(channel.Id, after, before, progress))
{ {
// Add encountered users to the list of mentionable users // Add encountered users to the list of mentionable users
@ -68,12 +67,12 @@ namespace DiscordChatExporter.Domain.Exporting
} }
// Render message // Render message
await renderer.RenderMessageAsync(message); await messageExporter.ExportMessageAsync(message);
renderedAnything = true; exportedAnything = true;
} }
// Throw if no messages were rendered // Throw if no messages were exported
if (!renderedAnything) if (!exportedAnything)
throw DiscordChatExporterException.ChannelEmpty(channel); throw DiscordChatExporterException.ChannelEmpty(channel);
} }
} }

View file

@ -62,7 +62,7 @@ namespace DiscordChatExporter.Domain.Exporting
return _writer = writer; return _writer = writer;
} }
public async Task RenderMessageAsync(Message message) public async Task ExportMessageAsync(Message message)
{ {
var writer = await GetWriterAsync(); var writer = await GetWriterAsync();
await writer.WriteMessageAsync(message); await writer.WriteMessageAsync(message);

View file

@ -31,6 +31,16 @@
<xs:documentation>Used to control if equality checks should use the static Equals method resolved from the base class.</xs:documentation> <xs:documentation>Used to control if equality checks should use the static Equals method resolved from the base class.</xs:documentation>
</xs:annotation> </xs:annotation>
</xs:attribute> </xs:attribute>
<xs:attribute name="SuppressWarnings" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to turn off build warnings from this weaver.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="SuppressOnPropertyNameChangedWarning" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to turn off build warnings about mismatched On_PropertyName_Changed methods.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType> </xs:complexType>
</xs:element> </xs:element>
</xs:all> </xs:all>

View file

@ -54,35 +54,28 @@ namespace DiscordChatExporter.Gui.ViewModels
_settingsService = settingsService; _settingsService = settingsService;
_updateService = updateService; _updateService = updateService;
// Set title
DisplayName = $"{App.Name} v{App.VersionString}"; DisplayName = $"{App.Name} v{App.VersionString}";
// Update busy state when progress manager changes // Update busy state when progress manager changes
ProgressManager.Bind(o => o.IsActive, (sender, args) => IsBusy = ProgressManager.IsActive); ProgressManager.Bind(o => o.IsActive,
(sender, args) => IsBusy = ProgressManager.IsActive);
ProgressManager.Bind(o => o.IsActive, ProgressManager.Bind(o => o.IsActive,
(sender, args) => IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress.IsEither(0, 1)); (sender, args) => IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress.IsEither(0, 1));
ProgressManager.Bind(o => o.Progress, ProgressManager.Bind(o => o.Progress,
(sender, args) => IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress.IsEither(0, 1)); (sender, args) => IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress.IsEither(0, 1));
} }
private DiscordClient GetDiscordClient(AuthToken token) => new DiscordClient(token);
private ChannelExporter GetChannelExporter(AuthToken token) => new ChannelExporter(GetDiscordClient(token));
private async Task HandleAutoUpdateAsync() private async Task HandleAutoUpdateAsync()
{ {
try try
{ {
// Check for updates
var updateVersion = await _updateService.CheckForUpdatesAsync(); var updateVersion = await _updateService.CheckForUpdatesAsync();
if (updateVersion == null) if (updateVersion == null)
return; return;
// Notify user of an update and prepare it
Notifications.Enqueue($"Downloading update to {App.Name} v{updateVersion}..."); Notifications.Enqueue($"Downloading update to {App.Name} v{updateVersion}...");
await _updateService.PrepareUpdateAsync(updateVersion); await _updateService.PrepareUpdateAsync(updateVersion);
// Prompt user to install update (otherwise install it when application exits)
Notifications.Enqueue( Notifications.Enqueue(
"Update has been downloaded and will be installed when you exit", "Update has been downloaded and will be installed when you exit",
"INSTALL NOW", () => "INSTALL NOW", () =>
@ -102,17 +95,14 @@ namespace DiscordChatExporter.Gui.ViewModels
{ {
base.OnViewLoaded(); base.OnViewLoaded();
// Load settings
_settingsService.Load(); _settingsService.Load();
// Get last token
if (_settingsService.LastToken != null) if (_settingsService.LastToken != null)
{ {
IsBotToken = _settingsService.LastToken.Type == AuthTokenType.Bot; IsBotToken = _settingsService.LastToken.Type == AuthTokenType.Bot;
TokenValue = _settingsService.LastToken.Value; TokenValue = _settingsService.LastToken.Value;
} }
// Check and prepare update
await HandleAutoUpdateAsync(); await HandleAutoUpdateAsync();
} }
@ -120,49 +110,44 @@ namespace DiscordChatExporter.Gui.ViewModels
{ {
base.OnClose(); base.OnClose();
// Save settings
_settingsService.Save(); _settingsService.Save();
// Finalize updates if necessary
_updateService.FinalizeUpdate(false); _updateService.FinalizeUpdate(false);
} }
public async void ShowSettings() public async void ShowSettings()
{ {
// Create dialog
var dialog = _viewModelFactory.CreateSettingsViewModel(); var dialog = _viewModelFactory.CreateSettingsViewModel();
// Show dialog
await _dialogManager.ShowDialogAsync(dialog); await _dialogManager.ShowDialogAsync(dialog);
} }
public bool CanPopulateGuildsAndChannels => !IsBusy && !string.IsNullOrWhiteSpace(TokenValue); public bool CanPopulateGuildsAndChannels =>
!IsBusy && !string.IsNullOrWhiteSpace(TokenValue);
public async void PopulateGuildsAndChannels() public async void PopulateGuildsAndChannels()
{ {
// Create progress operation using var operation = ProgressManager.CreateOperation();
var operation = ProgressManager.CreateOperation();
try try
{ {
// Sanitize token var tokenValue = TokenValue?.Trim('"');
TokenValue = TokenValue!.Trim('"'); if (string.IsNullOrWhiteSpace(tokenValue))
return;
// Create token
var token = new AuthToken( var token = new AuthToken(
IsBotToken ? AuthTokenType.Bot : AuthTokenType.User, IsBotToken ? AuthTokenType.Bot : AuthTokenType.User,
TokenValue); tokenValue
);
// Save token
_settingsService.LastToken = token; _settingsService.LastToken = token;
// Prepare available guild list var discord = new DiscordClient(token);
var availableGuilds = new List<GuildViewModel>(); var availableGuilds = new List<GuildViewModel>();
// Get direct messages // Direct messages
{ {
var guild = Guild.DirectMessages; var guild = Guild.DirectMessages;
var channels = await GetDiscordClient(token).GetDirectMessageChannelsAsync(); var channels = await discord.GetDirectMessageChannelsAsync();
// Create channel view models // Create channel view models
var channelViewModels = new List<ChannelViewModel>(); var channelViewModels = new List<ChannelViewModel>();
@ -188,11 +173,11 @@ namespace DiscordChatExporter.Gui.ViewModels
availableGuilds.Add(guildViewModel); availableGuilds.Add(guildViewModel);
} }
// Get guilds // Guilds
var guilds = await GetDiscordClient(token).GetUserGuildsAsync(); var guilds = await discord.GetUserGuildsAsync();
foreach (var guild in guilds) foreach (var guild in guilds)
{ {
var channels = await GetDiscordClient(token).GetGuildChannelsAsync(guild.Id); var channels = await discord.GetGuildChannelsAsync(guild.Id);
var categoryChannels = channels.Where(c => c.Type == ChannelType.GuildCategory).ToArray(); var categoryChannels = channels.Where(c => c.Type == ChannelType.GuildCategory).ToArray();
var exportableChannels = channels.Where(c => c.IsTextChannel).ToArray(); var exportableChannels = channels.Where(c => c.IsTextChannel).ToArray();
@ -220,40 +205,32 @@ namespace DiscordChatExporter.Gui.ViewModels
availableGuilds.Add(guildViewModel); availableGuilds.Add(guildViewModel);
} }
// Update available guild list
AvailableGuilds = availableGuilds; AvailableGuilds = availableGuilds;
// Pre-select first guild
SelectedGuild = AvailableGuilds.FirstOrDefault(); SelectedGuild = AvailableGuilds.FirstOrDefault();
} }
catch (DiscordChatExporterException ex) when (!ex.IsCritical) catch (DiscordChatExporterException ex) when (!ex.IsCritical)
{ {
Notifications.Enqueue(ex.Message); Notifications.Enqueue(ex.Message.TrimEnd('.'));
}
finally
{
operation.Dispose();
} }
} }
public bool CanExportChannels => !IsBusy && SelectedGuild != null && SelectedChannels != null && SelectedChannels.Any(); public bool CanExportChannels =>
!IsBusy && SelectedGuild != null && SelectedChannels != null && SelectedChannels.Any();
public async void ExportChannels() public async void ExportChannels()
{ {
// Get last used token var token = _settingsService.LastToken;
var token = _settingsService.LastToken!; if (token == null || SelectedGuild == null || SelectedChannels == null || !SelectedChannels.Any())
return;
// Create dialog var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels);
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild!, SelectedChannels!);
// Show dialog, if canceled - return
if (await _dialogManager.ShowDialogAsync(dialog) != true) if (await _dialogManager.ShowDialogAsync(dialog) != true)
return; return;
// Create a progress operation for each channel to export var exporter = new ChannelExporter(token);
var operations = ProgressManager.CreateOperations(dialog.Channels!.Count); var operations = ProgressManager.CreateOperations(dialog.Channels!.Count);
// Export channels
var successfulExportCount = 0; var successfulExportCount = 0;
await dialog.Channels.Zip(operations).ParallelForEachAsync(async tuple => await dialog.Channels.Zip(operations).ParallelForEachAsync(async tuple =>
{ {
@ -261,7 +238,7 @@ namespace DiscordChatExporter.Gui.ViewModels
try try
{ {
await GetChannelExporter(token).ExportAsync(dialog.Guild!, channel!, await exporter.ExportAsync(dialog.Guild!, channel!,
dialog.OutputPath!, dialog.SelectedFormat, _settingsService.DateFormat, dialog.OutputPath!, dialog.SelectedFormat, _settingsService.DateFormat,
dialog.PartitionLimit, dialog.After, dialog.Before, operation); dialog.PartitionLimit, dialog.After, dialog.Before, operation);
@ -269,7 +246,7 @@ namespace DiscordChatExporter.Gui.ViewModels
} }
catch (DiscordChatExporterException ex) when (!ex.IsCritical) catch (DiscordChatExporterException ex) when (!ex.IsCritical)
{ {
Notifications.Enqueue(ex.Message); Notifications.Enqueue(ex.Message.TrimEnd('.'));
} }
finally finally
{ {