mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2024-09-19 12:18:48 -04:00
parent
6620c6299c
commit
d9a78d8e27
6 changed files with 158 additions and 120 deletions
|
@ -56,4 +56,19 @@ public class HtmlReplySpecs
|
|||
message.Text().Should().Contain("reply to attachment");
|
||||
message.QuerySelector(".chatlog__reply-link")?.Text().Should().Contain("Click to see attachment");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Message_with_a_reply_to_an_interaction_is_rendered_correctly()
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/569
|
||||
|
||||
// Act
|
||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||
ChannelIds.ReplyTestCases,
|
||||
Snowflake.Parse("1075152916417085492")
|
||||
);
|
||||
|
||||
// Assert
|
||||
message.Text().Should().Contain("used /poll");
|
||||
}
|
||||
}
|
18
DiscordChatExporter.Core/Discord/Data/Interaction.cs
Normal file
18
DiscordChatExporter.Core/Discord/Data/Interaction.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System.Text.Json;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using JsonExtensions.Reading;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
// https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object
|
||||
public record Interaction(Snowflake Id, string Name, User User)
|
||||
{
|
||||
public static Interaction Parse(JsonElement json)
|
||||
{
|
||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var name = json.GetProperty("name").GetNonWhiteSpaceString();
|
||||
var user = json.GetProperty("user").Pipe(User.Parse);
|
||||
|
||||
return new Interaction(id, name, user);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ using JsonExtensions.Reading;
|
|||
namespace DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object
|
||||
public record Message(
|
||||
public partial record Message(
|
||||
Snowflake Id,
|
||||
MessageKind Kind,
|
||||
MessageFlags Flags,
|
||||
|
@ -26,7 +26,27 @@ public record Message(
|
|||
IReadOnlyList<Reaction> Reactions,
|
||||
IReadOnlyList<User> MentionedUsers,
|
||||
MessageReference? Reference,
|
||||
Message? ReferencedMessage) : IHasId
|
||||
Message? ReferencedMessage,
|
||||
Interaction? Interaction) : IHasId
|
||||
{
|
||||
public bool IsReplyLike => Kind == MessageKind.Reply || Interaction is not null;
|
||||
|
||||
public IEnumerable<User> GetReferencedUsers()
|
||||
{
|
||||
yield return Author;
|
||||
|
||||
foreach (var user in MentionedUsers)
|
||||
yield return user;
|
||||
|
||||
if (ReferencedMessage is not null)
|
||||
yield return ReferencedMessage.Author;
|
||||
|
||||
if (Interaction is not null)
|
||||
yield return Interaction.User;
|
||||
}
|
||||
}
|
||||
|
||||
public partial record Message
|
||||
{
|
||||
private static IReadOnlyList<Embed> NormalizeEmbeds(IReadOnlyList<Embed> embeds)
|
||||
{
|
||||
|
@ -124,6 +144,7 @@ public record Message(
|
|||
|
||||
var messageReference = json.GetPropertyOrNull("message_reference")?.Pipe(MessageReference.Parse);
|
||||
var referencedMessage = json.GetPropertyOrNull("referenced_message")?.Pipe(Parse);
|
||||
var interaction = json.GetPropertyOrNull("interaction")?.Pipe(Interaction.Parse);
|
||||
|
||||
return new Message(
|
||||
id,
|
||||
|
@ -141,7 +162,8 @@ public record Message(
|
|||
reactions,
|
||||
mentionedUsers,
|
||||
messageReference,
|
||||
referencedMessage
|
||||
referencedMessage,
|
||||
interaction
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
|
@ -33,8 +32,8 @@ public class ChannelExporter
|
|||
progress,
|
||||
cancellationToken))
|
||||
{
|
||||
// Resolve members for the author and mentioned users
|
||||
foreach (var user in message.MentionedUsers.Prepend(message.Author))
|
||||
// Resolve members for referenced users
|
||||
foreach (var user in message.GetReferencedUsers())
|
||||
await context.PopulateMemberAsync(user.Id, cancellationToken);
|
||||
|
||||
// Export the message
|
||||
|
|
|
@ -35,16 +35,23 @@ internal class JsonMessageWriter : MessageWriter
|
|||
? await PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken)
|
||||
: markdown;
|
||||
|
||||
private async ValueTask WriteAttachmentAsync(
|
||||
Attachment attachment,
|
||||
private async ValueTask WriteUserAsync(
|
||||
User user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
_writer.WriteString("id", attachment.Id.ToString());
|
||||
_writer.WriteString("url", await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken));
|
||||
_writer.WriteString("fileName", attachment.FileName);
|
||||
_writer.WriteNumber("fileSizeBytes", attachment.FileSize.TotalBytes);
|
||||
_writer.WriteString("id", user.Id.ToString());
|
||||
_writer.WriteString("name", user.Name);
|
||||
_writer.WriteString("discriminator", user.DiscriminatorFormatted);
|
||||
_writer.WriteString("nickname", Context.TryGetMember(user.Id)?.Nick ?? user.Name);
|
||||
_writer.WriteString("color", Context.TryGetUserColor(user.Id)?.ToHex());
|
||||
_writer.WriteBoolean("isBot", user.IsBot);
|
||||
|
||||
_writer.WriteString(
|
||||
"avatarUrl",
|
||||
await Context.ResolveAssetUrlAsync(user.AvatarUrl, cancellationToken)
|
||||
);
|
||||
|
||||
_writer.WriteEndObject();
|
||||
await _writer.FlushAsync(cancellationToken);
|
||||
|
@ -184,58 +191,6 @@ 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.ResolveAssetUrlAsync(sticker.SourceUrl, cancellationToken));
|
||||
|
||||
_writer.WriteEndObject();
|
||||
await _writer.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async ValueTask WriteReactionAsync(
|
||||
Reaction reaction,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
// Emoji
|
||||
_writer.WriteStartObject("emoji");
|
||||
_writer.WriteString("id", reaction.Emoji.Id.ToString());
|
||||
_writer.WriteString("name", reaction.Emoji.Name);
|
||||
_writer.WriteString("code", reaction.Emoji.Code);
|
||||
_writer.WriteBoolean("isAnimated", reaction.Emoji.IsAnimated);
|
||||
_writer.WriteString("imageUrl", await Context.ResolveAssetUrlAsync(reaction.Emoji.ImageUrl, cancellationToken));
|
||||
_writer.WriteEndObject();
|
||||
|
||||
_writer.WriteNumber("count", reaction.Count);
|
||||
|
||||
_writer.WriteEndObject();
|
||||
await _writer.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async ValueTask WriteMentionAsync(
|
||||
User mentionedUser,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
_writer.WriteString("id", mentionedUser.Id.ToString());
|
||||
_writer.WriteString("name", mentionedUser.Name);
|
||||
_writer.WriteString("discriminator", mentionedUser.DiscriminatorFormatted);
|
||||
_writer.WriteString("nickname", Context.TryGetMember(mentionedUser.Id)?.Nick ?? mentionedUser.Name);
|
||||
_writer.WriteBoolean("isBot", mentionedUser.IsBot);
|
||||
|
||||
_writer.WriteEndObject();
|
||||
await _writer.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override async ValueTask WritePreambleAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Root object (start)
|
||||
|
@ -310,26 +265,23 @@ internal class JsonMessageWriter : MessageWriter
|
|||
}
|
||||
|
||||
// Author
|
||||
_writer.WriteStartObject("author");
|
||||
_writer.WriteString("id", message.Author.Id.ToString());
|
||||
_writer.WriteString("name", message.Author.Name);
|
||||
_writer.WriteString("discriminator", message.Author.DiscriminatorFormatted);
|
||||
_writer.WriteString("nickname", Context.TryGetMember(message.Author.Id)?.Nick ?? message.Author.Name);
|
||||
_writer.WriteString("color", Context.TryGetUserColor(message.Author.Id)?.ToHex());
|
||||
_writer.WriteBoolean("isBot", message.Author.IsBot);
|
||||
|
||||
_writer.WriteString(
|
||||
"avatarUrl",
|
||||
await Context.ResolveAssetUrlAsync(message.Author.AvatarUrl, cancellationToken)
|
||||
);
|
||||
|
||||
_writer.WriteEndObject();
|
||||
_writer.WritePropertyName("author");
|
||||
await WriteUserAsync(message.Author, cancellationToken);
|
||||
|
||||
// Attachments
|
||||
_writer.WriteStartArray("attachments");
|
||||
|
||||
foreach (var attachment in message.Attachments)
|
||||
await WriteAttachmentAsync(attachment, cancellationToken);
|
||||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
_writer.WriteString("id", attachment.Id.ToString());
|
||||
_writer.WriteString("url", await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken));
|
||||
_writer.WriteString("fileName", attachment.FileName);
|
||||
_writer.WriteNumber("fileSizeBytes", attachment.FileSize.TotalBytes);
|
||||
|
||||
_writer.WriteEndObject();
|
||||
}
|
||||
|
||||
_writer.WriteEndArray();
|
||||
|
||||
|
@ -345,7 +297,16 @@ internal class JsonMessageWriter : MessageWriter
|
|||
_writer.WriteStartArray("stickers");
|
||||
|
||||
foreach (var sticker in message.Stickers)
|
||||
await WriteStickerAsync(sticker, cancellationToken);
|
||||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
_writer.WriteString("id", sticker.Id.ToString());
|
||||
_writer.WriteString("name", sticker.Name);
|
||||
_writer.WriteString("format", sticker.Format.ToString());
|
||||
_writer.WriteString("sourceUrl", await Context.ResolveAssetUrlAsync(sticker.SourceUrl, cancellationToken));
|
||||
|
||||
_writer.WriteEndObject();
|
||||
}
|
||||
|
||||
_writer.WriteEndArray();
|
||||
|
||||
|
@ -353,15 +314,30 @@ internal class JsonMessageWriter : MessageWriter
|
|||
_writer.WriteStartArray("reactions");
|
||||
|
||||
foreach (var reaction in message.Reactions)
|
||||
await WriteReactionAsync(reaction, cancellationToken);
|
||||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
// Emoji
|
||||
_writer.WriteStartObject("emoji");
|
||||
_writer.WriteString("id", reaction.Emoji.Id.ToString());
|
||||
_writer.WriteString("name", reaction.Emoji.Name);
|
||||
_writer.WriteString("code", reaction.Emoji.Code);
|
||||
_writer.WriteBoolean("isAnimated", reaction.Emoji.IsAnimated);
|
||||
_writer.WriteString("imageUrl", await Context.ResolveAssetUrlAsync(reaction.Emoji.ImageUrl, cancellationToken));
|
||||
_writer.WriteEndObject();
|
||||
|
||||
_writer.WriteNumber("count", reaction.Count);
|
||||
|
||||
_writer.WriteEndObject();
|
||||
}
|
||||
|
||||
_writer.WriteEndArray();
|
||||
|
||||
// Mentions
|
||||
_writer.WriteStartArray("mentions");
|
||||
|
||||
foreach (var mention in message.MentionedUsers)
|
||||
await WriteMentionAsync(mention, cancellationToken);
|
||||
foreach (var user in message.MentionedUsers)
|
||||
await WriteUserAsync(user, cancellationToken);
|
||||
|
||||
_writer.WriteEndArray();
|
||||
|
||||
|
@ -375,6 +351,20 @@ internal class JsonMessageWriter : MessageWriter
|
|||
_writer.WriteEndObject();
|
||||
}
|
||||
|
||||
// Interaction
|
||||
if (message.Interaction is not null)
|
||||
{
|
||||
_writer.WriteStartObject("interaction");
|
||||
|
||||
_writer.WriteString("id", message.Interaction.Id.ToString());
|
||||
_writer.WriteString("name", message.Interaction.Name);
|
||||
|
||||
_writer.WritePropertyName("user");
|
||||
await WriteUserAsync(message.Interaction.User, cancellationToken);
|
||||
|
||||
_writer.WriteEndObject();
|
||||
}
|
||||
|
||||
_writer.WriteEndObject();
|
||||
await _writer.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
|
|
@ -31,30 +31,6 @@
|
|||
Context.Request.ShouldFormatMarkdown
|
||||
? await HtmlMarkdownVisitor.FormatAsync(Context, markdown, false, CancellationToken)
|
||||
: markdown;
|
||||
|
||||
var firstMessage = Messages.First();
|
||||
|
||||
var userMember = Context.TryGetMember(firstMessage.Author.Id);
|
||||
|
||||
var userColor = Context.TryGetUserColor(firstMessage.Author.Id);
|
||||
|
||||
var userNick = firstMessage.Author.IsBot
|
||||
? firstMessage.Author.Name
|
||||
: userMember?.Nick ?? firstMessage.Author.Name;
|
||||
|
||||
var referencedUserMember = firstMessage.ReferencedMessage is not null
|
||||
? Context.TryGetMember(firstMessage.ReferencedMessage.Author.Id)
|
||||
: null;
|
||||
|
||||
var referencedUserColor = firstMessage.ReferencedMessage is not null
|
||||
? Context.TryGetUserColor(firstMessage.ReferencedMessage.Author.Id)
|
||||
: null;
|
||||
|
||||
var referencedUserNick = firstMessage.ReferencedMessage is not null
|
||||
? firstMessage.ReferencedMessage.Author.IsBot
|
||||
? firstMessage.ReferencedMessage.Author.Name
|
||||
: referencedUserMember?.Nick ?? firstMessage.ReferencedMessage.Author.Name
|
||||
: null;
|
||||
}
|
||||
|
||||
<div class="chatlog__message-group">
|
||||
|
@ -62,19 +38,17 @@
|
|||
{
|
||||
var isFirst = i == 0;
|
||||
|
||||
var authorMember = Context.TryGetMember(message.Author.Id);
|
||||
var authorColor = Context.TryGetUserColor(message.Author.Id);
|
||||
var authorNick = message.Author.IsBot
|
||||
? message.Author.Name
|
||||
: authorMember?.Nick ?? message.Author.Name;
|
||||
|
||||
<div id="chatlog__message-container-@message.Id" class="chatlog__message-container @(message.IsPinned ? "chatlog__message-container--pinned" : null)" data-message-id="@message.Id">
|
||||
<div class="chatlog__message">
|
||||
@{/* System notification */}
|
||||
@if (message.Kind.IsSystemNotification())
|
||||
{
|
||||
// System notifications are grouped even if the message author is different.
|
||||
// That's why we have to update the user values with the author of the current message.
|
||||
userMember = Context.TryGetMember(message.Author.Id);
|
||||
userColor = Context.TryGetUserColor(message.Author.Id);
|
||||
userNick = message.Author.IsBot
|
||||
? message.Author.Name
|
||||
: userMember?.Nick ?? message.Author.Name;
|
||||
|
||||
<div class="chatlog__message-aside">
|
||||
<svg class="chatlog__system-notification-icon">
|
||||
@{
|
||||
|
@ -97,7 +71,7 @@
|
|||
|
||||
<div class="chatlog__message-primary">
|
||||
@{/* Author name */}
|
||||
<span class="chatlog__system-notification-author" style="@(userColor is not null ? $"color: rgb({userColor.Value.R}, {userColor.Value.G}, {userColor.Value.B})" : null)" title="@message.Author.FullName" data-user-id="@message.Author.Id">@userNick</span>
|
||||
<span class="chatlog__system-notification-author" style="@(authorColor is not null ? $"color: rgb({authorColor.Value.R}, {authorColor.Value.G}, {authorColor.Value.B})" : null)" title="@message.Author.FullName" data-user-id="@message.Author.Id">@authorNick</span>
|
||||
|
||||
@{/* Space out the content */}
|
||||
<span> </span>
|
||||
|
@ -169,7 +143,7 @@
|
|||
@if (isFirst)
|
||||
{
|
||||
// Reply symbol
|
||||
if (message.Kind == MessageKind.Reply)
|
||||
if (message.IsReplyLike)
|
||||
{
|
||||
<div class="chatlog__reply-symbol"></div>
|
||||
}
|
||||
|
@ -186,12 +160,18 @@
|
|||
<div class="chatlog__message-primary">
|
||||
@if (isFirst)
|
||||
{
|
||||
// Reply
|
||||
if (message.Kind == MessageKind.Reply && message.Reference is not null)
|
||||
// Message referenced by the reply
|
||||
if (message.IsReplyLike)
|
||||
{
|
||||
<div class="chatlog__reply">
|
||||
@if (message.ReferencedMessage is not null)
|
||||
{
|
||||
var referencedUserMember = Context.TryGetMember(message.ReferencedMessage.Author.Id);
|
||||
var referencedUserColor = Context.TryGetUserColor(message.ReferencedMessage.Author.Id);
|
||||
var referencedUserNick = message.ReferencedMessage.Author.IsBot
|
||||
? message.ReferencedMessage.Author.Name
|
||||
: referencedUserMember?.Nick ?? message.ReferencedMessage.Author.Name;
|
||||
|
||||
<img class="chatlog__reply-avatar" src="@await ResolveAssetUrlAsync(message.ReferencedMessage.Author.AvatarUrl)" alt="Avatar" loading="lazy">
|
||||
<div class="chatlog__reply-author" style="@(referencedUserColor is not null ? $"color: rgb({referencedUserColor.Value.R}, {referencedUserColor.Value.G}, {referencedUserColor.Value.B})" : null)" title="@message.ReferencedMessage.Author.FullName">@referencedUserNick</div>
|
||||
<div class="chatlog__reply-content">
|
||||
|
@ -217,6 +197,20 @@
|
|||
}
|
||||
</div>
|
||||
}
|
||||
else if (message.Interaction is not null)
|
||||
{
|
||||
var interactionUserMember = Context.TryGetMember(message.Interaction.User.Id);
|
||||
var interactionUserColor = Context.TryGetUserColor(message.Interaction.User.Id);
|
||||
var interactionUserNick = message.Interaction.User.IsBot
|
||||
? message.Interaction.User.Name
|
||||
: interactionUserMember?.Nick ?? message.Interaction.User.Name;
|
||||
|
||||
<img class="chatlog__reply-avatar" src="@await ResolveAssetUrlAsync(message.Interaction.User.AvatarUrl)" alt="Avatar" loading="lazy">
|
||||
<div class="chatlog__reply-author" style="@(interactionUserColor is not null ? $"color: rgb({interactionUserColor.Value.R}, {interactionUserColor.Value.G}, {interactionUserColor.Value.B})" : null)" title="@message.Interaction.User.FullName">@interactionUserNick</div>
|
||||
<div class="chatlog__reply-content">
|
||||
used /@message.Interaction.Name
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="chatlog__reply-unknown">
|
||||
|
@ -229,7 +223,7 @@
|
|||
// Header
|
||||
<div class="chatlog__header">
|
||||
@{/* Author name */}
|
||||
<span class="chatlog__author" style="@(userColor is not null ? $"color: rgb({userColor.Value.R}, {userColor.Value.G}, {userColor.Value.B})" : null)" title="@message.Author.FullName" data-user-id="@message.Author.Id">@userNick</span>
|
||||
<span class="chatlog__author" style="@(authorColor is not null ? $"color: rgb({authorColor.Value.R}, {authorColor.Value.G}, {authorColor.Value.B})" : null)" title="@message.Author.FullName" data-user-id="@message.Author.Id">@authorNick</span>
|
||||
|
||||
@{/* Bot tag */}
|
||||
@if (message.Author.IsBot)
|
||||
|
|
Loading…
Reference in a new issue