From cf83cbd89c8a1491afec2523da06f688d11a2c6e Mon Sep 17 00:00:00 2001 From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> Date: Sat, 5 Feb 2022 16:26:23 -0800 Subject: [PATCH] Add support for stickers (#802) --- .../Specs/HtmlWriting/StickerSpecs.cs | 45 ++++++++++++++++ .../Specs/JsonWriting/StickerSpecs.cs | 54 +++++++++++++++++++ .../Specs/PartitioningSpecs.cs | 2 +- .../TestData/ChannelIds.cs | 2 + .../Discord/Data/Message.cs | 6 +++ .../Discord/Data/Sticker.cs | 25 +++++++++ .../Discord/Data/StickerFormat.cs | 8 +++ .../Writers/Html/MessageGroupTemplate.cshtml | 16 ++++++ .../Writers/Html/PreambleTemplate.cshtml | 34 +++++++++++- .../Exporting/Writers/JsonMessageWriter.cs | 23 ++++++++ .../Writers/PlainTextMessageWriter.cs | 22 +++++++- 11 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 DiscordChatExporter.Cli.Tests/Specs/HtmlWriting/StickerSpecs.cs create mode 100644 DiscordChatExporter.Cli.Tests/Specs/JsonWriting/StickerSpecs.cs create mode 100644 DiscordChatExporter.Core/Discord/Data/Sticker.cs create mode 100644 DiscordChatExporter.Core/Discord/Data/StickerFormat.cs diff --git a/DiscordChatExporter.Cli.Tests/Specs/HtmlWriting/StickerSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/HtmlWriting/StickerSpecs.cs new file mode 100644 index 0000000..e4f3b19 --- /dev/null +++ b/DiscordChatExporter.Cli.Tests/Specs/HtmlWriting/StickerSpecs.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using DiscordChatExporter.Cli.Tests.Fixtures; +using DiscordChatExporter.Cli.Tests.TestData; +using DiscordChatExporter.Core.Discord; +using FluentAssertions; +using Xunit; + +namespace DiscordChatExporter.Cli.Tests.Specs.HtmlWriting; + +public record StickerSpecs(ExportWrapperFixture ExportWrapper) : IClassFixture +{ + [Fact] + public async Task Message_with_a_PNG_based_sticker_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsHtmlAsync( + ChannelIds.StickerTestCases, + Snowflake.Parse("939670623158943754") + ); + + var container = message.QuerySelector("[title='rock']"); + var sourceUrl = container?.QuerySelector("img")?.GetAttribute("src"); + + // Assert + container.Should().NotBeNull(); + sourceUrl.Should().Be("https://discord.com/stickers/904215665597120572.png"); + } + + [Fact] + public async Task Message_with_a_Lottie_based_sticker_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsHtmlAsync( + ChannelIds.StickerTestCases, + Snowflake.Parse("939670526517997590") + ); + + var container = message.QuerySelector("[title='Yikes']"); + var sourceUrl = container?.QuerySelector("div[data-source]")?.GetAttribute("data-source"); + + // Assert + container.Should().NotBeNull(); + sourceUrl.Should().Be("https://discord.com/stickers/816087132447178774.json"); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli.Tests/Specs/JsonWriting/StickerSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/JsonWriting/StickerSpecs.cs new file mode 100644 index 0000000..93138a1 --- /dev/null +++ b/DiscordChatExporter.Cli.Tests/Specs/JsonWriting/StickerSpecs.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Threading.Tasks; +using DiscordChatExporter.Cli.Tests.Fixtures; +using DiscordChatExporter.Cli.Tests.TestData; +using DiscordChatExporter.Core.Discord; +using FluentAssertions; +using Xunit; + +namespace DiscordChatExporter.Cli.Tests.Specs.JsonWriting; + +public record StickerSpecs(ExportWrapperFixture ExportWrapper) : IClassFixture +{ + [Fact] + public async Task Message_with_a_PNG_based_sticker_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsJsonAsync( + ChannelIds.StickerTestCases, + Snowflake.Parse("939670623158943754") + ); + + var sticker = message + .GetProperty("stickers") + .EnumerateArray() + .Single(); + + // Assert + sticker.GetProperty("id").GetString().Should().Be("904215665597120572"); + sticker.GetProperty("name").GetString().Should().Be("rock"); + sticker.GetProperty("format").GetString().Should().Be("PngAnimated"); + sticker.GetProperty("sourceUrl").GetString().Should().Be("https://discord.com/stickers/904215665597120572.png"); + } + + [Fact] + public async Task Message_with_a_Lottie_based_sticker_is_rendered_correctly() + { + // Act + var message = await ExportWrapper.GetMessageAsJsonAsync( + ChannelIds.StickerTestCases, + Snowflake.Parse("939670526517997590") + ); + + var sticker = message + .GetProperty("stickers") + .EnumerateArray() + .Single(); + + // Assert + sticker.GetProperty("id").GetString().Should().Be("816087132447178774"); + sticker.GetProperty("name").GetString().Should().Be("Yikes"); + sticker.GetProperty("format").GetString().Should().Be("Lottie"); + sticker.GetProperty("sourceUrl").GetString().Should().Be("https://discord.com/stickers/816087132447178774.json"); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Cli.Tests/Specs/PartitioningSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/PartitioningSpecs.cs index 6a64778..bff1920 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/PartitioningSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/PartitioningSpecs.cs @@ -59,6 +59,6 @@ public record PartitioningSpecs(TempOutputFixture TempOutput) : IClassFixture Attachments, IReadOnlyList Embeds, + IReadOnlyList Stickers, IReadOnlyList Reactions, IReadOnlyList MentionedUsers, MessageReference? Reference, @@ -60,6 +61,10 @@ public record Message( json.GetPropertyOrNull("embeds")?.EnumerateArrayOrNull()?.Select(Embed.Parse).ToArray() ?? Array.Empty(); + var stickers = + json.GetPropertyOrNull("sticker_items")?.EnumerateArrayOrNull()?.Select(Sticker.Parse).ToArray() ?? + Array.Empty(); + var reactions = json.GetPropertyOrNull("reactions")?.EnumerateArrayOrNull()?.Select(Reaction.Parse).ToArray() ?? Array.Empty(); @@ -79,6 +84,7 @@ public record Message( content, attachments, embeds, + stickers, reactions, mentionedUsers, messageReference, diff --git a/DiscordChatExporter.Core/Discord/Data/Sticker.cs b/DiscordChatExporter.Core/Discord/Data/Sticker.cs new file mode 100644 index 0000000..5274f46 --- /dev/null +++ b/DiscordChatExporter.Core/Discord/Data/Sticker.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +using DiscordChatExporter.Core.Utils.Extensions; +using JsonExtensions.Reading; + +namespace DiscordChatExporter.Core.Discord.Data; + +public record Sticker(Snowflake Id, string Name, StickerFormat Format, string SourceUrl) +{ + private static string GetSourceUrl(Snowflake id, StickerFormat format) + { + var extension = format == StickerFormat.Lottie ? "json" : "png"; + return $"https://discord.com/stickers/{id}.{extension}"; + } + + public static Sticker Parse(JsonElement json) + { + var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); + var name = json.GetProperty("name").GetNonWhiteSpaceString(); + var format = (StickerFormat)json.GetProperty("format_type").GetInt32(); + + var sourceUrl = GetSourceUrl(id, format); + + return new Sticker(id, name, format, sourceUrl); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/Data/StickerFormat.cs b/DiscordChatExporter.Core/Discord/Data/StickerFormat.cs new file mode 100644 index 0000000..12ffde3 --- /dev/null +++ b/DiscordChatExporter.Core/Discord/Data/StickerFormat.cs @@ -0,0 +1,8 @@ +namespace DiscordChatExporter.Core.Discord.Data; + +public enum StickerFormat +{ + Png = 1, + PngAnimated = 2, + Lottie = 3 +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml b/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml index 07c14ba..84a99e9 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml @@ -1,6 +1,7 @@ @using System @using System.Linq @using System.Threading.Tasks +@using DiscordChatExporter.Core.Discord.Data @using DiscordChatExporter.Core.Exporting.Writers.Html; @namespace DiscordChatExporter.Core.Exporting.Writers.Html @@ -411,6 +412,21 @@ } } + @{/* Stickers */} + @foreach (var sticker in message.Stickers) + { +
+ @if (sticker.Format is StickerFormat.Png or StickerFormat.PngAnimated) + { + Sticker + } + else if (sticker.Format == StickerFormat.Lottie) + { +
+ } +
+ } + @{/* Message reactions */} @if (message.Reactions.Any()) { diff --git a/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml b/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml index a98ac63..d4d656c 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml @@ -597,6 +597,16 @@ border-radius: 3px; } + .chatlog__sticker { + width: 180px; + height: 180px; + } + + .chatlog__sticker--media { + max-width: 100%; + max-height: 100%; + } + .chatlog__reactions { display: flex; } @@ -660,7 +670,29 @@ + + @{/* Lottie animation support */} + + diff --git a/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs b/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs index 9d68d76..3c523bf 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/JsonMessageWriter.cs @@ -161,6 +161,21 @@ internal class JsonMessageWriter : MessageWriter await _writer.FlushAsync(cancellationToken); } + private async ValueTask WriteStickerAsync( + Sticker sticker, + CancellationToken cancellationToken = default) + { + _writer.WriteStartObject(); + + _writer.WriteString("id", sticker.Id.ToString()); + _writer.WriteString("name", sticker.Name); + _writer.WriteString("format", sticker.Format.ToString()); + _writer.WriteString("sourceUrl", await Context.ResolveMediaUrlAsync(sticker.SourceUrl, cancellationToken)); + + _writer.WriteEndObject(); + await _writer.FlushAsync(cancellationToken); + } + private async ValueTask WriteReactionAsync( Reaction reaction, CancellationToken cancellationToken = default) @@ -276,6 +291,14 @@ internal class JsonMessageWriter : MessageWriter _writer.WriteEndArray(); + // Stickers + _writer.WriteStartArray("stickers"); + + foreach (var sticker in message.Stickers) + await WriteStickerAsync(sticker, cancellationToken); + + _writer.WriteEndArray(); + // Reactions _writer.WriteStartArray("reactions"); diff --git a/DiscordChatExporter.Core/Exporting/Writers/PlainTextMessageWriter.cs b/DiscordChatExporter.Core/Exporting/Writers/PlainTextMessageWriter.cs index 6ecf413..cee268f 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/PlainTextMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/PlainTextMessageWriter.cs @@ -98,6 +98,25 @@ internal class PlainTextMessageWriter : MessageWriter } } + private async ValueTask WriteStickersAsync( + IReadOnlyList stickers, + CancellationToken cancellationToken = default) + { + if (!stickers.Any()) + return; + + await _writer.WriteLineAsync("{Stickers}"); + + foreach (var sticker in stickers) + { + cancellationToken.ThrowIfCancellationRequested(); + + await _writer.WriteLineAsync(await Context.ResolveMediaUrlAsync(sticker.SourceUrl, cancellationToken)); + } + + await _writer.WriteLineAsync(); + } + private async ValueTask WriteReactionsAsync( IReadOnlyList reactions, CancellationToken cancellationToken = default) @@ -156,9 +175,10 @@ internal class PlainTextMessageWriter : MessageWriter await _writer.WriteLineAsync(); - // Attachments, embeds, reactions + // Attachments, embeds, reactions, etc. await WriteAttachmentsAsync(message.Attachments, cancellationToken); await WriteEmbedsAsync(message.Embeds, cancellationToken); + await WriteStickersAsync(message.Stickers, cancellationToken); await WriteReactionsAsync(message.Reactions, cancellationToken); await _writer.WriteLineAsync();