diff --git a/.gitignore b/.gitignore index dab02c4..f874e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.userosscache *.sln.docstates .idea/ +.vs/ # Build results [Dd]ebug/ diff --git a/DiscordChatExporter.Domain/Discord/DiscordClient.cs b/DiscordChatExporter.Domain/Discord/DiscordClient.cs index 7efebbe..1cd1e08 100644 --- a/DiscordChatExporter.Domain/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Domain/Discord/DiscordClient.cs @@ -117,26 +117,33 @@ namespace DiscordChatExporter.Domain.Discord { var response = await GetJsonResponseAsync($"guilds/{guildId}/channels"); - var categories = response + var orderedResponse = response .EnumerateArray() - .ToDictionary( - j => j.GetProperty("id").GetString(), - j => j.GetProperty("name").GetString() - ); + .OrderBy(j => j.GetProperty("position").GetInt32()) + .ThenBy(j => ulong.Parse(j.GetProperty("id").GetString())); - foreach (var channelJson in response.EnumerateArray()) + var categories = orderedResponse + .Where(j => j.GetProperty("type").GetInt32() == (int)ChannelType.GuildCategory) + .Select((j, index) => ChannelCategory.Parse(j, index + 1)) + .ToDictionary(j => j.Id.ToString()); + + var position = 0; + + foreach (var channelJson in orderedResponse) { var parentId = channelJson.GetPropertyOrNull("parent_id")?.GetString(); var category = !string.IsNullOrWhiteSpace(parentId) ? categories.GetValueOrDefault(parentId) : null; - - var channel = Channel.Parse(channelJson, category); + + var channel = Channel.Parse(channelJson, category, position); // Skip non-text channels if (!channel.IsTextChannel) continue; + position++; + yield return channel; } } @@ -162,10 +169,11 @@ namespace DiscordChatExporter.Domain.Discord return response?.Pipe(Member.Parse); } - private async ValueTask GetChannelCategoryAsync(Snowflake channelParentId) + public async ValueTask GetChannelCategoryAsync(Snowflake channelId) { - var response = await GetJsonResponseAsync($"channels/{channelParentId}"); - return response.GetProperty("name").GetString(); + var response = await GetJsonResponseAsync($"channels/{channelId}"); + + return ChannelCategory.Parse(response); } public async ValueTask GetChannelAsync(Snowflake channelId) diff --git a/DiscordChatExporter.Domain/Discord/Models/Channel.cs b/DiscordChatExporter.Domain/Discord/Models/Channel.cs index 5555409..0baf04e 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Channel.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Channel.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Text.Json; using DiscordChatExporter.Domain.Discord.Models.Common; using DiscordChatExporter.Domain.Utilities; @@ -36,56 +37,67 @@ namespace DiscordChatExporter.Domain.Discord.Models public Snowflake GuildId { get; } - public string Category { get; } + public ChannelCategory Category { get; } public string Name { get; } + public int Position { get; } + public string? Topic { get; } - public Channel(Snowflake id, ChannelType type, Snowflake guildId, string category, string name, string? topic) + public Channel(Snowflake id, ChannelType type, Snowflake guildId, ChannelCategory? category, string name, int position, string? topic) { Id = id; Type = type; GuildId = guildId; - Category = category; + Category = category ?? GetDefaultCategory(type); Name = name; + Position = position; Topic = topic; } public override string ToString() => Name; + } public partial class Channel { - 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) + private static ChannelCategory GetDefaultCategory(ChannelType channelType) => new( + Snowflake.Zero, + channelType switch + { + ChannelType.GuildTextChat => "Text", + ChannelType.DirectTextChat => "Private", + ChannelType.DirectGroupTextChat => "Group", + ChannelType.GuildNews => "News", + ChannelType.GuildStore => "Store", + _ => "Default" + }, + 0 + ); + + public static Channel Parse(JsonElement json, ChannelCategory? category = null, int? position = null) { var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse); var guildId = json.GetPropertyOrNull("guild_id")?.GetString().Pipe(Snowflake.Parse); var topic = json.GetPropertyOrNull("topic")?.GetString(); - var type = (ChannelType) json.GetProperty("type").GetInt32(); + 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.ToString(); + position ??= json.GetProperty("position").GetInt32(); + return new Channel( id, type, guildId ?? Guild.DirectMessages.Id, category ?? GetDefaultCategory(type), name, + position.Value, topic ); } diff --git a/DiscordChatExporter.Domain/Discord/Models/ChannelCategory.cs b/DiscordChatExporter.Domain/Discord/Models/ChannelCategory.cs new file mode 100644 index 0000000..ade3fad --- /dev/null +++ b/DiscordChatExporter.Domain/Discord/Models/ChannelCategory.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Text.Json; +using DiscordChatExporter.Domain.Discord.Models.Common; +using DiscordChatExporter.Domain.Utilities; +using JsonExtensions.Reading; +using Tyrrrz.Extensions; + +namespace DiscordChatExporter.Domain.Discord.Models +{ + public partial class ChannelCategory : IHasId + { + public Snowflake Id { get; } + + public string Name { get; } + + public int Position { get; } + + public ChannelCategory(Snowflake id, string name, int position) + { + Id = id; + Name = name; + Position = position; + } + + public override string ToString() => Name; + + } + + public partial class ChannelCategory + { + public static ChannelCategory Parse(JsonElement json, int? position = null) + { + var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse); + position ??= json.GetProperty("position").GetInt32(); + + var name = json.GetPropertyOrNull("name")?.GetString() ?? + json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(User.Parse).Select(u => u.Name).JoinToString(", ") ?? + id.ToString(); + + return new ChannelCategory( + id, + name, + position.Value + ); + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/Common/ChannelPositionBasedComparer.cs b/DiscordChatExporter.Domain/Discord/Models/Common/ChannelPositionBasedComparer.cs new file mode 100644 index 0000000..e07590a --- /dev/null +++ b/DiscordChatExporter.Domain/Discord/Models/Common/ChannelPositionBasedComparer.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace DiscordChatExporter.Domain.Discord.Models.Common +{ + public partial class ChannelPositionBasedComparer : IComparer + { + public int Compare(Channel? x, Channel? y) + { + int result; + if (x != null) + { + result = x.Position.CompareTo(y?.Position); + } + else if (y != null) + { + result = -y.Position.CompareTo(x?.Position); + } + else + { + result = 0; + } + return result; + } + } + + public partial class ChannelPositionBasedComparer + { + public static ChannelPositionBasedComparer Instance { get; } = new(); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Exporting/ExportRequest.cs b/DiscordChatExporter.Domain/Exporting/ExportRequest.cs index eb53301..a04a4be 100644 --- a/DiscordChatExporter.Domain/Exporting/ExportRequest.cs +++ b/DiscordChatExporter.Domain/Exporting/ExportRequest.cs @@ -1,5 +1,7 @@ -using System.IO; +using System; +using System.IO; using System.Text; +using System.Text.RegularExpressions; using DiscordChatExporter.Domain.Discord; using DiscordChatExporter.Domain.Discord.Models; using DiscordChatExporter.Domain.Internal; @@ -81,6 +83,26 @@ namespace DiscordChatExporter.Domain.Exporting Snowflake? after = null, Snowflake? before = null) { + + // Formats path + outputPath = Regex.Replace(outputPath, "%.", m => + PathEx.EscapePath(m.Value switch + { + "%g" => guild.Id.ToString(), + "%G" => guild.Name, + "%t" => channel.Category.Id.ToString(), + "%T" => channel.Category.Name, + "%c" => channel.Id.ToString(), + "%C" => channel.Name, + "%p" => channel.Position.ToString(), + "%P" => channel.Category.Position.ToString(), + "%a" => (after ?? Snowflake.Zero).ToDate().ToString("yyyy-MM-dd"), + "%b" => (before?.ToDate() ?? DateTime.Now).ToString("yyyy-MM-dd"), + "%%" => "%", + _ => m.Value + }) + ); + // Output is a directory if (Directory.Exists(outputPath) || string.IsNullOrWhiteSpace(Path.GetExtension(outputPath))) { @@ -102,7 +124,7 @@ namespace DiscordChatExporter.Domain.Exporting var buffer = new StringBuilder(); // Guild and channel names - buffer.Append($"{guild.Name} - {channel.Category} - {channel.Name} [{channel.Id}]"); + buffer.Append($"{guild.Name} - {channel.Category.Name} - {channel.Name} [{channel.Id}]"); // Date range if (after != null || before != null) diff --git a/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs b/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs index a897f4f..0c27f97 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs @@ -192,7 +192,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers _writer.WriteStartObject("channel"); _writer.WriteString("id", Context.Request.Channel.Id.ToString()); _writer.WriteString("type", Context.Request.Channel.Type.ToString()); - _writer.WriteString("category", Context.Request.Channel.Category); + _writer.WriteString("category", Context.Request.Channel.Category.Name); _writer.WriteString("name", Context.Request.Channel.Name); _writer.WriteString("topic", Context.Request.Channel.Topic); _writer.WriteEndObject(); diff --git a/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs b/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs index 6f25999..9e486e8 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs @@ -113,7 +113,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers { await _writer.WriteLineAsync('='.Repeat(62)); await _writer.WriteLineAsync($"Guild: {Context.Request.Guild.Name}"); - await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.Category} / {Context.Request.Channel.Name}"); + await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.Category.Name} / {Context.Request.Channel.Name}"); if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic)) await _writer.WriteLineAsync($"Topic: {Context.Request.Channel.Topic}"); diff --git a/DiscordChatExporter.Gui/Views/RootView.xaml b/DiscordChatExporter.Gui/Views/RootView.xaml index 292d6df..841c3cc 100644 --- a/DiscordChatExporter.Gui/Views/RootView.xaml +++ b/DiscordChatExporter.Gui/Views/RootView.xaml @@ -30,7 +30,7 @@ - +