mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-02-14 15:53:30 -07:00
Add support for stickers (#802)
This commit is contained in:
parent
d51d0d4872
commit
cf83cbd89c
|
|
@ -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
|
// Assert
|
||||||
Directory.EnumerateFiles(dirPath, fileNameWithoutExt + "*")
|
Directory.EnumerateFiles(dirPath, fileNameWithoutExt + "*")
|
||||||
.Should()
|
.Should()
|
||||||
.HaveCount(2);
|
.HaveCount(3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +10,8 @@ public static class ChannelIds
|
||||||
|
|
||||||
public static Snowflake EmbedTestCases { get; } = Snowflake.Parse("866472452459462687");
|
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 FilterTestCases { get; } = Snowflake.Parse("866744075033641020");
|
||||||
|
|
||||||
public static Snowflake MentionTestCases { get; } = Snowflake.Parse("866458801389174794");
|
public static Snowflake MentionTestCases { get; } = Snowflake.Parse("866458801389174794");
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ public record Message(
|
||||||
string Content,
|
string Content,
|
||||||
IReadOnlyList<Attachment> Attachments,
|
IReadOnlyList<Attachment> Attachments,
|
||||||
IReadOnlyList<Embed> Embeds,
|
IReadOnlyList<Embed> Embeds,
|
||||||
|
IReadOnlyList<Sticker> Stickers,
|
||||||
IReadOnlyList<Reaction> Reactions,
|
IReadOnlyList<Reaction> Reactions,
|
||||||
IReadOnlyList<User> MentionedUsers,
|
IReadOnlyList<User> MentionedUsers,
|
||||||
MessageReference? Reference,
|
MessageReference? Reference,
|
||||||
|
|
@ -60,6 +61,10 @@ public record Message(
|
||||||
json.GetPropertyOrNull("embeds")?.EnumerateArrayOrNull()?.Select(Embed.Parse).ToArray() ??
|
json.GetPropertyOrNull("embeds")?.EnumerateArrayOrNull()?.Select(Embed.Parse).ToArray() ??
|
||||||
Array.Empty<Embed>();
|
Array.Empty<Embed>();
|
||||||
|
|
||||||
|
var stickers =
|
||||||
|
json.GetPropertyOrNull("sticker_items")?.EnumerateArrayOrNull()?.Select(Sticker.Parse).ToArray() ??
|
||||||
|
Array.Empty<Sticker>();
|
||||||
|
|
||||||
var reactions =
|
var reactions =
|
||||||
json.GetPropertyOrNull("reactions")?.EnumerateArrayOrNull()?.Select(Reaction.Parse).ToArray() ??
|
json.GetPropertyOrNull("reactions")?.EnumerateArrayOrNull()?.Select(Reaction.Parse).ToArray() ??
|
||||||
Array.Empty<Reaction>();
|
Array.Empty<Reaction>();
|
||||||
|
|
@ -79,6 +84,7 @@ public record Message(
|
||||||
content,
|
content,
|
||||||
attachments,
|
attachments,
|
||||||
embeds,
|
embeds,
|
||||||
|
stickers,
|
||||||
reactions,
|
reactions,
|
||||||
mentionedUsers,
|
mentionedUsers,
|
||||||
messageReference,
|
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
|
||||||
@using System.Linq
|
@using System.Linq
|
||||||
@using System.Threading.Tasks
|
@using System.Threading.Tasks
|
||||||
|
@using DiscordChatExporter.Core.Discord.Data
|
||||||
@using DiscordChatExporter.Core.Exporting.Writers.Html;
|
@using DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||||
|
|
||||||
@namespace 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 */}
|
@{/* Message reactions */}
|
||||||
@if (message.Reactions.Any())
|
@if (message.Reactions.Any())
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -597,6 +597,16 @@
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chatlog__sticker {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__sticker--media {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.chatlog__reactions {
|
.chatlog__reactions {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
@ -660,7 +670,29 @@
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,21 @@ internal class JsonMessageWriter : MessageWriter
|
||||||
await _writer.FlushAsync(cancellationToken);
|
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(
|
private async ValueTask WriteReactionAsync(
|
||||||
Reaction reaction,
|
Reaction reaction,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
|
|
@ -276,6 +291,14 @@ internal class JsonMessageWriter : MessageWriter
|
||||||
|
|
||||||
_writer.WriteEndArray();
|
_writer.WriteEndArray();
|
||||||
|
|
||||||
|
// Stickers
|
||||||
|
_writer.WriteStartArray("stickers");
|
||||||
|
|
||||||
|
foreach (var sticker in message.Stickers)
|
||||||
|
await WriteStickerAsync(sticker, cancellationToken);
|
||||||
|
|
||||||
|
_writer.WriteEndArray();
|
||||||
|
|
||||||
// Reactions
|
// Reactions
|
||||||
_writer.WriteStartArray("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(
|
private async ValueTask WriteReactionsAsync(
|
||||||
IReadOnlyList<Reaction> reactions,
|
IReadOnlyList<Reaction> reactions,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
|
|
@ -156,9 +175,10 @@ internal class PlainTextMessageWriter : MessageWriter
|
||||||
|
|
||||||
await _writer.WriteLineAsync();
|
await _writer.WriteLineAsync();
|
||||||
|
|
||||||
// Attachments, embeds, reactions
|
// Attachments, embeds, reactions, etc.
|
||||||
await WriteAttachmentsAsync(message.Attachments, cancellationToken);
|
await WriteAttachmentsAsync(message.Attachments, cancellationToken);
|
||||||
await WriteEmbedsAsync(message.Embeds, cancellationToken);
|
await WriteEmbedsAsync(message.Embeds, cancellationToken);
|
||||||
|
await WriteStickersAsync(message.Stickers, cancellationToken);
|
||||||
await WriteReactionsAsync(message.Reactions, cancellationToken);
|
await WriteReactionsAsync(message.Reactions, cancellationToken);
|
||||||
|
|
||||||
await _writer.WriteLineAsync();
|
await _writer.WriteLineAsync();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue