diff --git a/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs b/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs index 913d4ce..a81d3c9 100644 --- a/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportDirectMessagesCommand.cs @@ -1,8 +1,9 @@ -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using CliFx; using CliFx.Attributes; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Domain.Discord.Models; +using DiscordChatExporter.Domain.Utilities; namespace DiscordChatExporter.Cli.Commands { @@ -11,10 +12,8 @@ namespace DiscordChatExporter.Cli.Commands { public override async ValueTask ExecuteAsync(IConsole console) { - var directMessageChannels = await GetDiscordClient().GetDirectMessageChannelsAsync(); - var channels = directMessageChannels.OrderBy(c => c.Name).ToArray(); - - await ExportMultipleAsync(console, channels); + var dmChannels = await GetDiscordClient().GetGuildChannelsAsync(Guild.DirectMessages.Id); + await ExportMultipleAsync(console, dmChannels); } } } \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs index f15e806..a5fe430 100644 --- a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs @@ -1,8 +1,8 @@ -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using CliFx; using CliFx.Attributes; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Domain.Utilities; namespace DiscordChatExporter.Cli.Commands { @@ -15,13 +15,7 @@ namespace DiscordChatExporter.Cli.Commands public override async ValueTask ExecuteAsync(IConsole console) { var guildChannels = await GetDiscordClient().GetGuildChannelsAsync(GuildId); - - var channels = guildChannels - .Where(c => c.IsTextChannel) - .OrderBy(c => c.Name) - .ToArray(); - - await ExportMultipleAsync(console, channels); + await ExportMultipleAsync(console, guildChannels); } } } \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs index d1a3a7b..86da571 100644 --- a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using CliFx; using CliFx.Attributes; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Domain.Utilities; namespace DiscordChatExporter.Cli.Commands { @@ -16,12 +17,7 @@ namespace DiscordChatExporter.Cli.Commands { var guildChannels = await GetDiscordClient().GetGuildChannelsAsync(GuildId); - var channels = guildChannels - .Where(c => c.IsTextChannel) - .OrderBy(c => c.Name) - .ToArray(); - - foreach (var channel in channels) + foreach (var channel in guildChannels.OrderBy(c => c.Name)) console.Output.WriteLine($"{channel.Id} | {channel.Name}"); } } diff --git a/DiscordChatExporter.Cli/Commands/GetDirectMessageChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetDirectMessageChannelsCommand.cs index 58cdfe8..12bc32c 100644 --- a/DiscordChatExporter.Cli/Commands/GetDirectMessageChannelsCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GetDirectMessageChannelsCommand.cs @@ -3,6 +3,8 @@ using System.Threading.Tasks; using CliFx; using CliFx.Attributes; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Domain.Discord.Models; +using DiscordChatExporter.Domain.Utilities; namespace DiscordChatExporter.Cli.Commands { @@ -11,10 +13,9 @@ namespace DiscordChatExporter.Cli.Commands { public override async ValueTask ExecuteAsync(IConsole console) { - var directMessageChannels = await GetDiscordClient().GetDirectMessageChannelsAsync(); - var channels = directMessageChannels.OrderBy(c => c.Name).ToArray(); + var dmChannels = await GetDiscordClient().GetGuildChannelsAsync(Guild.DirectMessages.Id); - foreach (var channel in channels) + foreach (var channel in dmChannels.OrderBy(c => c.Name)) console.Output.WriteLine($"{channel.Id} | {channel.Name}"); } } diff --git a/DiscordChatExporter.Domain/Discord/DiscordClient.Parsing.cs b/DiscordChatExporter.Domain/Discord/DiscordClient.Parsing.cs deleted file mode 100644 index 0b7d858..0000000 --- a/DiscordChatExporter.Domain/Discord/DiscordClient.Parsing.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System; -using System.Drawing; -using System.Linq; -using System.Text.Json; -using DiscordChatExporter.Domain.Discord.Models; -using DiscordChatExporter.Domain.Discord.Models.Common; -using DiscordChatExporter.Domain.Internal; -using Tyrrrz.Extensions; - -namespace DiscordChatExporter.Domain.Discord -{ - public partial class DiscordClient - { - private string ParseId(JsonElement json) => - json.GetProperty("id").GetString(); - - private User ParseUser(JsonElement json) - { - var id = ParseId(json); - var discriminator = json.GetProperty("discriminator").GetString().Pipe(int.Parse); - var name = json.GetProperty("username").GetString(); - var avatarHash = json.GetProperty("avatar").GetString(); - var isBot = json.GetPropertyOrNull("bot")?.GetBoolean() ?? false; - - return new User(id, discriminator, name, avatarHash, isBot); - } - - private Member ParseMember(JsonElement json) - { - var userId = json.GetProperty("user").Pipe(ParseId); - var nick = json.GetPropertyOrNull("nick")?.GetString(); - - var roleIds = - json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString()).ToArray() ?? - Array.Empty(); - - return new Member(userId, nick, roleIds); - } - - private Guild ParseGuild(JsonElement json) - { - var id = ParseId(json); - var name = json.GetProperty("name").GetString(); - var iconHash = json.GetProperty("icon").GetString(); - - var roles = - json.GetPropertyOrNull("roles")?.EnumerateArray().Select(ParseRole).ToArray() ?? - Array.Empty(); - - return new Guild(id, name, iconHash, roles); - } - - private Channel ParseChannel(JsonElement json) - { - var id = ParseId(json); - var parentId = json.GetPropertyOrNull("parent_id")?.GetString(); - var type = (ChannelType) json.GetProperty("type").GetInt32(); - var topic = json.GetPropertyOrNull("topic")?.GetString(); - - var guildId = - json.GetPropertyOrNull("guild_id")?.GetString() ?? - Guild.DirectMessages.Id; - - var name = - json.GetPropertyOrNull("name")?.GetString() ?? - json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(ParseUser).Select(u => u.Name).JoinToString(", ") ?? - id; - - return new Channel(id, guildId, parentId, type, name, topic); - } - - private Role ParseRole(JsonElement json) - { - var id = ParseId(json); - var name = json.GetProperty("name").GetString(); - var color = json.GetPropertyOrNull("color")?.GetInt32().Pipe(Color.FromArgb).ResetAlpha().NullIf(c => c.ToRgb() <= 0); - var position = json.GetProperty("position").GetInt32(); - - return new Role(id, name, color, position); - } - - private Attachment ParseAttachment(JsonElement json) - { - var id = ParseId(json); - var url = json.GetProperty("url").GetString(); - var width = json.GetPropertyOrNull("width")?.GetInt32(); - var height = json.GetPropertyOrNull("height")?.GetInt32(); - var fileName = json.GetProperty("filename").GetString(); - var fileSize = json.GetProperty("size").GetInt64().Pipe(FileSize.FromBytes); - - return new Attachment(id, url, fileName, width, height, fileSize); - } - - private EmbedAuthor ParseEmbedAuthor(JsonElement json) - { - var name = json.GetPropertyOrNull("name")?.GetString(); - var url = json.GetPropertyOrNull("url")?.GetString(); - var iconUrl = json.GetPropertyOrNull("icon_url")?.GetString(); - - return new EmbedAuthor(name, url, iconUrl); - } - - private EmbedField ParseEmbedField(JsonElement json) - { - var name = json.GetProperty("name").GetString(); - var value = json.GetProperty("value").GetString(); - var isInline = json.GetPropertyOrNull("inline")?.GetBoolean() ?? false; - - return new EmbedField(name, value, isInline); - } - - private EmbedImage ParseEmbedImage(JsonElement json) - { - var url = json.GetPropertyOrNull("url")?.GetString(); - var width = json.GetPropertyOrNull("width")?.GetInt32(); - var height = json.GetPropertyOrNull("height")?.GetInt32(); - - return new EmbedImage(url, width, height); - } - - private EmbedFooter ParseEmbedFooter(JsonElement json) - { - var text = json.GetProperty("text").GetString(); - var iconUrl = json.GetPropertyOrNull("icon_url")?.GetString(); - - return new EmbedFooter(text, iconUrl); - } - - private Embed ParseEmbed(JsonElement json) - { - var title = json.GetPropertyOrNull("title")?.GetString(); - var description = json.GetPropertyOrNull("description")?.GetString(); - var url = json.GetPropertyOrNull("url")?.GetString(); - var timestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset(); - var color = json.GetPropertyOrNull("color")?.GetInt32().Pipe(Color.FromArgb).ResetAlpha(); - - var author = json.GetPropertyOrNull("author")?.Pipe(ParseEmbedAuthor); - var thumbnail = json.GetPropertyOrNull("thumbnail")?.Pipe(ParseEmbedImage); - var image = json.GetPropertyOrNull("image")?.Pipe(ParseEmbedImage); - var footer = json.GetPropertyOrNull("footer")?.Pipe(ParseEmbedFooter); - - var fields = - json.GetPropertyOrNull("fields")?.EnumerateArray().Select(ParseEmbedField).ToArray() ?? - Array.Empty(); - - return new Embed( - title, - url, - timestamp, - color, - author, - description, - fields, - thumbnail, - image, - footer - ); - } - - private Emoji ParseEmoji(JsonElement json) - { - var id = json.GetPropertyOrNull("id")?.GetString(); - var name = json.GetProperty("name").GetString(); - var isAnimated = json.GetPropertyOrNull("animated")?.GetBoolean() ?? false; - - return new Emoji(id, name, isAnimated); - } - - private Reaction ParseReaction(JsonElement json) - { - var count = json.GetProperty("count").GetInt32(); - var emoji = json.GetProperty("emoji").Pipe(ParseEmoji); - - return new Reaction(emoji, count); - } - - private Message ParseMessage(JsonElement json) - { - var id = ParseId(json); - var channelId = json.GetProperty("channel_id").GetString(); - var timestamp = json.GetProperty("timestamp").GetDateTimeOffset(); - var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffset(); - var type = (MessageType) json.GetProperty("type").GetInt32(); - var isPinned = json.GetPropertyOrNull("pinned")?.GetBoolean() ?? false; - - var content = type switch - { - MessageType.RecipientAdd => "Added a recipient.", - MessageType.RecipientRemove => "Removed a recipient.", - MessageType.Call => "Started a call.", - MessageType.ChannelNameChange => "Changed the channel name.", - MessageType.ChannelIconChange => "Changed the channel icon.", - MessageType.ChannelPinnedMessage => "Pinned a message.", - MessageType.GuildMemberJoin => "Joined the server.", - _ => json.GetPropertyOrNull("content")?.GetString() ?? "" - }; - - var author = json.GetProperty("author").Pipe(ParseUser); - - var attachments = - json.GetPropertyOrNull("attachments")?.EnumerateArray().Select(ParseAttachment).ToArray() ?? - Array.Empty(); - - var embeds = - json.GetPropertyOrNull("embeds")?.EnumerateArray().Select(ParseEmbed).ToArray() ?? - Array.Empty(); - - var reactions = - json.GetPropertyOrNull("reactions")?.EnumerateArray().Select(ParseReaction).ToArray() ?? - Array.Empty(); - - var mentionedUsers = - json.GetPropertyOrNull("mentions")?.EnumerateArray().Select(ParseUser).ToArray() ?? - Array.Empty(); - - return new Message( - id, - channelId, - type, - author, - timestamp, - editedTimestamp, - isPinned, - content, - attachments, - embeds, - reactions, - mentionedUsers - ); - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/DiscordClient.cs b/DiscordChatExporter.Domain/Discord/DiscordClient.cs index 9b81c88..0d0405d 100644 --- a/DiscordChatExporter.Domain/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Domain/Discord/DiscordClient.cs @@ -68,7 +68,6 @@ namespace DiscordChatExporter.Domain.Discord return await response.Content.ReadAsJsonAsync(); } - // TODO: do we need this? private async Task TryGetApiResponseAsync(string url) { try @@ -81,36 +80,11 @@ namespace DiscordChatExporter.Domain.Discord } } - public async Task GetGuildAsync(string guildId) - { - // Special case for direct messages pseudo-guild - if (guildId == Guild.DirectMessages.Id) - return Guild.DirectMessages; - - var response = await GetApiResponseAsync($"guilds/{guildId}"); - var guild = ParseGuild(response); - - return guild; - } - - public async Task GetGuildMemberAsync(string guildId, string userId) - { - var response = await TryGetApiResponseAsync($"guilds/{guildId}/members/{userId}"); - return response?.Pipe(ParseMember); - } - - public async Task GetChannelAsync(string channelId) - { - var response = await GetApiResponseAsync($"channels/{channelId}"); - var channel = ParseChannel(response); - - return channel; - } - public async IAsyncEnumerable GetUserGuildsAsync() { - var afterId = ""; + yield return Guild.DirectMessages; + var afterId = ""; while (true) { var url = new UrlBuilder() @@ -122,15 +96,12 @@ namespace DiscordChatExporter.Domain.Discord var response = await GetApiResponseAsync(url); var isEmpty = true; - - // Get full guild object foreach (var guildJson in response.EnumerateArray()) { - var guildId = ParseId(guildJson); - - yield return await GetGuildAsync(guildId); - afterId = guildId; + var guild = Guild.Parse(guildJson); + yield return guild; + afterId = guild.Id; isEmpty = false; } @@ -139,27 +110,93 @@ namespace DiscordChatExporter.Domain.Discord } } - public async Task> GetDirectMessageChannelsAsync() + public async Task GetGuildAsync(string guildId) + { + if (guildId == Guild.DirectMessages.Id) + return Guild.DirectMessages; + + var response = await GetApiResponseAsync($"guilds/{guildId}"); + return Guild.Parse(response); + } + + public async IAsyncEnumerable GetGuildChannelsAsync(string guildId) + { + if (guildId == Guild.DirectMessages.Id) + { + var response = await GetApiResponseAsync("users/@me/channels"); + foreach (var channelJson in response.EnumerateArray()) + yield return Channel.Parse(channelJson); + } + else + { + var response = await GetApiResponseAsync($"guilds/{guildId}/channels"); + + var categories = response + .EnumerateArray() + .ToDictionary( + j => j.GetProperty("id").GetString(), + j => j.GetProperty("name").GetString() + ); + + foreach (var channelJson in response.EnumerateArray()) + { + var parentId = channelJson.GetPropertyOrNull("parent_id")?.GetString(); + var category = !string.IsNullOrWhiteSpace(parentId) + ? categories.GetValueOrDefault(parentId) + : null; + + var channel = Channel.Parse(channelJson, category); + + // Skip non-text channels + if (!channel.IsTextChannel) + continue; + + yield return channel; + } + } + } + + public async IAsyncEnumerable GetGuildRolesAsync(string guildId) { - var response = await GetApiResponseAsync("users/@me/channels"); - var channels = response.EnumerateArray().Select(ParseChannel).ToArray(); + if (guildId == Guild.DirectMessages.Id) + yield break; + + var response = await GetApiResponseAsync($"guilds/{guildId}/roles"); - return channels; + foreach (var roleJson in response.EnumerateArray()) + { + yield return Role.Parse(roleJson); + } } - public async Task> GetGuildChannelsAsync(string guildId) + public async Task TryGetGuildMemberAsync(string guildId, User user) { - // Direct messages pseudo-guild if (guildId == Guild.DirectMessages.Id) - return Array.Empty(); + return Member.CreateForUser(user); + + var response = await TryGetApiResponseAsync($"guilds/{guildId}/members/{user.Id}"); + return response?.Pipe(Member.Parse); + } + + private async Task GetChannelCategoryAsync(string channelParentId) + { + var response = await GetApiResponseAsync($"channels/{channelParentId}"); + return response.GetProperty("name").GetString(); + } + + public async Task GetChannelAsync(string channelId) + { + var response = await GetApiResponseAsync($"channels/{channelId}"); - var response = await GetApiResponseAsync($"guilds/{guildId}/channels"); - var channels = response.EnumerateArray().Select(ParseChannel).ToArray(); + var parentId = response.GetPropertyOrNull("parent_id")?.GetString(); + var category = !string.IsNullOrWhiteSpace(parentId) + ? await GetChannelCategoryAsync(parentId) + : null; - return channels; + return Channel.Parse(response, category); } - private async Task GetLastMessageAsync(string channelId, DateTimeOffset? before = null) + private async Task TryGetLastMessageAsync(string channelId, DateTimeOffset? before = null) { var url = new UrlBuilder() .SetPath($"channels/{channelId}/messages") @@ -168,8 +205,7 @@ namespace DiscordChatExporter.Domain.Discord .Build(); var response = await GetApiResponseAsync(url); - - return response.EnumerateArray().Select(ParseMessage).FirstOrDefault(); + return response.EnumerateArray().Select(Message.Parse).LastOrDefault(); } public async IAsyncEnumerable GetMessagesAsync( @@ -178,9 +214,8 @@ namespace DiscordChatExporter.Domain.Discord DateTimeOffset? before = null, IProgress? progress = null) { - var lastMessage = await GetLastMessageAsync(channelId, before); - - // If the last message doesn't exist or it's outside of range - return + // Get the last message in the specified range + var lastMessage = await TryGetLastMessageAsync(channelId, before); if (lastMessage == null || lastMessage.Timestamp < after) yield break; @@ -199,13 +234,13 @@ namespace DiscordChatExporter.Domain.Discord var messages = response .EnumerateArray() - .Select(ParseMessage) + .Select(Message.Parse) .Reverse() // reverse because messages appear newest first .ToArray(); // Break if there are no messages (can happen if messages are deleted during execution) if (!messages.Any()) - break; + yield break; foreach (var message in messages) { @@ -223,10 +258,6 @@ namespace DiscordChatExporter.Domain.Discord yield return message; afterId = message.Id; - - // Yielded last message - break loop - if (message.Id == lastMessage.Id) - yield break; } } } diff --git a/DiscordChatExporter.Domain/Discord/Models/Attachment.cs b/DiscordChatExporter.Domain/Discord/Models/Attachment.cs index 6efc3e0..f8f4277 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Attachment.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Attachment.cs @@ -1,12 +1,13 @@ using System; using System.IO; using System.Linq; +using System.Text.Json; using DiscordChatExporter.Domain.Discord.Models.Common; +using DiscordChatExporter.Domain.Internal; namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/channel#attachment-object - public partial class Attachment : IHasId { public string Id { get; } @@ -19,9 +20,11 @@ namespace DiscordChatExporter.Domain.Discord.Models public int? Height { get; } - public bool IsImage => ImageFileExtensions.Contains(Path.GetExtension(FileName), StringComparer.OrdinalIgnoreCase); + public bool IsImage => + ImageFileExtensions.Contains(Path.GetExtension(FileName), StringComparer.OrdinalIgnoreCase); - public bool IsSpoiler => IsImage && FileName.StartsWith("SPOILER_", StringComparison.Ordinal); + public bool IsSpoiler => + IsImage && FileName.StartsWith("SPOILER_", StringComparison.Ordinal); public FileSize FileSize { get; } @@ -41,5 +44,17 @@ namespace DiscordChatExporter.Domain.Discord.Models public partial class Attachment { private static readonly string[] ImageFileExtensions = {".jpg", ".jpeg", ".png", ".gif", ".bmp"}; + + public static Attachment Parse(JsonElement json) + { + var id = json.GetProperty("id").GetString(); + var url = json.GetProperty("url").GetString(); + var width = json.GetPropertyOrNull("width")?.GetInt32(); + var height = json.GetPropertyOrNull("height")?.GetInt32(); + var fileName = json.GetProperty("filename").GetString(); + var fileSize = json.GetProperty("size").GetInt64().Pipe(FileSize.FromBytes); + + return new Attachment(id, url, fileName, width, height, fileSize); + } } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/Channel.cs b/DiscordChatExporter.Domain/Discord/Models/Channel.cs index 66a7784..b67f8f4 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Channel.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Channel.cs @@ -1,10 +1,13 @@ -using DiscordChatExporter.Domain.Discord.Models.Common; +using System.Linq; +using System.Text.Json; +using DiscordChatExporter.Domain.Discord.Models.Common; +using DiscordChatExporter.Domain.Internal; +using Tyrrrz.Extensions; namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/channel#channel-object-channel-types // Order of enum fields needs to match the order in the docs. - public enum ChannelType { GuildTextChat, @@ -17,15 +20,10 @@ namespace DiscordChatExporter.Domain.Discord.Models } // https://discordapp.com/developers/docs/resources/channel#channel-object - public partial class Channel : IHasId { public string Id { get; } - public string GuildId { get; } - - public string? ParentId { get; } - public ChannelType Type { get; } public bool IsTextChannel => @@ -35,16 +33,20 @@ namespace DiscordChatExporter.Domain.Discord.Models Type == ChannelType.GuildNews || Type == ChannelType.GuildStore; + public string GuildId { get; } + + public string Category { get; } + public string Name { get; } public string? Topic { get; } - public Channel(string id, string guildId, string? parentId, ChannelType type, string name, string? topic) + public Channel(string id, ChannelType type, string guildId, string category, string name, string? topic) { Id = id; - GuildId = guildId; - ParentId = parentId; Type = type; + GuildId = guildId; + Category = category; Name = name; Topic = topic; } @@ -54,7 +56,36 @@ namespace DiscordChatExporter.Domain.Discord.Models public partial class Channel { - public static Channel CreateDeletedChannel(string id) => - new Channel(id, "unknown-guild", null, ChannelType.GuildTextChat, "deleted-channel", null); + private static string GetDefaultCategory(ChannelType channelType) => channelType switch + { + ChannelType.GuildTextChat => "Text", + ChannelType.DirectTextChat => "Private", + ChannelType.DirectGroupTextChat => "Group", + ChannelType.GuildNews => "News", + ChannelType.GuildStore => "Store", + _ => "Default" + }; + + public static Channel Parse(JsonElement json, string? category = null) + { + var id = json.GetProperty("id").GetString(); + var guildId = json.GetPropertyOrNull("guild_id")?.GetString(); + var topic = json.GetPropertyOrNull("topic")?.GetString(); + + var type = (ChannelType) json.GetProperty("type").GetInt32(); + + var name = + json.GetPropertyOrNull("name")?.GetString() ?? + json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(User.Parse).Select(u => u.Name).JoinToString(", ") ?? + id; + + return new Channel( + id, + type, + guildId ?? Guild.DirectMessages.Id, + category ?? GetDefaultCategory(type), + name, + topic); + } } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/Common/FileSize.cs b/DiscordChatExporter.Domain/Discord/Models/Common/FileSize.cs index a9d7fd3..1850443 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Common/FileSize.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Common/FileSize.cs @@ -3,7 +3,6 @@ namespace DiscordChatExporter.Domain.Discord.Models.Common { // Loosely based on https://github.com/omar/ByteSize (MIT license) - public readonly partial struct FileSize { public long TotalBytes { get; } diff --git a/DiscordChatExporter.Domain/Discord/Models/Embed.cs b/DiscordChatExporter.Domain/Discord/Models/Embed.cs index 2c46064..b75c583 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Embed.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Embed.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.Linq; +using System.Text.Json; +using DiscordChatExporter.Domain.Internal; namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/channel#embed-object - - public class Embed + public partial class Embed { public string? Title { get; } @@ -54,4 +56,38 @@ namespace DiscordChatExporter.Domain.Discord.Models public override string ToString() => Title ?? ""; } + + public partial class Embed + { + public static Embed Parse(JsonElement json) + { + var title = json.GetPropertyOrNull("title")?.GetString(); + var description = json.GetPropertyOrNull("description")?.GetString(); + var url = json.GetPropertyOrNull("url")?.GetString(); + var timestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset(); + var color = json.GetPropertyOrNull("color")?.GetInt32().Pipe(System.Drawing.Color.FromArgb).ResetAlpha(); + + var author = json.GetPropertyOrNull("author")?.Pipe(EmbedAuthor.Parse); + var thumbnail = json.GetPropertyOrNull("thumbnail")?.Pipe(EmbedImage.Parse); + var image = json.GetPropertyOrNull("image")?.Pipe(EmbedImage.Parse); + var footer = json.GetPropertyOrNull("footer")?.Pipe(EmbedFooter.Parse); + + var fields = + json.GetPropertyOrNull("fields")?.EnumerateArray().Select(EmbedField.Parse).ToArray() ?? + Array.Empty(); + + return new Embed( + title, + url, + timestamp, + color, + author, + description, + fields, + thumbnail, + image, + footer + ); + } + } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/EmbedAuthor.cs b/DiscordChatExporter.Domain/Discord/Models/EmbedAuthor.cs index 43b47a9..6597a95 100644 --- a/DiscordChatExporter.Domain/Discord/Models/EmbedAuthor.cs +++ b/DiscordChatExporter.Domain/Discord/Models/EmbedAuthor.cs @@ -1,8 +1,10 @@ +using System.Text.Json; +using DiscordChatExporter.Domain.Internal; + namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/channel#embed-object-embed-author-structure - - public class EmbedAuthor + public partial class EmbedAuthor { public string? Name { get; } @@ -19,4 +21,16 @@ namespace DiscordChatExporter.Domain.Discord.Models public override string ToString() => Name ?? ""; } + + public partial class EmbedAuthor + { + public static EmbedAuthor Parse(JsonElement json) + { + var name = json.GetPropertyOrNull("name")?.GetString(); + var url = json.GetPropertyOrNull("url")?.GetString(); + var iconUrl = json.GetPropertyOrNull("icon_url")?.GetString(); + + return new EmbedAuthor(name, url, iconUrl); + } + } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/EmbedField.cs b/DiscordChatExporter.Domain/Discord/Models/EmbedField.cs index 844fe7d..a72faf7 100644 --- a/DiscordChatExporter.Domain/Discord/Models/EmbedField.cs +++ b/DiscordChatExporter.Domain/Discord/Models/EmbedField.cs @@ -1,8 +1,10 @@ +using System.Text.Json; +using DiscordChatExporter.Domain.Internal; + namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/channel#embed-object-embed-field-structure - - public class EmbedField + public partial class EmbedField { public string Name { get; } @@ -19,4 +21,16 @@ namespace DiscordChatExporter.Domain.Discord.Models public override string ToString() => $"{Name} | {Value}"; } + + public partial class EmbedField + { + public static EmbedField Parse(JsonElement json) + { + var name = json.GetProperty("name").GetString(); + var value = json.GetProperty("value").GetString(); + var isInline = json.GetPropertyOrNull("inline")?.GetBoolean() ?? false; + + return new EmbedField(name, value, isInline); + } + } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/EmbedFooter.cs b/DiscordChatExporter.Domain/Discord/Models/EmbedFooter.cs index 50901da..44022cf 100644 --- a/DiscordChatExporter.Domain/Discord/Models/EmbedFooter.cs +++ b/DiscordChatExporter.Domain/Discord/Models/EmbedFooter.cs @@ -1,8 +1,10 @@ +using System.Text.Json; +using DiscordChatExporter.Domain.Internal; + namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/channel#embed-object-embed-footer-structure - - public class EmbedFooter + public partial class EmbedFooter { public string Text { get; } @@ -16,4 +18,15 @@ namespace DiscordChatExporter.Domain.Discord.Models public override string ToString() => Text; } + + public partial class EmbedFooter + { + public static EmbedFooter Parse(JsonElement json) + { + var text = json.GetProperty("text").GetString(); + var iconUrl = json.GetPropertyOrNull("icon_url")?.GetString(); + + return new EmbedFooter(text, iconUrl); + } + } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/EmbedImage.cs b/DiscordChatExporter.Domain/Discord/Models/EmbedImage.cs index 119eecb..aa8f5d9 100644 --- a/DiscordChatExporter.Domain/Discord/Models/EmbedImage.cs +++ b/DiscordChatExporter.Domain/Discord/Models/EmbedImage.cs @@ -1,8 +1,10 @@ +using System.Text.Json; +using DiscordChatExporter.Domain.Internal; + namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/channel#embed-object-embed-image-structure - - public class EmbedImage + public partial class EmbedImage { public string? Url { get; } @@ -17,4 +19,16 @@ namespace DiscordChatExporter.Domain.Discord.Models Width = width; } } + + public partial class EmbedImage + { + public static EmbedImage Parse(JsonElement json) + { + var url = json.GetPropertyOrNull("url")?.GetString(); + var width = json.GetPropertyOrNull("width")?.GetInt32(); + var height = json.GetPropertyOrNull("height")?.GetInt32(); + + return new EmbedImage(url, width, height); + } + } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/Emoji.cs b/DiscordChatExporter.Domain/Discord/Models/Emoji.cs index 79b15f4..f73513b 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Emoji.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Emoji.cs @@ -1,12 +1,13 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Json; +using DiscordChatExporter.Domain.Internal; using Tyrrrz.Extensions; namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/emoji#emoji-object - public partial class Emoji { public string? Id { get; } @@ -60,12 +61,19 @@ namespace DiscordChatExporter.Domain.Discord.Models return $"https://cdn.discordapp.com/emojis/{id}.png"; } - // Get runes + // Standard emoji var emojiRunes = GetRunes(name).ToArray(); - - // Get corresponding Twemoji image var twemojiName = GetTwemojiName(emojiRunes); return $"https://twemoji.maxcdn.com/2/72x72/{twemojiName}.png"; } + + public static Emoji Parse(JsonElement json) + { + var id = json.GetPropertyOrNull("id")?.GetString(); + var name = json.GetProperty("name").GetString(); + var isAnimated = json.GetPropertyOrNull("animated")?.GetBoolean() ?? false; + + return new Emoji(id, name, isAnimated); + } } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/Guild.cs b/DiscordChatExporter.Domain/Discord/Models/Guild.cs index cba8ea4..091fef5 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Guild.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Guild.cs @@ -1,36 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Text.Json; using DiscordChatExporter.Domain.Discord.Models.Common; -using DiscordChatExporter.Domain.Internal; namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/guild#guild-object - public partial class Guild : IHasId { public string Id { get; } public string Name { get; } - public string? IconHash { get; } - - public string IconUrl => !string.IsNullOrWhiteSpace(IconHash) - ? $"https://cdn.discordapp.com/icons/{Id}/{IconHash}.png" - : "https://cdn.discordapp.com/embed/avatars/0.png"; - - public IReadOnlyList Roles { get; } + public string IconUrl { get; } - public Dictionary Members { get; } - - public Guild(string id, string name, string? iconHash, IReadOnlyList roles) + public Guild(string id, string name, string? iconHash) { Id = id; Name = name; - IconHash = iconHash; - Roles = roles; - Members = new Dictionary(); + + IconUrl = GetIconUrl(id, iconHash); } public override string ToString() => Name; @@ -38,18 +25,21 @@ namespace DiscordChatExporter.Domain.Discord.Models public partial class Guild { - public static string GetUserColor(Guild guild, User user) => - guild.Members.GetValueOrDefault(user.Id, null)? - .RoleIds - .Select(r => guild.Roles.FirstOrDefault(role => r == role.Id)) - .Where(r => r != null) - .Where(r => r.Color != null) - .Aggregate(null, (a, b) => (a?.Position ?? 0) > b.Position ? a : b)? - .Color? - .ToHexString() ?? ""; - - public static string GetUserNick(Guild guild, User user) => guild.Members.GetValueOrDefault(user.Id)?.Nick ?? user.Name; - - public static Guild DirectMessages { get; } = new Guild("@me", "Direct Messages", null, Array.Empty()); + private static string GetIconUrl(string? id, string? iconHash) => + !string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(iconHash) + ? $"https://cdn.discordapp.com/icons/{id}/{iconHash}.png" + : "https://cdn.discordapp.com/embed/avatars/0.png"; + + public static Guild DirectMessages { get; } = + new Guild("@me", "Direct Messages", null); + + public static Guild Parse(JsonElement json) + { + var id = json.GetProperty("id").GetString(); + var name = json.GetProperty("name").GetString(); + var iconHash = json.GetProperty("icon").GetString(); + + return new Guild(id, name, iconHash); + } } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/Member.cs b/DiscordChatExporter.Domain/Discord/Models/Member.cs index 198a437..0b1356e 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Member.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Member.cs @@ -1,22 +1,52 @@ +using System; using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using DiscordChatExporter.Domain.Discord.Models.Common; +using DiscordChatExporter.Domain.Internal; namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/guild#guild-member-object - - public class Member + public partial class Member : IHasId { - public string UserId { get; } + public string Id => User.Id; + + public User User { get; } - public string? Nick { get; } + public string Nick { get; } public IReadOnlyList RoleIds { get; } - public Member(string userId, string? nick, IReadOnlyList roleIds) + public Member(User user, string? nick, IReadOnlyList roleIds) { - UserId = userId; - Nick = nick; + User = user; + Nick = nick ?? user.Name; RoleIds = roleIds; } + + public override string ToString() => Nick; + } + + public partial class Member + { + public static Member CreateForUser(User user) => + new Member(user, null, Array.Empty()); + + public static Member Parse(JsonElement json) + { + var user = json.GetProperty("user").Pipe(User.Parse); + var nick = json.GetPropertyOrNull("nick")?.GetString(); + + var roleIds = + json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString()).ToArray() ?? + Array.Empty(); + + return new Member( + user, + nick, + roleIds + ); + } } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/Message.cs b/DiscordChatExporter.Domain/Discord/Models/Message.cs index 16ad41c..03df1c4 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Message.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Message.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using DiscordChatExporter.Domain.Discord.Models.Common; +using DiscordChatExporter.Domain.Internal; namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/channel#message-object-message-types - public enum MessageType { Default, @@ -20,13 +21,10 @@ namespace DiscordChatExporter.Domain.Discord.Models } // https://discordapp.com/developers/docs/resources/channel#message-object - - public class Message : IHasId + public partial class Message : IHasId { public string Id { get; } - public string ChannelId { get; } - public MessageType Type { get; } public User Author { get; } @@ -49,7 +47,6 @@ namespace DiscordChatExporter.Domain.Discord.Models public Message( string id, - string channelId, MessageType type, User author, DateTimeOffset timestamp, @@ -62,7 +59,6 @@ namespace DiscordChatExporter.Domain.Discord.Models IReadOnlyList mentionedUsers) { Id = id; - ChannelId = channelId; Type = type; Author = author; Timestamp = timestamp; @@ -80,4 +76,59 @@ namespace DiscordChatExporter.Domain.Discord.Models ? "" : ""); } + + public partial class Message + { + public static Message Parse(JsonElement json) + { + var id = json.GetProperty("id").GetString(); + var author = json.GetProperty("author").Pipe(User.Parse); + var timestamp = json.GetProperty("timestamp").GetDateTimeOffset(); + var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffset(); + var type = (MessageType) json.GetProperty("type").GetInt32(); + var isPinned = json.GetPropertyOrNull("pinned")?.GetBoolean() ?? false; + + var content = type switch + { + MessageType.RecipientAdd => "Added a recipient.", + MessageType.RecipientRemove => "Removed a recipient.", + MessageType.Call => "Started a call.", + MessageType.ChannelNameChange => "Changed the channel name.", + MessageType.ChannelIconChange => "Changed the channel icon.", + MessageType.ChannelPinnedMessage => "Pinned a message.", + MessageType.GuildMemberJoin => "Joined the server.", + _ => json.GetPropertyOrNull("content")?.GetString() ?? "" + }; + + var attachments = + json.GetPropertyOrNull("attachments")?.EnumerateArray().Select(Attachment.Parse).ToArray() ?? + Array.Empty(); + + var embeds = + json.GetPropertyOrNull("embeds")?.EnumerateArray().Select(Embed.Parse).ToArray() ?? + Array.Empty(); + + var reactions = + json.GetPropertyOrNull("reactions")?.EnumerateArray().Select(Reaction.Parse).ToArray() ?? + Array.Empty(); + + var mentionedUsers = + json.GetPropertyOrNull("mentions")?.EnumerateArray().Select(User.Parse).ToArray() ?? + Array.Empty(); + + return new Message( + id, + type, + author, + timestamp, + editedTimestamp, + isPinned, + content, + attachments, + embeds, + reactions, + mentionedUsers + ); + } + } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/Reaction.cs b/DiscordChatExporter.Domain/Discord/Models/Reaction.cs index a960008..9f31449 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Reaction.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Reaction.cs @@ -1,8 +1,10 @@ -namespace DiscordChatExporter.Domain.Discord.Models +using System.Text.Json; +using DiscordChatExporter.Domain.Internal; + +namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/channel#reaction-object - - public class Reaction + public partial class Reaction { public Emoji Emoji { get; } @@ -16,4 +18,15 @@ public override string ToString() => $"{Emoji} ({Count})"; } + + public partial class Reaction + { + public static Reaction Parse(JsonElement json) + { + var count = json.GetProperty("count").GetInt32(); + var emoji = json.GetProperty("emoji").Pipe(Emoji.Parse); + + return new Reaction(emoji, count); + } + } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/Role.cs b/DiscordChatExporter.Domain/Discord/Models/Role.cs index 01eb920..12c6b44 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Role.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Role.cs @@ -1,26 +1,26 @@ using System.Drawing; -using DiscordChatExporter.Domain.Discord.Models.Common; +using System.Text.Json; +using DiscordChatExporter.Domain.Internal; namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/topics/permissions#role-object - - public partial class Role : IHasId + public partial class Role { public string Id { get; } public string Name { get; } - public Color? Color { get; } - public int Position { get; } - public Role(string id, string name, Color? color, int position) + public Color? Color { get; } + + public Role(string id, string name, int position, Color? color) { Id = id; Name = name; - Color = color; Position = position; + Color = color; } public override string ToString() => Name; @@ -28,6 +28,19 @@ namespace DiscordChatExporter.Domain.Discord.Models public partial class Role { - public static Role CreateDeletedRole(string id) => new Role(id, "deleted-role", null, -1); + public static Role Parse(JsonElement json) + { + var id = json.GetProperty("id").GetString(); + var name = json.GetProperty("name").GetString(); + var position = json.GetProperty("position").GetInt32(); + + var color = json.GetPropertyOrNull("color")? + .GetInt32() + .Pipe(System.Drawing.Color.FromArgb) + .ResetAlpha() + .NullIf(c => c.ToRgb() <= 0); + + return new Role(id, name, position, color); + } } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/User.cs b/DiscordChatExporter.Domain/Discord/Models/User.cs index 0ff9355..32f2de0 100644 --- a/DiscordChatExporter.Domain/Discord/Models/User.cs +++ b/DiscordChatExporter.Domain/Discord/Models/User.cs @@ -1,33 +1,31 @@ using System; +using System.Text.Json; using DiscordChatExporter.Domain.Discord.Models.Common; +using DiscordChatExporter.Domain.Internal; namespace DiscordChatExporter.Domain.Discord.Models { // https://discordapp.com/developers/docs/resources/user#user-object - public partial class User : IHasId { public string Id { get; } + public bool IsBot { get; } + public int Discriminator { get; } public string Name { get; } public string FullName => $"{Name}#{Discriminator:0000}"; - public string? AvatarHash { get; } - public string AvatarUrl { get; } - public bool IsBot { get; } - - public User(string id, int discriminator, string name, string? avatarHash, bool isBot) + public User(string id, bool isBot, int discriminator, string name, string? avatarHash) { Id = id; + IsBot = isBot; Discriminator = discriminator; Name = name; - AvatarHash = avatarHash; - IsBot = isBot; AvatarUrl = GetAvatarUrl(id, discriminator, avatarHash); } @@ -54,6 +52,15 @@ namespace DiscordChatExporter.Domain.Discord.Models return $"https://cdn.discordapp.com/embed/avatars/{discriminator % 5}.png"; } - public static User CreateUnknownUser(string id) => new User(id, 0, "Unknown", null, false); + public static User Parse(JsonElement json) + { + var id = json.GetProperty("id").GetString(); + var discriminator = json.GetProperty("discriminator").GetString().Pipe(int.Parse); + var name = json.GetProperty("username").GetString(); + var avatarHash = json.GetProperty("avatar").GetString(); + var isBot = json.GetPropertyOrNull("bot")?.GetBoolean() ?? false; + + return new User(id, isBot, discriminator, name, avatarHash); + } } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/UrlBuilder.cs b/DiscordChatExporter.Domain/Discord/UrlBuilder.cs index 1b1b6e8..de701c7 100644 --- a/DiscordChatExporter.Domain/Discord/UrlBuilder.cs +++ b/DiscordChatExporter.Domain/Discord/UrlBuilder.cs @@ -40,9 +40,7 @@ namespace DiscordChatExporter.Domain.Discord buffer.Append(_path); if (_queryParameters.Any()) - buffer.Append('?'); - - buffer.AppendJoin('&', _queryParameters.Select(kvp => $"{kvp.Key}={kvp.Value}")); + buffer.Append('?').AppendJoin('&', _queryParameters.Select(kvp => $"{kvp.Key}={kvp.Value}")); return buffer.ToString(); } diff --git a/DiscordChatExporter.Domain/Exporting/ChannelExporter.cs b/DiscordChatExporter.Domain/Exporting/ChannelExporter.cs index c528bd7..6e2cbb2 100644 --- a/DiscordChatExporter.Domain/Exporting/ChannelExporter.cs +++ b/DiscordChatExporter.Domain/Exporting/ChannelExporter.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; using DiscordChatExporter.Domain.Discord; using DiscordChatExporter.Domain.Discord.Models; using DiscordChatExporter.Domain.Discord.Models.Common; using DiscordChatExporter.Domain.Exceptions; -using Tyrrrz.Extensions; +using DiscordChatExporter.Domain.Utilities; namespace DiscordChatExporter.Domain.Exporting { @@ -36,37 +37,35 @@ namespace DiscordChatExporter.Domain.Exporting var options = new ExportOptions(baseFilePath, format, partitionLimit); // Context - var mentionableUsers = new HashSet(IdBasedEqualityComparer.Instance); - var mentionableChannels = await _discord.GetGuildChannelsAsync(guild.Id); - var mentionableRoles = guild.Roles; + var contextMembers = new HashSet(IdBasedEqualityComparer.Instance); + var contextChannels = await _discord.GetGuildChannelsAsync(guild.Id); + var contextRoles = await _discord.GetGuildRolesAsync(guild.Id); var context = new ExportContext( guild, channel, after, before, dateFormat, - mentionableUsers, mentionableChannels, mentionableRoles + contextMembers, contextChannels, contextRoles ); await using var messageExporter = new MessageExporter(options, context); var exportedAnything = false; + var encounteredUsers = new HashSet(IdBasedEqualityComparer.Instance); await foreach (var message in _discord.GetMessagesAsync(channel.Id, after, before, progress)) { - // Add encountered users to the list of mentionable users - var encounteredUsers = new List(); - encounteredUsers.Add(message.Author); - encounteredUsers.AddRange(message.MentionedUsers); - - mentionableUsers.AddRange(encounteredUsers); - - foreach (User u in encounteredUsers) + // Resolve members for referenced users + foreach (var referencedUser in message.MentionedUsers.Prepend(message.Author)) { - if (!guild.Members.ContainsKey(u.Id)) + if (encounteredUsers.Add(referencedUser)) { - var member = await _discord.GetGuildMemberAsync(guild.Id, u.Id); - guild.Members[u.Id] = member; + var member = + await _discord.TryGetGuildMemberAsync(guild.Id, referencedUser) ?? + Member.CreateForUser(referencedUser); + + contextMembers.Add(member); } } - // Render message + // Export message await messageExporter.ExportMessageAsync(message); exportedAnything = true; } diff --git a/DiscordChatExporter.Domain/Exporting/ExportContext.cs b/DiscordChatExporter.Domain/Exporting/ExportContext.cs index 2361346..d59ac52 100644 --- a/DiscordChatExporter.Domain/Exporting/ExportContext.cs +++ b/DiscordChatExporter.Domain/Exporting/ExportContext.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Drawing; +using System.Linq; using DiscordChatExporter.Domain.Discord.Models; namespace DiscordChatExporter.Domain.Exporting @@ -16,11 +18,11 @@ namespace DiscordChatExporter.Domain.Exporting public string DateFormat { get; } - public IReadOnlyCollection MentionableUsers { get; } + public IReadOnlyCollection Members { get; } - public IReadOnlyCollection MentionableChannels { get; } + public IReadOnlyCollection Channels { get; } - public IReadOnlyCollection MentionableRoles { get; } + public IReadOnlyCollection Roles { get; } public ExportContext( Guild guild, @@ -28,19 +30,41 @@ namespace DiscordChatExporter.Domain.Exporting DateTimeOffset? after, DateTimeOffset? before, string dateFormat, - IReadOnlyCollection mentionableUsers, - IReadOnlyCollection mentionableChannels, - IReadOnlyCollection mentionableRoles) - + IReadOnlyCollection members, + IReadOnlyCollection channels, + IReadOnlyCollection roles) { Guild = guild; Channel = channel; After = after; Before = before; DateFormat = dateFormat; - MentionableUsers = mentionableUsers; - MentionableChannels = mentionableChannels; - MentionableRoles = mentionableRoles; + Members = members; + Channels = channels; + Roles = roles; + } + + public Member? TryGetMentionedMember(string id) => + Members.FirstOrDefault(m => m.Id == id); + + public Channel? TryGetMentionedChannel(string id) => + Channels.FirstOrDefault(c => c.Id == id); + + public Role? TryGetMentionedRole(string id) => + Roles.FirstOrDefault(r => r.Id == id); + + public Member? TryGetUserMember(User user) => Members + .FirstOrDefault(m => m.Id == user.Id); + + public Color? TryGetUserColor(User user) + { + var member = TryGetUserMember(user); + var roles = member?.RoleIds.Join(Roles, i => i, r => r.Id, (_, role) => role); + + return roles? + .OrderByDescending(r => r.Position) + .Select(r => r.Color) + .FirstOrDefault(c => c != null); } } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Exporting/Resources/HtmlCore.css b/DiscordChatExporter.Domain/Exporting/Resources/HtmlCore.css index f48a208..0428cba 100644 --- a/DiscordChatExporter.Domain/Exporting/Resources/HtmlCore.css +++ b/DiscordChatExporter.Domain/Exporting/Resources/HtmlCore.css @@ -48,7 +48,7 @@ img { } .markdown { - display: inline-block; + max-width: 100%; white-space: pre-wrap; line-height: 1.3; overflow-wrap: break-word; diff --git a/DiscordChatExporter.Domain/Exporting/Resources/HtmlLayoutTemplate.html b/DiscordChatExporter.Domain/Exporting/Resources/HtmlLayoutTemplate.html index c933157..ff20806 100644 --- a/DiscordChatExporter.Domain/Exporting/Resources/HtmlLayoutTemplate.html +++ b/DiscordChatExporter.Domain/Exporting/Resources/HtmlLayoutTemplate.html @@ -62,22 +62,22 @@
{{ Context.Guild.Name | html.escape }}
-
{{ Context.Channel.Name | html.escape }}
+
{{ Context.Channel.Category | html.escape }} / {{ Context.Channel.Name | html.escape }}
{{~ if Context.Channel.Topic ~}} -
{{ Context.Channel.Topic | html.escape }}
+
{{ Context.Channel.Topic | html.escape }}
{{~ end ~}} {{~ if Context.After || Context.Before ~}} -
- {{~ if Context.After && Context.Before ~}} - Between {{ Context.After | FormatDate | html.escape }} and {{ Context.Before | FormatDate | html.escape }} - {{~ else if Context.After ~}} - After {{ Context.After | FormatDate | html.escape }} - {{~ else if Context.Before ~}} - Before {{ Context.Before | FormatDate | html.escape }} - {{~ end ~}} -
+
+ {{~ if Context.After && Context.Before ~}} + Between {{ Context.After | FormatDate | html.escape }} and {{ Context.Before | FormatDate | html.escape }} + {{~ else if Context.After ~}} + After {{ Context.After | FormatDate | html.escape }} + {{~ else if Context.Before ~}} + Before {{ Context.Before | FormatDate | html.escape }} + {{~ end ~}} +
{{~ end ~}}
diff --git a/DiscordChatExporter.Domain/Exporting/Resources/HtmlMessageGroupTemplate.html b/DiscordChatExporter.Domain/Exporting/Resources/HtmlMessageGroupTemplate.html index 7e539c1..0550f5b 100644 --- a/DiscordChatExporter.Domain/Exporting/Resources/HtmlMessageGroupTemplate.html +++ b/DiscordChatExporter.Domain/Exporting/Resources/HtmlMessageGroupTemplate.html @@ -5,27 +5,30 @@
{{~ # Author name and timestamp ~}} - {{ GetUserNick Context.Guild MessageGroup.Author | html.escape }} + {{~ userColor = TryGetUserColor MessageGroup.Author | FormatColorRgb ~}} + {{ (TryGetUserNick MessageGroup.Author ?? MessageGroup.Author.Name) | html.escape }} {{~ # Bot tag ~}} {{~ if MessageGroup.Author.IsBot ~}} - BOT + BOT {{~ end ~}} {{ MessageGroup.Timestamp | FormatDate | html.escape }} {{~ # Messages ~}} {{~ for message in MessageGroup.Messages ~}} -
+
{{~ # Content ~}} {{~ if message.Content ~}}
-
{{ message.Content | FormatMarkdown }}
+
+ {{- message.Content | FormatMarkdown -}} - {{~ # Edited timestamp ~}} - {{~ if message.EditedTimestamp ~}} - (edited) - {{~ end ~}} + {{- # Edited timestamp -}} + {{- if message.EditedTimestamp -}} + {{-}}(edited){{-}} + {{- end -}} +
{{~ end ~}} @@ -89,16 +92,16 @@ {{~ if embed.Title ~}}
{{~ if embed.Url ~}} - {{ embed.Title | FormatMarkdown }} +
{{ embed.Title | FormatEmbedMarkdown }}
{{~ else ~}} - {{ embed.Title | FormatMarkdown }} +
{{ embed.Title | FormatEmbedMarkdown }}
{{~ end ~}}
{{~ end ~}} {{~ # Description ~}} {{~ if embed.Description ~}} -
{{ embed.Description | FormatMarkdown }}
+
{{ embed.Description | FormatEmbedMarkdown }}
{{~ end ~}} {{~ # Fields ~}} @@ -107,10 +110,10 @@ {{~ for field in embed.Fields ~}}
{{~ if field.Name ~}} -
{{ field.Name | FormatMarkdown }}
+
{{ field.Name | FormatEmbedMarkdown }}
{{~ end ~}} {{~ if field.Value ~}} -
{{ field.Value | FormatMarkdown }}
+
{{ field.Value | FormatEmbedMarkdown }}
{{~ end ~}}
{{~ end ~}} diff --git a/DiscordChatExporter.Domain/Exporting/Writers/HtmlMessageWriter.cs b/DiscordChatExporter.Domain/Exporting/Writers/HtmlMessageWriter.cs index 79f170d..c7d870c 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/HtmlMessageWriter.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/HtmlMessageWriter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.IO; using System.Linq; using System.Reflection; @@ -73,12 +74,20 @@ namespace DiscordChatExporter.Domain.Exporting.Writers scriptObject.Import("FormatDate", new Func(d => d.ToLocalString(Context.DateFormat))); - scriptObject.Import("FormatMarkdown", - new Func(FormatMarkdown)); + scriptObject.Import("FormatColorRgb", + new Func(c => c != null ? $"rgb({c?.R}, {c?.G}, {c?.B})" : null)); + + scriptObject.Import("TryGetUserColor", + new Func(Context.TryGetUserColor)); - scriptObject.Import("GetUserColor", new Func(Guild.GetUserColor)); + scriptObject.Import("TryGetUserNick", + new Func(u => Context.TryGetUserMember(u)?.Nick)); + + scriptObject.Import("FormatMarkdown", + new Func(m => FormatMarkdown(m))); - scriptObject.Import("GetUserNick", new Func(Guild.GetUserNick)); + scriptObject.Import("FormatEmbedMarkdown", + new Func(m => FormatMarkdown(m, false))); // Push model templateContext.PushGlobal(scriptObject); @@ -89,8 +98,8 @@ namespace DiscordChatExporter.Domain.Exporting.Writers return templateContext; } - private string FormatMarkdown(string? markdown) => - HtmlMarkdownVisitor.Format(Context, markdown ?? ""); + private string FormatMarkdown(string? markdown, bool isJumboAllowed = true) => + HtmlMarkdownVisitor.Format(Context, markdown ?? "", isJumboAllowed); private async Task RenderCurrentMessageGroupAsync() { diff --git a/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs b/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs index ceb0fcd..41c4346 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs @@ -84,8 +84,8 @@ namespace DiscordChatExporter.Domain.Exporting.Writers { _writer.WriteStartObject(); - _writer.WriteString("name", embedField.Name); - _writer.WriteString("value", embedField.Value); + _writer.WriteString("name", FormatMarkdown(embedField.Name)); + _writer.WriteString("value", FormatMarkdown(embedField.Value)); _writer.WriteBoolean("isInline", embedField.IsInline); _writer.WriteEndObject(); @@ -156,6 +156,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers _writer.WriteStartObject("channel"); _writer.WriteString("id", Context.Channel.Id); _writer.WriteString("type", Context.Channel.Type.ToString()); + _writer.WriteString("category", Context.Channel.Category); _writer.WriteString("name", Context.Channel.Name); _writer.WriteString("topic", Context.Channel.Topic); _writer.WriteEndObject(); diff --git a/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs b/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs index 7a9e6fa..5b68304 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs @@ -85,38 +85,38 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors } else if (mention.Type == MentionType.User) { - var user = _context.MentionableUsers.FirstOrDefault(u => u.Id == mention.Id) ?? - User.CreateUnknownUser(mention.Id); - - var nick = Guild.GetUserNick(_context.Guild, user); + var member = _context.TryGetMentionedMember(mention.Id); + var fullName = member?.User.FullName ?? "Unknown"; + var nick = member?.Nick ?? "Unknown"; _buffer - .Append($"") + .Append($"") .Append("@").Append(HtmlEncode(nick)) .Append(""); } else if (mention.Type == MentionType.Channel) { - var channel = _context.MentionableChannels.FirstOrDefault(c => c.Id == mention.Id) ?? - Channel.CreateDeletedChannel(mention.Id); + var channel = _context.TryGetMentionedChannel(mention.Id); + var name = channel?.Name ?? "deleted-channel"; _buffer .Append("") - .Append("#").Append(HtmlEncode(channel.Name)) + .Append("#").Append(HtmlEncode(name)) .Append(""); } else if (mention.Type == MentionType.Role) { - var role = _context.MentionableRoles.FirstOrDefault(r => r.Id == mention.Id) ?? - Role.CreateDeletedRole(mention.Id); + var role = _context.TryGetMentionedRole(mention.Id); + var name = role?.Name ?? "deleted-role"; + var color = role?.Color; - var style = role.Color != null - ? $"color: {role.Color.Value.ToHexString()}; background-color: rgba({role.Color.Value.ToRgbString()}, 0.1);" + var style = color != null + ? $"color: rgb({color?.R}, {color?.G}, {color?.B}); background-color: rgba({color?.R}, {color?.G}, {color?.B}, 0.1);" : ""; _buffer .Append($"") - .Append("@").Append(HtmlEncode(role.Name)) + .Append("@").Append(HtmlEncode(name)) .Append(""); } @@ -162,10 +162,13 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors { private static string HtmlEncode(string text) => WebUtility.HtmlEncode(text); - public static string Format(ExportContext context, string markdown) + public static string Format(ExportContext context, string markdown, bool isJumboAllowed = true) { var nodes = MarkdownParser.Parse(markdown); - var isJumbo = nodes.All(n => n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text)); + + var isJumbo = + isJumboAllowed && + nodes.All(n => n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text)); var buffer = new StringBuilder(); diff --git a/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs b/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs index 0a88b97..747e93d 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs @@ -1,6 +1,4 @@ -using System.Linq; -using System.Text; -using DiscordChatExporter.Domain.Discord.Models; +using System.Text; using DiscordChatExporter.Domain.Markdown; using DiscordChatExporter.Domain.Markdown.Ast; @@ -27,24 +25,24 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors { if (mention.Type == MentionType.User) { - var user = _context.MentionableUsers.FirstOrDefault(u => u.Id == mention.Id) ?? - User.CreateUnknownUser(mention.Id); + var member = _context.TryGetMentionedMember(mention.Id); + var name = member?.User.Name ?? "Unknown"; - _buffer.Append($"@{user.Name}"); + _buffer.Append($"@{name}"); } else if (mention.Type == MentionType.Channel) { - var channel = _context.MentionableChannels.FirstOrDefault(c => c.Id == mention.Id) ?? - Channel.CreateDeletedChannel(mention.Id); + var channel = _context.TryGetMentionedChannel(mention.Id); + var name = channel?.Name ?? "deleted-channel"; - _buffer.Append($"#{channel.Name}"); + _buffer.Append($"#{name}"); } else if (mention.Type == MentionType.Role) { - var role = _context.MentionableRoles.FirstOrDefault(r => r.Id == mention.Id) ?? - Role.CreateDeletedRole(mention.Id); + var role = _context.TryGetMentionedRole(mention.Id); + var name = role?.Name ?? "deleted-role"; - _buffer.Append($"@{role.Name}"); + _buffer.Append($"@{name}"); } return base.VisitMention(mention); diff --git a/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs b/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs index 1f0e84f..fc132a3 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs @@ -76,8 +76,8 @@ namespace DiscordChatExporter.Domain.Exporting.Writers foreach (var field in embed.Fields) { buffer - .AppendLineIfNotNullOrWhiteSpace(field.Name) - .AppendLineIfNotNullOrWhiteSpace(field.Value); + .AppendLineIfNotNullOrWhiteSpace(FormatMarkdown(field.Name)) + .AppendLineIfNotNullOrWhiteSpace(FormatMarkdown(field.Value)); } buffer @@ -135,7 +135,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers buffer.Append('=', 62).AppendLine(); buffer.AppendLine($"Guild: {Context.Guild.Name}"); - buffer.AppendLine($"Channel: {Context.Channel.Name}"); + buffer.AppendLine($"Channel: {Context.Channel.Category} / {Context.Channel.Name}"); if (!string.IsNullOrWhiteSpace(Context.Channel.Topic)) buffer.AppendLine($"Topic: {Context.Channel.Topic}"); diff --git a/DiscordChatExporter.Domain/Internal/ColorExtensions.cs b/DiscordChatExporter.Domain/Internal/ColorExtensions.cs index 8f3a97f..68a8871 100644 --- a/DiscordChatExporter.Domain/Internal/ColorExtensions.cs +++ b/DiscordChatExporter.Domain/Internal/ColorExtensions.cs @@ -4,12 +4,10 @@ namespace DiscordChatExporter.Domain.Internal { internal static class ColorExtensions { - public static Color ResetAlpha(this Color color) => Color.FromArgb(1, color); + public static Color WithAlpha(this Color color, int alpha) => Color.FromArgb(alpha, color); - public static int ToRgb(this Color color) => color.ToArgb() & 0xffffff; - - public static string ToHexString(this Color color) => $"#{color.ToRgb():x6}"; + public static Color ResetAlpha(this Color color) => color.WithAlpha(255); - public static string ToRgbString(this Color color) => $"{color.R}, {color.G}, {color.B}"; + public static int ToRgb(this Color color) => color.ToArgb() & 0xffffff; } } \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Behaviors/ChannelMultiSelectionListBoxBehavior.cs b/DiscordChatExporter.Gui/Behaviors/ChannelMultiSelectionListBoxBehavior.cs new file mode 100644 index 0000000..a9de60d --- /dev/null +++ b/DiscordChatExporter.Gui/Behaviors/ChannelMultiSelectionListBoxBehavior.cs @@ -0,0 +1,8 @@ +using DiscordChatExporter.Domain.Discord.Models; + +namespace DiscordChatExporter.Gui.Behaviors +{ + public class ChannelMultiSelectionListBoxBehavior : MultiSelectionListBoxBehavior + { + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Behaviors/ChannelViewModelMultiSelectionListBoxBehavior.cs b/DiscordChatExporter.Gui/Behaviors/ChannelViewModelMultiSelectionListBoxBehavior.cs deleted file mode 100644 index 37a0836..0000000 --- a/DiscordChatExporter.Gui/Behaviors/ChannelViewModelMultiSelectionListBoxBehavior.cs +++ /dev/null @@ -1,8 +0,0 @@ -using DiscordChatExporter.Gui.ViewModels.Components; - -namespace DiscordChatExporter.Gui.Behaviors -{ - public class ChannelViewModelMultiSelectionListBoxBehavior : MultiSelectionListBoxBehavior - { - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/Components/ChannelViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Components/ChannelViewModel.cs deleted file mode 100644 index cfeecce..0000000 --- a/DiscordChatExporter.Gui/ViewModels/Components/ChannelViewModel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using DiscordChatExporter.Domain.Discord.Models; -using Stylet; - -namespace DiscordChatExporter.Gui.ViewModels.Components -{ - public partial class ChannelViewModel : PropertyChangedBase - { - public Channel? Model { get; set; } - - public string? Category { get; set; } - } - - public partial class ChannelViewModel - { - public static implicit operator Channel?(ChannelViewModel? viewModel) => viewModel?.Model; - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/Components/GuildViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Components/GuildViewModel.cs deleted file mode 100644 index 9d65c3c..0000000 --- a/DiscordChatExporter.Gui/ViewModels/Components/GuildViewModel.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using DiscordChatExporter.Domain.Discord.Models; -using Stylet; - -namespace DiscordChatExporter.Gui.ViewModels.Components -{ - public partial class GuildViewModel : PropertyChangedBase - { - public Guild? Model { get; set; } - - public IReadOnlyList? Channels { get; set; } - } - - public partial class GuildViewModel - { - public static implicit operator Guild?(GuildViewModel? viewModel) => viewModel?.Model; - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs index f843ad0..9f9f6ae 100644 --- a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using DiscordChatExporter.Domain.Discord.Models; using DiscordChatExporter.Domain.Exporting; using DiscordChatExporter.Gui.Services; -using DiscordChatExporter.Gui.ViewModels.Components; using DiscordChatExporter.Gui.ViewModels.Framework; namespace DiscordChatExporter.Gui.ViewModels.Dialogs @@ -13,9 +13,9 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs private readonly DialogManager _dialogManager; private readonly SettingsService _settingsService; - public GuildViewModel? Guild { get; set; } + public Guild? Guild { get; set; } - public IReadOnlyList? Channels { get; set; } + public IReadOnlyList? Channels { get; set; } public bool IsSingleChannel => Channels == null || Channels.Count == 1; @@ -61,7 +61,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs var channel = Channels.Single(); // Generate default file name - var defaultFileName = ChannelExporter.GetDefaultExportFileName(Guild!, channel!, SelectedFormat, After, Before); + var defaultFileName = ChannelExporter.GetDefaultExportFileName(Guild!, channel, SelectedFormat, After, Before); // Generate filter var ext = SelectedFormat.GetFileExtension(); diff --git a/DiscordChatExporter.Gui/ViewModels/Framework/Extensions.cs b/DiscordChatExporter.Gui/ViewModels/Framework/Extensions.cs index 84f32f6..5584e8d 100644 --- a/DiscordChatExporter.Gui/ViewModels/Framework/Extensions.cs +++ b/DiscordChatExporter.Gui/ViewModels/Framework/Extensions.cs @@ -1,33 +1,13 @@ using System.Collections.Generic; using DiscordChatExporter.Domain.Discord.Models; -using DiscordChatExporter.Gui.ViewModels.Components; using DiscordChatExporter.Gui.ViewModels.Dialogs; namespace DiscordChatExporter.Gui.ViewModels.Framework { public static class Extensions { - public static ChannelViewModel CreateChannelViewModel(this IViewModelFactory factory, Channel model, string? category = null) - { - var viewModel = factory.CreateChannelViewModel(); - viewModel.Model = model; - viewModel.Category = category; - - return viewModel; - } - - public static GuildViewModel CreateGuildViewModel(this IViewModelFactory factory, Guild model, - IReadOnlyList channels) - { - var viewModel = factory.CreateGuildViewModel(); - viewModel.Model = model; - viewModel.Channels = channels; - - return viewModel; - } - public static ExportSetupViewModel CreateExportSetupViewModel(this IViewModelFactory factory, - GuildViewModel guild, IReadOnlyList channels) + Guild guild, IReadOnlyList channels) { var viewModel = factory.CreateExportSetupViewModel(); viewModel.Guild = guild; diff --git a/DiscordChatExporter.Gui/ViewModels/Framework/IViewModelFactory.cs b/DiscordChatExporter.Gui/ViewModels/Framework/IViewModelFactory.cs index f02c801..e741b6d 100644 --- a/DiscordChatExporter.Gui/ViewModels/Framework/IViewModelFactory.cs +++ b/DiscordChatExporter.Gui/ViewModels/Framework/IViewModelFactory.cs @@ -1,15 +1,10 @@ -using DiscordChatExporter.Gui.ViewModels.Components; -using DiscordChatExporter.Gui.ViewModels.Dialogs; +using DiscordChatExporter.Gui.ViewModels.Dialogs; namespace DiscordChatExporter.Gui.ViewModels.Framework { // Used to instantiate new view models while making use of dependency injection public interface IViewModelFactory { - ChannelViewModel CreateChannelViewModel(); - - GuildViewModel CreateGuildViewModel(); - ExportSetupViewModel CreateExportSetupViewModel(); SettingsViewModel CreateSettingsViewModel(); diff --git a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs index 10eb231..db7fc75 100644 --- a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs @@ -9,7 +9,6 @@ using DiscordChatExporter.Domain.Exceptions; using DiscordChatExporter.Domain.Exporting; using DiscordChatExporter.Domain.Utilities; using DiscordChatExporter.Gui.Services; -using DiscordChatExporter.Gui.ViewModels.Components; using DiscordChatExporter.Gui.ViewModels.Framework; using Gress; using MaterialDesignThemes.Wpf; @@ -37,11 +36,17 @@ namespace DiscordChatExporter.Gui.ViewModels public string? TokenValue { get; set; } - public IReadOnlyList? AvailableGuilds { get; private set; } + private IReadOnlyDictionary>? GuildChannelMap { get; set; } - public GuildViewModel? SelectedGuild { get; set; } + public IReadOnlyList? AvailableGuilds => GuildChannelMap?.Keys.ToArray(); - public IReadOnlyList? SelectedChannels { get; set; } + public Guild? SelectedGuild { get; set; } + + public IReadOnlyList? AvailableChannels => SelectedGuild != null + ? GuildChannelMap?[SelectedGuild] + : null; + + public IReadOnlyList? SelectedChannels { get; set; } public RootViewModel( IViewModelFactory viewModelFactory, @@ -142,71 +147,18 @@ namespace DiscordChatExporter.Gui.ViewModels var discord = new DiscordClient(token); - var availableGuilds = new List(); - - // Direct messages - { - var guild = Guild.DirectMessages; - var channels = await discord.GetDirectMessageChannelsAsync(); - - // Create channel view models - var channelViewModels = new List(); - foreach (var channel in channels) - { - // Get fake category - var category = channel.Type == ChannelType.DirectTextChat ? "Private" : "Group"; - - // Create channel view model - var channelViewModel = _viewModelFactory.CreateChannelViewModel(channel, category); - - // Add to list - channelViewModels.Add(channelViewModel); - } - - // Create guild view model - var guildViewModel = _viewModelFactory.CreateGuildViewModel(guild, - channelViewModels.OrderBy(c => c.Category) - .ThenBy(c => c.Model!.Name) - .ToArray()); - - // Add to list - availableGuilds.Add(guildViewModel); - } - - // Guilds - var guilds = await discord.GetUserGuildsAsync(); - foreach (var guild in guilds) + var guildChannelMap = new Dictionary>(); + await foreach (var guild in discord.GetUserGuildsAsync()) { var channels = await discord.GetGuildChannelsAsync(guild.Id); - var categoryChannels = channels.Where(c => c.Type == ChannelType.GuildCategory).ToArray(); - var exportableChannels = channels.Where(c => c.IsTextChannel).ToArray(); - - // Create channel view models - var channelViewModels = new List(); - foreach (var channel in exportableChannels) - { - // Get category - var category = categoryChannels.FirstOrDefault(c => c.Id == channel.ParentId)?.Name; - - // Create channel view model - var channelViewModel = _viewModelFactory.CreateChannelViewModel(channel, category); - - // Add to list - channelViewModels.Add(channelViewModel); - } - - // Create guild view model - var guildViewModel = _viewModelFactory.CreateGuildViewModel(guild, - channelViewModels.OrderBy(c => c.Category) - .ThenBy(c => c.Model!.Name) - .ToArray()); - - // Add to list - availableGuilds.Add(guildViewModel); + guildChannelMap[guild] = channels + .OrderBy(c => c.Category) + .ThenBy(c => c.Name) + .ToArray(); } - AvailableGuilds = availableGuilds; - SelectedGuild = AvailableGuilds.FirstOrDefault(); + GuildChannelMap = guildChannelMap; + SelectedGuild = guildChannelMap.Keys.FirstOrDefault(); } catch (DiscordChatExporterException ex) when (!ex.IsCritical) { diff --git a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml index 57a8df7..8a4d338 100644 --- a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml +++ b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml @@ -26,7 +26,7 @@ Width="32" Height="32"> - + @@ -54,8 +54,8 @@ + Text="{Binding Channels[0].Name, Mode=OneWay}" + ToolTip="{Binding Channels[0].Name, Mode=OneWay}" /> diff --git a/DiscordChatExporter.Gui/Views/RootView.xaml b/DiscordChatExporter.Gui/Views/RootView.xaml index eddfffe..16014c2 100644 --- a/DiscordChatExporter.Gui/Views/RootView.xaml +++ b/DiscordChatExporter.Gui/Views/RootView.xaml @@ -226,7 +226,7 @@ Margin="-8" Background="Transparent" Cursor="Hand" - ToolTip="{Binding Model.Name}"> + ToolTip="{Binding Name}"> - + @@ -253,11 +253,11 @@ - + @@ -286,7 +286,7 @@ FontSize="14"> - +