From 3d9ee3b3391ef8fccac8ded08dae626d3ca5d2c1 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Sun, 27 Dec 2020 19:41:28 +0200 Subject: [PATCH] Embrace Snowflake as first class citizen --- .../Commands/Base/ExportCommandBase.cs | 78 ++++++------------- .../Base/ExportMultipleCommandBase.cs | 9 +-- .../Commands/Base/TokenCommandBase.cs | 12 +-- .../Commands/ExportAllCommand.cs | 5 +- .../Commands/ExportChannelCommand.cs | 6 +- .../Commands/ExportGuildCommand.cs | 6 +- .../Commands/GetChannelsCommand.cs | 6 +- .../Internal/Pollyfills.cs | 9 +++ .../Discord/DiscordClient.cs | 46 +++++------ .../Discord/Models/Attachment.cs | 8 +- .../Discord/Models/Channel.cs | 13 ++-- .../Discord/Models/Common/IHasId.cs | 2 +- .../Models/Common/IdBasedEqualityComparer.cs | 4 +- .../Discord/Models/Embed.cs | 1 + .../Discord/Models/Guild.cs | 11 +-- .../Discord/Models/Member.cs | 14 ++-- .../Discord/Models/Message.cs | 8 +- .../Discord/Models/Reaction.cs | 2 +- .../Discord/Models/Role.cs | 10 ++- .../Discord/Models/User.cs | 10 +-- .../Discord/Snowflake.cs | 68 ++++++++++++++++ .../Exporting/ExportContext.cs | 12 ++- .../Exporting/ExportRequest.cs | 26 +++---- .../Exporting/MediaDownloader.cs | 1 + .../Exporting/Writers/CsvMessageWriter.cs | 2 +- .../Exporting/Writers/Html/MessageGroup.cs | 2 +- .../Writers/Html/PreambleTemplate.cshtml | 6 +- .../Exporting/Writers/JsonMessageWriter.cs | 16 ++-- .../MarkdownVisitors/HtmlMarkdownVisitor.cs | 7 +- .../PlainTextMarkdownVisitor.cs | 7 +- .../Writers/PlainTextMessageWriter.cs | 4 +- .../Internal/Extensions/DateExtensions.cs | 6 -- .../Internal/Extensions/GenericExtensions.cs | 2 - .../Utilities/GeneralExtensions.cs | 9 +++ .../Dialogs/ExportSetupViewModel.cs | 6 +- .../ViewModels/RootViewModel.cs | 4 +- 36 files changed, 243 insertions(+), 195 deletions(-) create mode 100644 DiscordChatExporter.Cli/Internal/Pollyfills.cs create mode 100644 DiscordChatExporter.Domain/Discord/Snowflake.cs create mode 100644 DiscordChatExporter.Domain/Utilities/GeneralExtensions.cs diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs index 75a8bca..21071d2 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs @@ -1,49 +1,40 @@ -using System; -using System.IO; -using System.Text.RegularExpressions; +using System.IO; using System.Threading.Tasks; using CliFx; using CliFx.Attributes; using CliFx.Exceptions; using CliFx.Utilities; +using DiscordChatExporter.Domain.Discord; using DiscordChatExporter.Domain.Discord.Models; using DiscordChatExporter.Domain.Exporting; namespace DiscordChatExporter.Cli.Commands.Base { - public abstract partial class ExportCommandBase : TokenCommandBase + public abstract class ExportCommandBase : TokenCommandBase { - [CommandOption("output", 'o', - Description = "Output file or directory path.")] - public string OutputPath { get; set; } = Directory.GetCurrentDirectory(); + [CommandOption("output", 'o', Description = "Output file or directory path.")] + public string OutputPath { get; init; } = Directory.GetCurrentDirectory(); - [CommandOption("format", 'f', - Description = "Export format.")] - public ExportFormat ExportFormat { get; set; } = ExportFormat.HtmlDark; + [CommandOption("format", 'f', Description = "Export format.")] + public ExportFormat ExportFormat { get; init; } = ExportFormat.HtmlDark; - [CommandOption("after", - Description = "Only include messages sent after this date. Alternatively, provide the ID of a message.")] - public string? After { get; set; } + [CommandOption("after", Description = "Only include messages sent after this date or message ID.")] + public Snowflake? After { get; init; } - [CommandOption("before", - Description = "Only include messages sent before this date. Alternatively, provide the ID of a message.")] - public string? Before { get; set; } + [CommandOption("before", Description = "Only include messages sent before this date or message ID.")] + public Snowflake? Before { get; init; } - [CommandOption("partition", 'p', - Description = "Split output into partitions limited to this number of messages.")] - public int? PartitionLimit { get; set; } + [CommandOption("partition", 'p', Description = "Split output into partitions limited to this number of messages.")] + public int? PartitionLimit { get; init; } - [CommandOption("media", - Description = "Download referenced media content.")] - public bool ShouldDownloadMedia { get; set; } + [CommandOption("media", Description = "Download referenced media content.")] + public bool ShouldDownloadMedia { get; init; } - [CommandOption("reuse-media", - Description = "Reuse already existing media content to skip redundant downloads.")] - public bool ShouldReuseMedia { get; set; } + [CommandOption("reuse-media", Description = "Reuse already existing media content to skip redundant downloads.")] + public bool ShouldReuseMedia { get; init; } - [CommandOption("dateformat", - Description = "Format used when writing dates.")] - public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt"; + [CommandOption("dateformat", Description = "Format used when writing dates.")] + public string DateFormat { get; init; } = "dd-MMM-yy hh:mm tt"; protected ChannelExporter GetChannelExporter() => new(GetDiscordClient()); @@ -57,8 +48,8 @@ namespace DiscordChatExporter.Cli.Commands.Base channel, OutputPath, ExportFormat, - ParseRangeOption(After, "--after"), - ParseRangeOption(Before, "--before"), + After, + Before, PartitionLimit, ShouldDownloadMedia, ShouldReuseMedia, @@ -77,7 +68,7 @@ namespace DiscordChatExporter.Cli.Commands.Base await ExportAsync(console, guild, channel); } - protected async ValueTask ExportAsync(IConsole console, string channelId) + protected async ValueTask ExportAsync(IConsole console, Snowflake channelId) { var channel = await GetDiscordClient().GetChannelAsync(channelId); await ExportAsync(console, channel); @@ -93,29 +84,4 @@ namespace DiscordChatExporter.Cli.Commands.Base return default; } } - - public abstract partial class ExportCommandBase : TokenCommandBase - { - protected static DateTimeOffset? ParseRangeOption(string? value, string optionName) - { - if (value == null) return null; - - var isSnowflake = Regex.IsMatch(value, @"^\d{18}$"); - var isDate = DateTimeOffset.TryParse(value, out var datetime); - - if (!isSnowflake && !isDate) - { - throw new ArgumentException($"Value for ${optionName} must be either a date or a message ID."); - } - - return isSnowflake ? ExtractDateTimeFromSnowflake() : datetime; - - DateTimeOffset ExtractDateTimeFromSnowflake() - { - var unixTimestampMs = (long.Parse(value) / 4194304 + 1420070400000); - return DateTimeOffset.FromUnixTimeMilliseconds(unixTimestampMs); - } - } - } - } \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportMultipleCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportMultipleCommandBase.cs index bb54201..a1f4cda 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportMultipleCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportMultipleCommandBase.cs @@ -17,9 +17,8 @@ namespace DiscordChatExporter.Cli.Commands.Base { public abstract class ExportMultipleCommandBase : ExportCommandBase { - [CommandOption("parallel", - Description = "Limits how many channels can be exported in parallel.")] - public int ParallelLimit { get; set; } = 1; + [CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")] + public int ParallelLimit { get; init; } = 1; protected async ValueTask ExportMultipleAsync(IConsole console, IReadOnlyList channels) { @@ -47,8 +46,8 @@ namespace DiscordChatExporter.Cli.Commands.Base channel, OutputPath, ExportFormat, - ParseRangeOption(After, "--after"), - ParseRangeOption(Before, "--before"), + After, + Before, PartitionLimit, ShouldDownloadMedia, ShouldReuseMedia, diff --git a/DiscordChatExporter.Cli/Commands/Base/TokenCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/TokenCommandBase.cs index 870ca0a..1b49a31 100644 --- a/DiscordChatExporter.Cli/Commands/Base/TokenCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/TokenCommandBase.cs @@ -7,15 +7,11 @@ namespace DiscordChatExporter.Cli.Commands.Base { public abstract class TokenCommandBase : ICommand { - [CommandOption("token", 't', IsRequired = true, - EnvironmentVariableName = "DISCORD_TOKEN", - Description = "Authorization token.")] - public string TokenValue { get; set; } = ""; + [CommandOption("token", 't', IsRequired = true, EnvironmentVariableName = "DISCORD_TOKEN", Description = "Authorization token.")] + public string TokenValue { get; init; } = ""; - [CommandOption("bot", 'b', - EnvironmentVariableName = "DISCORD_TOKEN_BOT", - Description = "Authorize as a bot.")] - public bool IsBotToken { get; set; } + [CommandOption("bot", 'b', EnvironmentVariableName = "DISCORD_TOKEN_BOT", Description = "Authorize as a bot.")] + public bool IsBotToken { get; init; } protected AuthToken GetAuthToken() => new( IsBotToken diff --git a/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs b/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs index a37880c..fe67b71 100644 --- a/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportAllCommand.cs @@ -10,9 +10,8 @@ namespace DiscordChatExporter.Cli.Commands [Command("exportall", Description = "Export all accessible channels.")] public class ExportAllCommand : ExportMultipleCommandBase { - [CommandOption("include-dm", - Description = "Include direct message channels.")] - public bool IncludeDirectMessages { get; set; } = true; + [CommandOption("include-dm", Description = "Include direct message channels.")] + public bool IncludeDirectMessages { get; init; } = true; public override async ValueTask ExecuteAsync(IConsole console) { diff --git a/DiscordChatExporter.Cli/Commands/ExportChannelCommand.cs b/DiscordChatExporter.Cli/Commands/ExportChannelCommand.cs index 108f4dc..97ca90a 100644 --- a/DiscordChatExporter.Cli/Commands/ExportChannelCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportChannelCommand.cs @@ -2,15 +2,15 @@ using CliFx; using CliFx.Attributes; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Domain.Discord; namespace DiscordChatExporter.Cli.Commands { [Command("export", Description = "Export a channel.")] public class ExportChannelCommand : ExportCommandBase { - [CommandOption("channel", 'c', IsRequired = true, - Description = "Channel ID.")] - public string ChannelId { get; set; } = ""; + [CommandOption("channel", 'c', IsRequired = true, Description = "Channel ID.")] + public Snowflake ChannelId { get; init; } public override async ValueTask ExecuteAsync(IConsole console) { diff --git a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs index 8564b37..0250a61 100644 --- a/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs +++ b/DiscordChatExporter.Cli/Commands/ExportGuildCommand.cs @@ -2,6 +2,7 @@ using CliFx; using CliFx.Attributes; using DiscordChatExporter.Cli.Commands.Base; +using DiscordChatExporter.Domain.Discord; using DiscordChatExporter.Domain.Utilities; namespace DiscordChatExporter.Cli.Commands @@ -9,9 +10,8 @@ namespace DiscordChatExporter.Cli.Commands [Command("exportguild", Description = "Export all channels within specified guild.")] public class ExportGuildCommand : ExportMultipleCommandBase { - [CommandOption("guild", 'g', IsRequired = true, - Description = "Guild ID.")] - public string GuildId { get; set; } = ""; + [CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")] + public Snowflake GuildId { get; init; } public override async ValueTask ExecuteAsync(IConsole console) { diff --git a/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs b/DiscordChatExporter.Cli/Commands/GetChannelsCommand.cs index 5b1a0e5..3eedc28 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.Discord; using DiscordChatExporter.Domain.Utilities; namespace DiscordChatExporter.Cli.Commands @@ -10,9 +11,8 @@ namespace DiscordChatExporter.Cli.Commands [Command("channels", Description = "Get the list of channels in a guild.")] public class GetChannelsCommand : TokenCommandBase { - [CommandOption("guild", 'g', IsRequired = true, - Description = "Guild ID.")] - public string GuildId { get; set; } = ""; + [CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")] + public Snowflake GuildId { get; init; } public override async ValueTask ExecuteAsync(IConsole console) { diff --git a/DiscordChatExporter.Cli/Internal/Pollyfills.cs b/DiscordChatExporter.Cli/Internal/Pollyfills.cs new file mode 100644 index 0000000..2e592b8 --- /dev/null +++ b/DiscordChatExporter.Cli/Internal/Pollyfills.cs @@ -0,0 +1,9 @@ +// ReSharper disable CheckNamespace +// TODO: remove after moving to .NET 5 + +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit + { + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/DiscordClient.cs b/DiscordChatExporter.Domain/Discord/DiscordClient.cs index c0450ec..7efebbe 100644 --- a/DiscordChatExporter.Domain/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Domain/Discord/DiscordClient.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using DiscordChatExporter.Domain.Discord.Models; using DiscordChatExporter.Domain.Exceptions; using DiscordChatExporter.Domain.Internal; -using DiscordChatExporter.Domain.Internal.Extensions; +using DiscordChatExporter.Domain.Utilities; using JsonExtensions.Http; using JsonExtensions.Reading; @@ -70,13 +70,14 @@ namespace DiscordChatExporter.Domain.Discord { yield return Guild.DirectMessages; - var afterId = ""; + var currentAfter = Snowflake.Zero; + while (true) { var url = new UrlBuilder() .SetPath("users/@me/guilds") .SetQueryParameter("limit", "100") - .SetQueryParameter("after", afterId) + .SetQueryParameter("after", currentAfter.ToString()) .Build(); var response = await GetJsonResponseAsync(url); @@ -86,7 +87,7 @@ namespace DiscordChatExporter.Domain.Discord { yield return guild; - afterId = guild.Id; + currentAfter = guild.Id; isEmpty = false; } @@ -95,7 +96,7 @@ namespace DiscordChatExporter.Domain.Discord } } - public async ValueTask GetGuildAsync(string guildId) + public async ValueTask GetGuildAsync(Snowflake guildId) { if (guildId == Guild.DirectMessages.Id) return Guild.DirectMessages; @@ -104,7 +105,7 @@ namespace DiscordChatExporter.Domain.Discord return Guild.Parse(response); } - public async IAsyncEnumerable GetGuildChannelsAsync(string guildId) + public async IAsyncEnumerable GetGuildChannelsAsync(Snowflake guildId) { if (guildId == Guild.DirectMessages.Id) { @@ -141,7 +142,7 @@ namespace DiscordChatExporter.Domain.Discord } } - public async IAsyncEnumerable GetGuildRolesAsync(string guildId) + public async IAsyncEnumerable GetGuildRolesAsync(Snowflake guildId) { if (guildId == Guild.DirectMessages.Id) yield break; @@ -152,7 +153,7 @@ namespace DiscordChatExporter.Domain.Discord yield return Role.Parse(roleJson); } - public async ValueTask TryGetGuildMemberAsync(string guildId, User user) + public async ValueTask TryGetGuildMemberAsync(Snowflake guildId, User user) { if (guildId == Guild.DirectMessages.Id) return Member.CreateForUser(user); @@ -161,30 +162,31 @@ namespace DiscordChatExporter.Domain.Discord return response?.Pipe(Member.Parse); } - private async ValueTask GetChannelCategoryAsync(string channelParentId) + private async ValueTask GetChannelCategoryAsync(Snowflake channelParentId) { var response = await GetJsonResponseAsync($"channels/{channelParentId}"); return response.GetProperty("name").GetString(); } - public async ValueTask GetChannelAsync(string channelId) + public async ValueTask GetChannelAsync(Snowflake channelId) { var response = await GetJsonResponseAsync($"channels/{channelId}"); - var parentId = response.GetPropertyOrNull("parent_id")?.GetString(); - var category = !string.IsNullOrWhiteSpace(parentId) - ? await GetChannelCategoryAsync(parentId) + var parentId = response.GetPropertyOrNull("parent_id")?.GetString().Pipe(Snowflake.Parse); + + var category = parentId != null + ? await GetChannelCategoryAsync(parentId.Value) : null; return Channel.Parse(response, category); } - private async ValueTask TryGetLastMessageAsync(string channelId, DateTimeOffset? before = null) + private async ValueTask TryGetLastMessageAsync(Snowflake channelId, Snowflake? before = null) { var url = new UrlBuilder() .SetPath($"channels/{channelId}/messages") .SetQueryParameter("limit", "1") - .SetQueryParameter("before", before?.ToSnowflake()) + .SetQueryParameter("before", before?.ToString()) .Build(); var response = await GetJsonResponseAsync(url); @@ -192,9 +194,9 @@ namespace DiscordChatExporter.Domain.Discord } public async IAsyncEnumerable GetMessagesAsync( - string channelId, - DateTimeOffset? after = null, - DateTimeOffset? before = null, + Snowflake channelId, + Snowflake? after = null, + Snowflake? before = null, IProgress? progress = null) { // Get the last message in the specified range. @@ -202,19 +204,19 @@ namespace DiscordChatExporter.Domain.Discord // will not appear in the output. // Additionally, it provides the date of the last message, which is used to calculate progress. var lastMessage = await TryGetLastMessageAsync(channelId, before); - if (lastMessage == null || lastMessage.Timestamp < after) + if (lastMessage == null || lastMessage.Timestamp < after?.ToDate()) yield break; // Keep track of first message in range in order to calculate progress var firstMessage = default(Message); - var afterId = after?.ToSnowflake() ?? "0"; + var currentAfter = after ?? Snowflake.Zero; while (true) { var url = new UrlBuilder() .SetPath($"channels/{channelId}/messages") .SetQueryParameter("limit", "100") - .SetQueryParameter("after", afterId) + .SetQueryParameter("after", currentAfter.ToString()) .Build(); var response = await GetJsonResponseAsync(url); @@ -244,7 +246,7 @@ namespace DiscordChatExporter.Domain.Discord ); yield return message; - afterId = message.Id; + currentAfter = message.Id; } } } diff --git a/DiscordChatExporter.Domain/Discord/Models/Attachment.cs b/DiscordChatExporter.Domain/Discord/Models/Attachment.cs index 60c3487..1d59c64 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Attachment.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Attachment.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Text.Json; using DiscordChatExporter.Domain.Discord.Models.Common; -using DiscordChatExporter.Domain.Internal.Extensions; +using DiscordChatExporter.Domain.Utilities; using JsonExtensions.Reading; namespace DiscordChatExporter.Domain.Discord.Models @@ -11,7 +11,7 @@ namespace DiscordChatExporter.Domain.Discord.Models // https://discord.com/developers/docs/resources/channel#attachment-object public partial class Attachment : IHasId { - public string Id { get; } + public Snowflake Id { get; } public string Url { get; } @@ -32,7 +32,7 @@ namespace DiscordChatExporter.Domain.Discord.Models public FileSize FileSize { get; } - public Attachment(string id, string url, string fileName, int? width, int? height, FileSize fileSize) + public Attachment(Snowflake id, string url, string fileName, int? width, int? height, FileSize fileSize) { Id = id; Url = url; @@ -58,7 +58,7 @@ namespace DiscordChatExporter.Domain.Discord.Models public static Attachment Parse(JsonElement json) { - var id = json.GetProperty("id").GetString(); + var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse); var url = json.GetProperty("url").GetString(); var width = json.GetPropertyOrNull("width")?.GetInt32(); var height = json.GetPropertyOrNull("height")?.GetInt32(); diff --git a/DiscordChatExporter.Domain/Discord/Models/Channel.cs b/DiscordChatExporter.Domain/Discord/Models/Channel.cs index 01d8708..5555409 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Channel.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Channel.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Text.Json; using DiscordChatExporter.Domain.Discord.Models.Common; +using DiscordChatExporter.Domain.Utilities; using JsonExtensions.Reading; using Tyrrrz.Extensions; @@ -22,7 +23,7 @@ namespace DiscordChatExporter.Domain.Discord.Models // https://discord.com/developers/docs/resources/channel#channel-object public partial class Channel : IHasId { - public string Id { get; } + public Snowflake Id { get; } public ChannelType Type { get; } @@ -33,7 +34,7 @@ namespace DiscordChatExporter.Domain.Discord.Models Type == ChannelType.GuildNews || Type == ChannelType.GuildStore; - public string GuildId { get; } + public Snowflake GuildId { get; } public string Category { get; } @@ -41,7 +42,7 @@ namespace DiscordChatExporter.Domain.Discord.Models public string? Topic { get; } - public Channel(string id, ChannelType type, string guildId, string category, string name, string? topic) + public Channel(Snowflake id, ChannelType type, Snowflake guildId, string category, string name, string? topic) { Id = id; Type = type; @@ -68,8 +69,8 @@ namespace DiscordChatExporter.Domain.Discord.Models public static Channel Parse(JsonElement json, string? category = null) { - var id = json.GetProperty("id").GetString(); - var guildId = json.GetPropertyOrNull("guild_id")?.GetString(); + 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(); @@ -77,7 +78,7 @@ namespace DiscordChatExporter.Domain.Discord.Models var name = json.GetPropertyOrNull("name")?.GetString() ?? json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(User.Parse).Select(u => u.Name).JoinToString(", ") ?? - id; + id.ToString(); return new Channel( id, diff --git a/DiscordChatExporter.Domain/Discord/Models/Common/IHasId.cs b/DiscordChatExporter.Domain/Discord/Models/Common/IHasId.cs index d1056f8..43ffff3 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Common/IHasId.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Common/IHasId.cs @@ -2,6 +2,6 @@ { public interface IHasId { - string Id { get; } + Snowflake Id { get; } } } \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Discord/Models/Common/IdBasedEqualityComparer.cs b/DiscordChatExporter.Domain/Discord/Models/Common/IdBasedEqualityComparer.cs index fff743f..69fa47d 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Common/IdBasedEqualityComparer.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Common/IdBasedEqualityComparer.cs @@ -5,9 +5,9 @@ namespace DiscordChatExporter.Domain.Discord.Models.Common { public partial class IdBasedEqualityComparer : IEqualityComparer { - public bool Equals(IHasId? x, IHasId? y) => StringComparer.Ordinal.Equals(x?.Id, y?.Id); + public bool Equals(IHasId? x, IHasId? y) => x?.Id == y?.Id; - public int GetHashCode(IHasId obj) => StringComparer.Ordinal.GetHashCode(obj.Id); + public int GetHashCode(IHasId obj) => obj.Id.GetHashCode(); } public partial class IdBasedEqualityComparer diff --git a/DiscordChatExporter.Domain/Discord/Models/Embed.cs b/DiscordChatExporter.Domain/Discord/Models/Embed.cs index 635d662..1e6ec3a 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Embed.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Embed.cs @@ -4,6 +4,7 @@ using System.Drawing; using System.Linq; using System.Text.Json; using DiscordChatExporter.Domain.Internal.Extensions; +using DiscordChatExporter.Domain.Utilities; using JsonExtensions.Reading; namespace DiscordChatExporter.Domain.Discord.Models diff --git a/DiscordChatExporter.Domain/Discord/Models/Guild.cs b/DiscordChatExporter.Domain/Discord/Models/Guild.cs index b490690..702fa78 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Guild.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Guild.cs @@ -1,18 +1,19 @@ using System.Text.Json; using DiscordChatExporter.Domain.Discord.Models.Common; +using DiscordChatExporter.Domain.Utilities; namespace DiscordChatExporter.Domain.Discord.Models { // https://discord.com/developers/docs/resources/guild#guild-object public partial class Guild : IHasId { - public string Id { get; } + public Snowflake Id { get; } public string Name { get; } public string IconUrl { get; } - public Guild(string id, string name, string iconUrl) + public Guild(Snowflake id, string name, string iconUrl) { Id = id; Name = name; @@ -24,17 +25,17 @@ namespace DiscordChatExporter.Domain.Discord.Models public partial class Guild { - public static Guild DirectMessages { get; } = new("@me", "Direct Messages", GetDefaultIconUrl()); + public static Guild DirectMessages { get; } = new(Snowflake.Zero, "Direct Messages", GetDefaultIconUrl()); private static string GetDefaultIconUrl() => "https://cdn.discordapp.com/embed/avatars/0.png"; - private static string GetIconUrl(string id, string iconHash) => + private static string GetIconUrl(Snowflake id, string iconHash) => $"https://cdn.discordapp.com/icons/{id}/{iconHash}.png"; public static Guild Parse(JsonElement json) { - var id = json.GetProperty("id").GetString(); + var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse); var name = json.GetProperty("name").GetString(); var iconHash = json.GetProperty("icon").GetString(); diff --git a/DiscordChatExporter.Domain/Discord/Models/Member.cs b/DiscordChatExporter.Domain/Discord/Models/Member.cs index 8487cd1..636ac75 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Member.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Member.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; using DiscordChatExporter.Domain.Discord.Models.Common; -using DiscordChatExporter.Domain.Internal.Extensions; +using DiscordChatExporter.Domain.Utilities; using JsonExtensions.Reading; namespace DiscordChatExporter.Domain.Discord.Models @@ -11,15 +11,15 @@ namespace DiscordChatExporter.Domain.Discord.Models // https://discord.com/developers/docs/resources/guild#guild-member-object public partial class Member : IHasId { - public string Id => User.Id; + public Snowflake Id => User.Id; public User User { get; } public string Nick { get; } - public IReadOnlyList RoleIds { get; } + public IReadOnlyList RoleIds { get; } - public Member(User user, string? nick, IReadOnlyList roleIds) + public Member(User user, string? nick, IReadOnlyList roleIds) { User = user; Nick = nick ?? user.Name; @@ -31,7 +31,7 @@ namespace DiscordChatExporter.Domain.Discord.Models public partial class Member { - public static Member CreateForUser(User user) => new(user, null, Array.Empty()); + public static Member CreateForUser(User user) => new(user, null, Array.Empty()); public static Member Parse(JsonElement json) { @@ -39,8 +39,8 @@ namespace DiscordChatExporter.Domain.Discord.Models var nick = json.GetPropertyOrNull("nick")?.GetString(); var roleIds = - json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString()).ToArray() ?? - Array.Empty(); + json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString().Pipe(Snowflake.Parse)).ToArray() ?? + Array.Empty(); return new Member( user, diff --git a/DiscordChatExporter.Domain/Discord/Models/Message.cs b/DiscordChatExporter.Domain/Discord/Models/Message.cs index b30f67b..cfae917 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Message.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Message.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; using DiscordChatExporter.Domain.Discord.Models.Common; -using DiscordChatExporter.Domain.Internal.Extensions; +using DiscordChatExporter.Domain.Utilities; using JsonExtensions.Reading; namespace DiscordChatExporter.Domain.Discord.Models @@ -24,7 +24,7 @@ namespace DiscordChatExporter.Domain.Discord.Models // https://discord.com/developers/docs/resources/channel#message-object public partial class Message : IHasId { - public string Id { get; } + public Snowflake Id { get; } public MessageType Type { get; } @@ -49,7 +49,7 @@ namespace DiscordChatExporter.Domain.Discord.Models public IReadOnlyList MentionedUsers { get; } public Message( - string id, + Snowflake id, MessageType type, User author, DateTimeOffset timestamp, @@ -83,7 +83,7 @@ namespace DiscordChatExporter.Domain.Discord.Models { public static Message Parse(JsonElement json) { - var id = json.GetProperty("id").GetString(); + var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse); var author = json.GetProperty("author").Pipe(User.Parse); var timestamp = json.GetProperty("timestamp").GetDateTimeOffset(); var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffset(); diff --git a/DiscordChatExporter.Domain/Discord/Models/Reaction.cs b/DiscordChatExporter.Domain/Discord/Models/Reaction.cs index d120492..c07ae5f 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Reaction.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Reaction.cs @@ -1,5 +1,5 @@ using System.Text.Json; -using DiscordChatExporter.Domain.Internal.Extensions; +using DiscordChatExporter.Domain.Utilities; namespace DiscordChatExporter.Domain.Discord.Models { diff --git a/DiscordChatExporter.Domain/Discord/Models/Role.cs b/DiscordChatExporter.Domain/Discord/Models/Role.cs index b7a6a73..1967d62 100644 --- a/DiscordChatExporter.Domain/Discord/Models/Role.cs +++ b/DiscordChatExporter.Domain/Discord/Models/Role.cs @@ -1,14 +1,16 @@ using System.Drawing; using System.Text.Json; +using DiscordChatExporter.Domain.Discord.Models.Common; using DiscordChatExporter.Domain.Internal.Extensions; +using DiscordChatExporter.Domain.Utilities; using JsonExtensions.Reading; namespace DiscordChatExporter.Domain.Discord.Models { // https://discord.com/developers/docs/topics/permissions#role-object - public partial class Role + public partial class Role : IHasId { - public string Id { get; } + public Snowflake Id { get; } public string Name { get; } @@ -16,7 +18,7 @@ namespace DiscordChatExporter.Domain.Discord.Models public Color? Color { get; } - public Role(string id, string name, int position, Color? color) + public Role(Snowflake id, string name, int position, Color? color) { Id = id; Name = name; @@ -31,7 +33,7 @@ namespace DiscordChatExporter.Domain.Discord.Models { public static Role Parse(JsonElement json) { - var id = json.GetProperty("id").GetString(); + var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse); var name = json.GetProperty("name").GetString(); var position = json.GetProperty("position").GetInt32(); diff --git a/DiscordChatExporter.Domain/Discord/Models/User.cs b/DiscordChatExporter.Domain/Discord/Models/User.cs index 1bc189c..8c859e6 100644 --- a/DiscordChatExporter.Domain/Discord/Models/User.cs +++ b/DiscordChatExporter.Domain/Discord/Models/User.cs @@ -1,7 +1,7 @@ using System; using System.Text.Json; using DiscordChatExporter.Domain.Discord.Models.Common; -using DiscordChatExporter.Domain.Internal.Extensions; +using DiscordChatExporter.Domain.Utilities; using JsonExtensions.Reading; namespace DiscordChatExporter.Domain.Discord.Models @@ -9,7 +9,7 @@ namespace DiscordChatExporter.Domain.Discord.Models // https://discord.com/developers/docs/resources/user#user-object public partial class User : IHasId { - public string Id { get; } + public Snowflake Id { get; } public bool IsBot { get; } @@ -21,7 +21,7 @@ namespace DiscordChatExporter.Domain.Discord.Models public string AvatarUrl { get; } - public User(string id, bool isBot, int discriminator, string name, string avatarUrl) + public User(Snowflake id, bool isBot, int discriminator, string name, string avatarUrl) { Id = id; IsBot = isBot; @@ -38,7 +38,7 @@ namespace DiscordChatExporter.Domain.Discord.Models private static string GetDefaultAvatarUrl(int discriminator) => $"https://cdn.discordapp.com/embed/avatars/{discriminator % 5}.png"; - private static string GetAvatarUrl(string id, string avatarHash) + private static string GetAvatarUrl(Snowflake id, string avatarHash) { // Animated if (avatarHash.StartsWith("a_", StringComparison.Ordinal)) @@ -50,7 +50,7 @@ namespace DiscordChatExporter.Domain.Discord.Models public static User Parse(JsonElement json) { - var id = json.GetProperty("id").GetString(); + var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse); var discriminator = json.GetProperty("discriminator").GetString().Pipe(int.Parse); var name = json.GetProperty("username").GetString(); var avatarHash = json.GetProperty("avatar").GetString(); diff --git a/DiscordChatExporter.Domain/Discord/Snowflake.cs b/DiscordChatExporter.Domain/Discord/Snowflake.cs new file mode 100644 index 0000000..896d74c --- /dev/null +++ b/DiscordChatExporter.Domain/Discord/Snowflake.cs @@ -0,0 +1,68 @@ +using System; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace DiscordChatExporter.Domain.Discord +{ + public readonly partial struct Snowflake + { + public ulong Value { get; } + + public Snowflake(ulong value) => Value = value; + + public DateTimeOffset ToDate() => + DateTimeOffset.FromUnixTimeMilliseconds((long) ((Value >> 22) + 1420070400000UL)).ToLocalTime(); + + public override string ToString() => Value.ToString(CultureInfo.InvariantCulture); + } + + public partial struct Snowflake + { + public static Snowflake Zero { get; } = new(0); + + public static Snowflake FromDate(DateTimeOffset date) + { + var value = ((ulong) date.ToUnixTimeMilliseconds() - 1420070400000UL) << 22; + return new Snowflake(value); + } + + public static Snowflake? TryParse(string? str, IFormatProvider? formatProvider = null) + { + if (string.IsNullOrWhiteSpace(str)) + return null; + + // As number + if (Regex.IsMatch(str, @"^\d{15,}$") && + ulong.TryParse(str, NumberStyles.Number, formatProvider, out var value)) + { + return new Snowflake(value); + } + + // As date + if (DateTimeOffset.TryParse(str, formatProvider, DateTimeStyles.None, out var date)) + { + return FromDate(date); + } + + return null; + } + + public static Snowflake Parse(string str, IFormatProvider? formatProvider) => + TryParse(str, formatProvider) ?? throw new FormatException($"Invalid snowflake: {str}."); + + public static Snowflake Parse(string str) => Parse(str, null); + } + + public partial struct Snowflake : IEquatable + { + public bool Equals(Snowflake other) => Value == other.Value; + + public override bool Equals(object? obj) => obj is Snowflake other && Equals(other); + + public override int GetHashCode() => Value.GetHashCode(); + + public static bool operator ==(Snowflake left, Snowflake right) => left.Equals(right); + + public static bool operator !=(Snowflake left, Snowflake right) => !(left == right); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Domain/Exporting/ExportContext.cs b/DiscordChatExporter.Domain/Exporting/ExportContext.cs index 535ba4d..ec29be0 100644 --- a/DiscordChatExporter.Domain/Exporting/ExportContext.cs +++ b/DiscordChatExporter.Domain/Exporting/ExportContext.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using DiscordChatExporter.Domain.Discord; using DiscordChatExporter.Domain.Discord.Models; using DiscordChatExporter.Domain.Internal.Extensions; using Tyrrrz.Extensions; @@ -44,16 +45,13 @@ namespace DiscordChatExporter.Domain.Exporting var dateFormat => date.ToLocalString(dateFormat) }; - public Member? TryGetMember(string id) => - Members.FirstOrDefault(m => m.Id == id); + public Member? TryGetMember(Snowflake id) => Members.FirstOrDefault(m => m.Id == id); - public Channel? TryGetChannel(string id) => - Channels.FirstOrDefault(c => c.Id == id); + public Channel? TryGetChannel(Snowflake id) => Channels.FirstOrDefault(c => c.Id == id); - public Role? TryGetRole(string id) => - Roles.FirstOrDefault(r => r.Id == id); + public Role? TryGetRole(Snowflake id) => Roles.FirstOrDefault(r => r.Id == id); - public Color? TryGetUserColor(string id) + public Color? TryGetUserColor(Snowflake id) { var member = TryGetMember(id); var roles = member?.RoleIds.Join(Roles, i => i, r => r.Id, (_, role) => role); diff --git a/DiscordChatExporter.Domain/Exporting/ExportRequest.cs b/DiscordChatExporter.Domain/Exporting/ExportRequest.cs index 745ba36..eb53301 100644 --- a/DiscordChatExporter.Domain/Exporting/ExportRequest.cs +++ b/DiscordChatExporter.Domain/Exporting/ExportRequest.cs @@ -1,6 +1,6 @@ -using System; -using System.IO; +using System.IO; using System.Text; +using DiscordChatExporter.Domain.Discord; using DiscordChatExporter.Domain.Discord.Models; using DiscordChatExporter.Domain.Internal; @@ -22,9 +22,9 @@ namespace DiscordChatExporter.Domain.Exporting public ExportFormat Format { get; } - public DateTimeOffset? After { get; } + public Snowflake? After { get; } - public DateTimeOffset? Before { get; } + public Snowflake? Before { get; } public int? PartitionLimit { get; } @@ -39,8 +39,8 @@ namespace DiscordChatExporter.Domain.Exporting Channel channel, string outputPath, ExportFormat format, - DateTimeOffset? after, - DateTimeOffset? before, + Snowflake? after, + Snowflake? before, int? partitionLimit, bool shouldDownloadMedia, bool shouldReuseMedia, @@ -78,8 +78,8 @@ namespace DiscordChatExporter.Domain.Exporting Channel channel, string outputPath, ExportFormat format, - DateTimeOffset? after = null, - DateTimeOffset? before = null) + Snowflake? after = null, + Snowflake? before = null) { // Output is a directory if (Directory.Exists(outputPath) || string.IsNullOrWhiteSpace(Path.GetExtension(outputPath))) @@ -96,8 +96,8 @@ namespace DiscordChatExporter.Domain.Exporting Guild guild, Channel channel, ExportFormat format, - DateTimeOffset? after = null, - DateTimeOffset? before = null) + Snowflake? after = null, + Snowflake? before = null) { var buffer = new StringBuilder(); @@ -112,17 +112,17 @@ namespace DiscordChatExporter.Domain.Exporting // Both 'after' and 'before' are set if (after != null && before != null) { - buffer.Append($"{after:yyyy-MM-dd} to {before:yyyy-MM-dd}"); + buffer.Append($"{after?.ToDate():yyyy-MM-dd} to {before?.ToDate():yyyy-MM-dd}"); } // Only 'after' is set else if (after != null) { - buffer.Append($"after {after:yyyy-MM-dd}"); + buffer.Append($"after {after?.ToDate():yyyy-MM-dd}"); } // Only 'before' is set else { - buffer.Append($"before {before:yyyy-MM-dd}"); + buffer.Append($"before {before?.ToDate():yyyy-MM-dd}"); } buffer.Append(")"); diff --git a/DiscordChatExporter.Domain/Exporting/MediaDownloader.cs b/DiscordChatExporter.Domain/Exporting/MediaDownloader.cs index c39ee80..6ef10c3 100644 --- a/DiscordChatExporter.Domain/Exporting/MediaDownloader.cs +++ b/DiscordChatExporter.Domain/Exporting/MediaDownloader.cs @@ -9,6 +9,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using DiscordChatExporter.Domain.Internal; using DiscordChatExporter.Domain.Internal.Extensions; +using DiscordChatExporter.Domain.Utilities; namespace DiscordChatExporter.Domain.Exporting { diff --git a/DiscordChatExporter.Domain/Exporting/Writers/CsvMessageWriter.cs b/DiscordChatExporter.Domain/Exporting/Writers/CsvMessageWriter.cs index c4f33fc..5d8d9cf 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/CsvMessageWriter.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/CsvMessageWriter.cs @@ -59,7 +59,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers public override async ValueTask WriteMessageAsync(Message message) { // Author ID - await _writer.WriteAsync(CsvEncode(message.Author.Id)); + await _writer.WriteAsync(CsvEncode(message.Author.Id.ToString())); await _writer.WriteAsync(','); // Author name diff --git a/DiscordChatExporter.Domain/Exporting/Writers/Html/MessageGroup.cs b/DiscordChatExporter.Domain/Exporting/Writers/Html/MessageGroup.cs index e747f51..18fbeb4 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/Html/MessageGroup.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/Html/MessageGroup.cs @@ -25,7 +25,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.Html internal partial class MessageGroup { public static bool CanJoin(Message message1, Message message2) => - string.Equals(message1.Author.Id, message2.Author.Id, StringComparison.Ordinal) && + message1.Author.Id == message2.Author.Id && string.Equals(message1.Author.FullName, message2.Author.FullName, StringComparison.Ordinal) && (message2.Timestamp - message1.Timestamp).Duration().TotalMinutes <= 7; diff --git a/DiscordChatExporter.Domain/Exporting/Writers/Html/PreambleTemplate.cshtml b/DiscordChatExporter.Domain/Exporting/Writers/Html/PreambleTemplate.cshtml index e00d5b7..1e9026c 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/Html/PreambleTemplate.cshtml +++ b/DiscordChatExporter.Domain/Exporting/Writers/Html/PreambleTemplate.cshtml @@ -83,15 +83,15 @@
@if (Model.ExportContext.Request.After != null && Model.ExportContext.Request.Before != null) { - @($"Between {FormatDate(Model.ExportContext.Request.After.Value)} and {FormatDate(Model.ExportContext.Request.Before.Value)}") + @($"Between {FormatDate(Model.ExportContext.Request.After.Value.ToDate())} and {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}") } else if (Model.ExportContext.Request.After != null) { - @($"After {FormatDate(Model.ExportContext.Request.After.Value)}") + @($"After {FormatDate(Model.ExportContext.Request.After.Value.ToDate())}") } else if (Model.ExportContext.Request.Before != null) { - @($"Before {FormatDate(Model.ExportContext.Request.Before.Value)}") + @($"Before {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}") }
} diff --git a/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs b/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs index fa26700..4c13c4f 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/JsonMessageWriter.cs @@ -32,7 +32,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers { _writer.WriteStartObject(); - _writer.WriteString("id", attachment.Id); + _writer.WriteString("id", attachment.Id.ToString()); _writer.WriteString("url", await Context.ResolveMediaUrlAsync(attachment.Url)); _writer.WriteString("fileName", attachment.FileName); _writer.WriteNumber("fileSizeBytes", attachment.FileSize.TotalBytes); @@ -166,7 +166,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers { _writer.WriteStartObject(); - _writer.WriteString("id", mentionedUser.Id); + _writer.WriteString("id", mentionedUser.Id.ToString()); _writer.WriteString("name", mentionedUser.Name); _writer.WriteNumber("discriminator", mentionedUser.Discriminator); _writer.WriteString("nickname", Context.TryGetMember(mentionedUser.Id)?.Nick ?? mentionedUser.Name); @@ -183,14 +183,14 @@ namespace DiscordChatExporter.Domain.Exporting.Writers // Guild _writer.WriteStartObject("guild"); - _writer.WriteString("id", Context.Request.Guild.Id); + _writer.WriteString("id", Context.Request.Guild.Id.ToString()); _writer.WriteString("name", Context.Request.Guild.Name); _writer.WriteString("iconUrl", await Context.ResolveMediaUrlAsync(Context.Request.Guild.IconUrl)); _writer.WriteEndObject(); // Channel _writer.WriteStartObject("channel"); - _writer.WriteString("id", Context.Request.Channel.Id); + _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("name", Context.Request.Channel.Name); @@ -199,8 +199,8 @@ namespace DiscordChatExporter.Domain.Exporting.Writers // Date range _writer.WriteStartObject("dateRange"); - _writer.WriteString("after", Context.Request.After); - _writer.WriteString("before", Context.Request.Before); + _writer.WriteString("after", Context.Request.After?.ToDate()); + _writer.WriteString("before", Context.Request.Before?.ToDate()); _writer.WriteEndObject(); // Message array (start) @@ -213,7 +213,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers _writer.WriteStartObject(); // Metadata - _writer.WriteString("id", message.Id); + _writer.WriteString("id", message.Id.ToString()); _writer.WriteString("type", message.Type.ToString()); _writer.WriteString("timestamp", message.Timestamp); _writer.WriteString("timestampEdited", message.EditedTimestamp); @@ -225,7 +225,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers // Author _writer.WriteStartObject("author"); - _writer.WriteString("id", message.Author.Id); + _writer.WriteString("id", message.Author.Id.ToString()); _writer.WriteString("name", message.Author.Name); _writer.WriteString("discriminator", $"{message.Author.Discriminator:0000}"); _writer.WriteBoolean("isBot", message.Author.IsBot); diff --git a/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs b/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs index 5f4b3ed..092534c 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/HtmlMarkdownVisitor.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; +using DiscordChatExporter.Domain.Discord; using DiscordChatExporter.Domain.Discord.Models; using DiscordChatExporter.Domain.Markdown; using DiscordChatExporter.Domain.Markdown.Ast; @@ -84,7 +85,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors } else if (mention.Type == MentionType.User) { - var member = _context.TryGetMember(mention.Id); + var member = _context.TryGetMember(Snowflake.Parse(mention.Id)); var fullName = member?.User.FullName ?? "Unknown"; var nick = member?.Nick ?? "Unknown"; @@ -95,7 +96,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors } else if (mention.Type == MentionType.Channel) { - var channel = _context.TryGetChannel(mention.Id); + var channel = _context.TryGetChannel(Snowflake.Parse(mention.Id)); var name = channel?.Name ?? "deleted-channel"; _buffer @@ -105,7 +106,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors } else if (mention.Type == MentionType.Role) { - var role = _context.TryGetRole(mention.Id); + var role = _context.TryGetRole(Snowflake.Parse(mention.Id)); var name = role?.Name ?? "deleted-role"; var color = role?.Color; diff --git a/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs b/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs index 505783a..7e40556 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/MarkdownVisitors/PlainTextMarkdownVisitor.cs @@ -1,4 +1,5 @@ using System.Text; +using DiscordChatExporter.Domain.Discord; using DiscordChatExporter.Domain.Markdown; using DiscordChatExporter.Domain.Markdown.Ast; @@ -29,21 +30,21 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors } else if (mention.Type == MentionType.User) { - var member = _context.TryGetMember(mention.Id); + var member = _context.TryGetMember(Snowflake.Parse(mention.Id)); var name = member?.User.Name ?? "Unknown"; _buffer.Append($"@{name}"); } else if (mention.Type == MentionType.Channel) { - var channel = _context.TryGetChannel(mention.Id); + var channel = _context.TryGetChannel(Snowflake.Parse(mention.Id)); var name = channel?.Name ?? "deleted-channel"; _buffer.Append($"#{name}"); } else if (mention.Type == MentionType.Role) { - var role = _context.TryGetRole(mention.Id); + var role = _context.TryGetRole(Snowflake.Parse(mention.Id)); var name = role?.Name ?? "deleted-role"; _buffer.Append($"@{name}"); diff --git a/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs b/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs index 9347722..6f25999 100644 --- a/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs +++ b/DiscordChatExporter.Domain/Exporting/Writers/PlainTextMessageWriter.cs @@ -119,10 +119,10 @@ namespace DiscordChatExporter.Domain.Exporting.Writers await _writer.WriteLineAsync($"Topic: {Context.Request.Channel.Topic}"); if (Context.Request.After != null) - await _writer.WriteLineAsync($"After: {Context.FormatDate(Context.Request.After.Value)}"); + await _writer.WriteLineAsync($"After: {Context.FormatDate(Context.Request.After.Value.ToDate())}"); if (Context.Request.Before != null) - await _writer.WriteLineAsync($"Before: {Context.FormatDate(Context.Request.Before.Value)}"); + await _writer.WriteLineAsync($"Before: {Context.FormatDate(Context.Request.Before.Value.ToDate())}"); await _writer.WriteLineAsync('='.Repeat(62)); await _writer.WriteLineAsync(); diff --git a/DiscordChatExporter.Domain/Internal/Extensions/DateExtensions.cs b/DiscordChatExporter.Domain/Internal/Extensions/DateExtensions.cs index 857dfe9..384844a 100644 --- a/DiscordChatExporter.Domain/Internal/Extensions/DateExtensions.cs +++ b/DiscordChatExporter.Domain/Internal/Extensions/DateExtensions.cs @@ -5,12 +5,6 @@ namespace DiscordChatExporter.Domain.Internal.Extensions { internal static class DateExtensions { - public static string ToSnowflake(this DateTimeOffset dateTime) - { - var value = ((ulong) dateTime.ToUnixTimeMilliseconds() - 1420070400000UL) << 22; - return value.ToString(); - } - public static string ToLocalString(this DateTimeOffset dateTime, string format) => dateTime.ToLocalTime().ToString(format, CultureInfo.InvariantCulture); } diff --git a/DiscordChatExporter.Domain/Internal/Extensions/GenericExtensions.cs b/DiscordChatExporter.Domain/Internal/Extensions/GenericExtensions.cs index f11d4d0..a68406a 100644 --- a/DiscordChatExporter.Domain/Internal/Extensions/GenericExtensions.cs +++ b/DiscordChatExporter.Domain/Internal/Extensions/GenericExtensions.cs @@ -4,8 +4,6 @@ namespace DiscordChatExporter.Domain.Internal.Extensions { internal static class GenericExtensions { - public static TOut Pipe(this TIn input, Func transform) => transform(input); - public static T? NullIf(this T value, Func predicate) where T : struct => !predicate(value) ? value diff --git a/DiscordChatExporter.Domain/Utilities/GeneralExtensions.cs b/DiscordChatExporter.Domain/Utilities/GeneralExtensions.cs new file mode 100644 index 0000000..1af67ce --- /dev/null +++ b/DiscordChatExporter.Domain/Utilities/GeneralExtensions.cs @@ -0,0 +1,9 @@ +using System; + +namespace DiscordChatExporter.Domain.Utilities +{ + public static class GeneralExtensions + { + public static TOut Pipe(this TIn input, Func transform) => transform(input); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs index 37e3a60..7fa9f2a 100644 --- a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using DiscordChatExporter.Domain.Discord; using DiscordChatExporter.Domain.Discord.Models; using DiscordChatExporter.Domain.Exporting; +using DiscordChatExporter.Domain.Utilities; using DiscordChatExporter.Gui.Services; using DiscordChatExporter.Gui.ViewModels.Framework; @@ -82,8 +84,8 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs Guild!, channel, SelectedFormat, - After, - Before + After?.Pipe(Snowflake.FromDate), + Before?.Pipe(Snowflake.FromDate) ); // Filter diff --git a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs index 754fc77..8d07810 100644 --- a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs @@ -211,8 +211,8 @@ namespace DiscordChatExporter.Gui.ViewModels channel!, dialog.OutputPath!, dialog.SelectedFormat, - dialog.After, - dialog.Before, + dialog.After?.Pipe(Snowflake.FromDate), + dialog.Before?.Pipe(Snowflake.FromDate), dialog.PartitionLimit, dialog.ShouldDownloadMedia, _settingsService.ShouldReuseMedia,