diff --git a/app/Server/Database/Export/Snowflake.cs b/app/Server/Database/Export/Snowflake.cs new file mode 100644 index 0000000..17ffc62 --- /dev/null +++ b/app/Server/Database/Export/Snowflake.cs @@ -0,0 +1,3 @@ +namespace DHT.Server.Database.Export; + +readonly record struct Snowflake(ulong Id); diff --git a/app/Server/Database/Export/SnowflakeJsonSerializer.cs b/app/Server/Database/Export/SnowflakeJsonSerializer.cs new file mode 100644 index 0000000..8312709 --- /dev/null +++ b/app/Server/Database/Export/SnowflakeJsonSerializer.cs @@ -0,0 +1,23 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DHT.Server.Database.Export; + +sealed class SnowflakeJsonSerializer : JsonConverter { + public override Snowflake Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + return new Snowflake(ulong.Parse(reader.GetString()!)); + } + + public override void Write(Utf8JsonWriter writer, Snowflake value, JsonSerializerOptions options) { + writer.WriteStringValue(value.Id.ToString()); + } + + public override Snowflake ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + return new Snowflake(ulong.Parse(reader.GetString()!)); + } + + public override void WriteAsPropertyName(Utf8JsonWriter writer, Snowflake value, JsonSerializerOptions options) { + writer.WritePropertyName(value.Id.ToString()); + } +} diff --git a/app/Server/Database/Export/ViewerJson.cs b/app/Server/Database/Export/ViewerJson.cs new file mode 100644 index 0000000..faf6ef8 --- /dev/null +++ b/app/Server/Database/Export/ViewerJson.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace DHT.Server.Database.Export; + +sealed class ViewerJson { + public required JsonMeta Meta { get; init; } + public required Dictionary> Data { get; init; } + + public sealed class JsonMeta { + public required Dictionary Users { get; init; } + public required List Userindex { get; init; } + public required List Servers { get; init; } + public required Dictionary Channels { get; init; } + } + + public sealed class JsonUser { + public required string Name { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Avatar { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Tag { get; init; } + } + + public sealed class JsonServer { + public required string Name { get; init; } + public required string Type { get; init; } + } + + public sealed class JsonChannel { + public required int Server { get; init; } + public required string Name { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Parent { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Position { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Topic { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Nsfw { get; init; } + } + + public sealed class JsonMessage { + public required int U { get; init; } + public required long T { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? M { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public long? Te { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? R { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonMessageAttachment[]? A { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[]? E { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonMessageReaction[]? Re { get; init; } + } + + public sealed class JsonMessageAttachment { + public required string Url { get; init; } + public required string Name { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Width { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Height { get; set; } + } + + public sealed class JsonMessageReaction { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Id { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? N { get; init; } + + public required bool A { get; init; } + public required int C { get; init; } + } +} diff --git a/app/Server/Database/Export/ViewerJsonContext.cs b/app/Server/Database/Export/ViewerJsonContext.cs new file mode 100644 index 0000000..341ab54 --- /dev/null +++ b/app/Server/Database/Export/ViewerJsonContext.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace DHT.Server.Database.Export; + +[JsonSourceGenerationOptions( + Converters = new [] { typeof(SnowflakeJsonSerializer) }, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + GenerationMode = JsonSourceGenerationMode.Default +)] +[JsonSerializable(typeof(ViewerJson))] +sealed partial class ViewerJsonContext : JsonSerializerContext {} diff --git a/app/Server/Database/Export/ViewerJsonExport.cs b/app/Server/Database/Export/ViewerJsonExport.cs index 609638d..decee50 100644 --- a/app/Server/Database/Export/ViewerJsonExport.cs +++ b/app/Server/Database/Export/ViewerJsonExport.cs @@ -42,26 +42,28 @@ public static class ViewerJsonExport { perf.Step("Collect database data"); - var value = new { - meta = new { users, userindex, servers, channels }, - data = GenerateMessageList(includedMessages, userIndices, strategy), + var value = new ViewerJson { + Meta = new ViewerJson.JsonMeta { + Users = users, + Userindex = userindex, + Servers = servers, + Channels = channels + }, + Data = GenerateMessageList(includedMessages, userIndices, strategy) }; perf.Step("Generate value object"); - var opts = new JsonSerializerOptions(); - opts.Converters.Add(new ViewerJsonSnowflakeSerializer()); - - await JsonSerializer.SerializeAsync(stream, value, opts); + await JsonSerializer.SerializeAsync(stream, value, ViewerJsonContext.Default.ViewerJson); perf.Step("Serialize to JSON"); perf.End(); } - private static Dictionary GenerateUserList(IDatabaseFile db, HashSet userIds, out List userindex, out Dictionary userIndices) { - var users = new Dictionary(); - userindex = new List(); - userIndices = new Dictionary(); + private static Dictionary GenerateUserList(IDatabaseFile db, HashSet userIds, out List userindex, out Dictionary userIndices) { + var users = new Dictionary(); + userindex = new List(); + userIndices = new Dictionary(); foreach (var user in db.GetAllUsers()) { var id = user.Id; @@ -69,30 +71,23 @@ public static class ViewerJsonExport { continue; } - var obj = new Dictionary { - ["name"] = user.Name - }; - - if (user.AvatarUrl != null) { - obj["avatar"] = user.AvatarUrl; - } - - if (user.Discriminator != null) { - obj["tag"] = user.Discriminator; - } - - var idStr = id.ToString(); + var idSnowflake = new Snowflake(id); userIndices[id] = users.Count; - userindex.Add(idStr); - users[idStr] = obj; + userindex.Add(idSnowflake); + + users[idSnowflake] = new ViewerJson.JsonUser { + Name = user.Name, + Avatar = user.AvatarUrl, + Tag = user.Discriminator + }; } return users; } - private static List GenerateServerList(IDatabaseFile db, HashSet serverIds, out Dictionary serverIndices) { - var servers = new List(); - serverIndices = new Dictionary(); + private static List GenerateServerList(IDatabaseFile db, HashSet serverIds, out Dictionary serverIndices) { + var servers = new List(); + serverIndices = new Dictionary(); foreach (var server in db.GetAllServers()) { var id = server.Id; @@ -101,113 +96,78 @@ public static class ViewerJsonExport { } serverIndices[id] = servers.Count; - servers.Add(new Dictionary { - ["name"] = server.Name, - ["type"] = ServerTypes.ToJsonViewerString(server.Type), + + servers.Add(new ViewerJson.JsonServer { + Name = server.Name, + Type = ServerTypes.ToJsonViewerString(server.Type) }); } return servers; } - private static Dictionary GenerateChannelList(List includedChannels, Dictionary serverIndices) { - var channels = new Dictionary(); + private static Dictionary GenerateChannelList(List includedChannels, Dictionary serverIndices) { + var channels = new Dictionary(); foreach (var channel in includedChannels) { - var obj = new Dictionary { - ["server"] = serverIndices[channel.Server], - ["name"] = channel.Name, + var channelIdSnowflake = new Snowflake(channel.Id); + + channels[channelIdSnowflake] = new ViewerJson.JsonChannel { + Server = serverIndices[channel.Server], + Name = channel.Name, + Parent = channel.ParentId?.ToString(), + Position = channel.Position, + Topic = channel.Topic, + Nsfw = channel.Nsfw }; - - if (channel.ParentId != null) { - obj["parent"] = channel.ParentId; - } - - if (channel.Position != null) { - obj["position"] = channel.Position; - } - - if (channel.Topic != null) { - obj["topic"] = channel.Topic; - } - - if (channel.Nsfw != null) { - obj["nsfw"] = channel.Nsfw; - } - - channels[channel.Id.ToString()] = obj; } return channels; } - private static Dictionary> GenerateMessageList( List includedMessages, Dictionary userIndices, IViewerExportStrategy strategy) { - var data = new Dictionary>(); + private static Dictionary> GenerateMessageList(List includedMessages, Dictionary userIndices, IViewerExportStrategy strategy) { + var data = new Dictionary>(); foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) { - var channel = grouping.Key.ToString(); - var channelData = new Dictionary(); + var channelIdSnowflake = new Snowflake(grouping.Key); + var channelData = new Dictionary(); foreach (var message in grouping) { - var obj = new Dictionary { - ["u"] = userIndices[message.Sender], - ["t"] = message.Timestamp, - }; - - if (!string.IsNullOrEmpty(message.Text)) { - obj["m"] = message.Text; - } - - if (message.EditTimestamp != null) { - obj["te"] = message.EditTimestamp; - } - - if (message.RepliedToId != null) { - obj["r"] = message.RepliedToId.Value; - } - - if (!message.Attachments.IsEmpty) { - obj["a"] = message.Attachments.Select(attachment => { - var a = new Dictionary { - { "url", strategy.GetAttachmentUrl(attachment) }, - { "name", Uri.TryCreate(attachment.NormalizedUrl, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.NormalizedUrl }, + var messageIdSnowflake = new Snowflake(message.Id); + + channelData[messageIdSnowflake] = new ViewerJson.JsonMessage { + U = userIndices[message.Sender], + T = message.Timestamp, + M = string.IsNullOrEmpty(message.Text) ? null : message.Text, + Te = message.EditTimestamp, + R = message.RepliedToId?.ToString(), + + A = message.Attachments.IsEmpty ? null : message.Attachments.Select(attachment => { + var a = new ViewerJson.JsonMessageAttachment { + Url = strategy.GetAttachmentUrl(attachment), + Name = Uri.TryCreate(attachment.NormalizedUrl, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.NormalizedUrl }; if (attachment is { Width: not null, Height: not null }) { - a["width"] = attachment.Width; - a["height"] = attachment.Height; + a.Width = attachment.Width; + a.Height = attachment.Height; } return a; - }).ToArray(); - } - - if (!message.Embeds.IsEmpty) { - obj["e"] = message.Embeds.Select(static embed => embed.Json).ToArray(); - } - - if (!message.Reactions.IsEmpty) { - obj["re"] = message.Reactions.Select(static reaction => { - var r = new Dictionary(); - - if (reaction.EmojiId != null) { - r["id"] = reaction.EmojiId.Value; - } - - if (reaction.EmojiName != null) { - r["n"] = reaction.EmojiName; - } - - r["a"] = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated); - r["c"] = reaction.Count; - return r; - }).ToArray(); - } - - channelData[message.Id.ToString()] = obj; + }).ToArray(), + + E = message.Embeds.IsEmpty ? null : message.Embeds.Select(static embed => embed.Json).ToArray(), + + Re = message.Reactions.IsEmpty ? null : message.Reactions.Select(static reaction => new ViewerJson.JsonMessageReaction { + Id = reaction.EmojiId?.ToString(), + N = reaction.EmojiName, + A = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated), + C = reaction.Count + }).ToArray() + }; } - data[channel] = channelData; + data[channelIdSnowflake] = channelData; } return data; diff --git a/app/Server/Database/Export/ViewerJsonSnowflakeSerializer.cs b/app/Server/Database/Export/ViewerJsonSnowflakeSerializer.cs deleted file mode 100644 index 492be0f..0000000 --- a/app/Server/Database/Export/ViewerJsonSnowflakeSerializer.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace DHT.Server.Database.Export; - -sealed class ViewerJsonSnowflakeSerializer : JsonConverter { - public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return ulong.Parse(reader.GetString()!); - } - - public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options) { - writer.WriteStringValue(value.ToString()); - } -}