mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-02-01 15:49:11 +00:00
Add support for stickers (#802)
This commit is contained in:
@@ -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<ExportWrapperFixture>
|
||||
{
|
||||
[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");
|
||||
}
|
||||
}
|
||||
@@ -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<ExportWrapperFixture>
|
||||
{
|
||||
[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");
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,6 @@ public record PartitioningSpecs(TempOutputFixture TempOutput) : IClassFixture<Te
|
||||
// Assert
|
||||
Directory.EnumerateFiles(dirPath, fileNameWithoutExt + "*")
|
||||
.Should()
|
||||
.HaveCount(2);
|
||||
.HaveCount(3);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ public static class ChannelIds
|
||||
|
||||
public static Snowflake EmbedTestCases { get; } = Snowflake.Parse("866472452459462687");
|
||||
|
||||
public static Snowflake StickerTestCases { get; } = Snowflake.Parse("939668868253769729");
|
||||
|
||||
public static Snowflake FilterTestCases { get; } = Snowflake.Parse("866744075033641020");
|
||||
|
||||
public static Snowflake MentionTestCases { get; } = Snowflake.Parse("866458801389174794");
|
||||
|
||||
@@ -21,6 +21,7 @@ public record Message(
|
||||
string Content,
|
||||
IReadOnlyList<Attachment> Attachments,
|
||||
IReadOnlyList<Embed> Embeds,
|
||||
IReadOnlyList<Sticker> Stickers,
|
||||
IReadOnlyList<Reaction> Reactions,
|
||||
IReadOnlyList<User> MentionedUsers,
|
||||
MessageReference? Reference,
|
||||
@@ -60,6 +61,10 @@ public record Message(
|
||||
json.GetPropertyOrNull("embeds")?.EnumerateArrayOrNull()?.Select(Embed.Parse).ToArray() ??
|
||||
Array.Empty<Embed>();
|
||||
|
||||
var stickers =
|
||||
json.GetPropertyOrNull("sticker_items")?.EnumerateArrayOrNull()?.Select(Sticker.Parse).ToArray() ??
|
||||
Array.Empty<Sticker>();
|
||||
|
||||
var reactions =
|
||||
json.GetPropertyOrNull("reactions")?.EnumerateArrayOrNull()?.Select(Reaction.Parse).ToArray() ??
|
||||
Array.Empty<Reaction>();
|
||||
@@ -79,6 +84,7 @@ public record Message(
|
||||
content,
|
||||
attachments,
|
||||
embeds,
|
||||
stickers,
|
||||
reactions,
|
||||
mentionedUsers,
|
||||
messageReference,
|
||||
|
||||
25
DiscordChatExporter.Core/Discord/Data/Sticker.cs
Normal file
25
DiscordChatExporter.Core/Discord/Data/Sticker.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
8
DiscordChatExporter.Core/Discord/Data/StickerFormat.cs
Normal file
8
DiscordChatExporter.Core/Discord/Data/StickerFormat.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
public enum StickerFormat
|
||||
{
|
||||
Png = 1,
|
||||
PngAnimated = 2,
|
||||
Lottie = 3
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
<div class="chatlog__sticker" title="@sticker.Name">
|
||||
@if (sticker.Format is StickerFormat.Png or StickerFormat.PngAnimated)
|
||||
{
|
||||
<img class="chatlog__sticker--media" src="@(await ResolveUrlAsync(sticker.SourceUrl))" alt="Sticker">
|
||||
}
|
||||
else if (sticker.Format == StickerFormat.Lottie)
|
||||
{
|
||||
<div class="chatlog__sticker--media" data-source="@(await ResolveUrlAsync(sticker.SourceUrl))"></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@{/* Message reactions */}
|
||||
@if (message.Reactions.Any())
|
||||
{
|
||||
|
||||
@@ -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 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.pre--multiline').forEach(block => hljs.highlightBlock(block));
|
||||
document.querySelectorAll('.pre--multiline').forEach(e => hljs.highlightBlock(e));
|
||||
});
|
||||
</script>
|
||||
|
||||
@{/* Lottie animation support */}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.8.1/lottie.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.chatlog__sticker--media[data-source]').forEach(e => {
|
||||
const imageDataUrl = e.getAttribute('data-source');
|
||||
|
||||
const anim = lottie.loadAnimation({
|
||||
container: e,
|
||||
renderer: 'svg',
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
path: imageDataUrl
|
||||
});
|
||||
|
||||
anim.addEventListener('data_failed', () =>
|
||||
e.innerHTML = '<strong>[Sticker cannot be rendered]</strong>'
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -98,6 +98,25 @@ internal class PlainTextMessageWriter : MessageWriter
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask WriteStickersAsync(
|
||||
IReadOnlyList<Sticker> 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<Reaction> 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();
|
||||
|
||||
Reference in New Issue
Block a user