mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-02-14 07:43:31 -07:00
Merge bed2ed9f85 into 72f9e981de
This commit is contained in:
commit
746fec23f1
|
|
@ -27,7 +27,8 @@ public partial record Message(
|
|||
IReadOnlyList<User> MentionedUsers,
|
||||
MessageReference? Reference,
|
||||
Message? ReferencedMessage,
|
||||
Interaction? Interaction
|
||||
Interaction? Interaction,
|
||||
MessageSnapshot? ForwardedMessage
|
||||
) : IHasId
|
||||
{
|
||||
public bool IsSystemNotification { get; } =
|
||||
|
|
@ -38,6 +39,9 @@ public partial record Message(
|
|||
// App interactions are rendered as replies in the Discord client, but they are not actually replies
|
||||
public bool IsReplyLike => IsReply || Interaction is not null;
|
||||
|
||||
// A message is a forward if its reference type is Forward
|
||||
public bool IsForwarded { get; } = Reference?.Kind == MessageReferenceKind.Forward;
|
||||
|
||||
public bool IsEmpty { get; } =
|
||||
string.IsNullOrWhiteSpace(Content)
|
||||
&& !Attachments.Any()
|
||||
|
|
@ -174,6 +178,13 @@ public partial record Message
|
|||
var referencedMessage = json.GetPropertyOrNull("referenced_message")?.Pipe(Parse);
|
||||
var interaction = json.GetPropertyOrNull("interaction")?.Pipe(Interaction.Parse);
|
||||
|
||||
// Parse message snapshots for forwarded messages
|
||||
// Currently Discord only supports 1 snapshot per forward
|
||||
var forwardedMessage = json.GetPropertyOrNull("message_snapshots")
|
||||
?.EnumerateArrayOrNull()
|
||||
?.Select(MessageSnapshot.Parse)
|
||||
.FirstOrDefault();
|
||||
|
||||
return new Message(
|
||||
id,
|
||||
kind,
|
||||
|
|
@ -191,7 +202,8 @@ public partial record Message
|
|||
mentionedUsers,
|
||||
messageReference,
|
||||
referencedMessage,
|
||||
interaction
|
||||
interaction,
|
||||
forwardedMessage
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ using JsonExtensions.Reading;
|
|||
namespace DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure
|
||||
public record MessageReference(Snowflake? MessageId, Snowflake? ChannelId, Snowflake? GuildId)
|
||||
public record MessageReference(
|
||||
Snowflake? MessageId,
|
||||
Snowflake? ChannelId,
|
||||
Snowflake? GuildId,
|
||||
MessageReferenceKind Kind
|
||||
)
|
||||
{
|
||||
public static MessageReference Parse(JsonElement json)
|
||||
{
|
||||
|
|
@ -21,6 +26,10 @@ public record MessageReference(Snowflake? MessageId, Snowflake? ChannelId, Snowf
|
|||
?.GetNonWhiteSpaceStringOrNull()
|
||||
?.Pipe(Snowflake.Parse);
|
||||
|
||||
return new MessageReference(messageId, channelId, guildId);
|
||||
var kind =
|
||||
json.GetPropertyOrNull("type")?.GetInt32OrNull()?.Pipe(t => (MessageReferenceKind)t)
|
||||
?? MessageReferenceKind.Default;
|
||||
|
||||
return new MessageReference(messageId, channelId, guildId, kind);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
namespace DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-reference-types
|
||||
public enum MessageReferenceKind
|
||||
{
|
||||
Default = 0,
|
||||
Forward = 1,
|
||||
}
|
||||
67
DiscordChatExporter.Core/Discord/Data/MessageSnapshot.cs
Normal file
67
DiscordChatExporter.Core/Discord/Data/MessageSnapshot.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using DiscordChatExporter.Core.Discord.Data.Embeds;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using JsonExtensions.Reading;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-snapshot-object
|
||||
// Message snapshots contain a subset of message fields for forwarded messages
|
||||
public record MessageSnapshot(
|
||||
string Content,
|
||||
IReadOnlyList<Attachment> Attachments,
|
||||
IReadOnlyList<Embed> Embeds,
|
||||
IReadOnlyList<Sticker> Stickers,
|
||||
DateTimeOffset Timestamp,
|
||||
DateTimeOffset? EditedTimestamp
|
||||
)
|
||||
{
|
||||
public static MessageSnapshot Parse(JsonElement json)
|
||||
{
|
||||
// The message snapshot has a "message" property containing the actual message data
|
||||
var messageJson = json.GetPropertyOrNull("message") ?? json;
|
||||
|
||||
var content = messageJson.GetPropertyOrNull("content")?.GetStringOrNull() ?? "";
|
||||
|
||||
var attachments =
|
||||
messageJson
|
||||
.GetPropertyOrNull("attachments")
|
||||
?.EnumerateArrayOrNull()
|
||||
?.Select(Attachment.Parse)
|
||||
.ToArray() ?? [];
|
||||
|
||||
var embeds =
|
||||
messageJson
|
||||
.GetPropertyOrNull("embeds")
|
||||
?.EnumerateArrayOrNull()
|
||||
?.Select(Embed.Parse)
|
||||
.ToArray() ?? [];
|
||||
|
||||
var stickers =
|
||||
messageJson
|
||||
.GetPropertyOrNull("sticker_items")
|
||||
?.EnumerateArrayOrNull()
|
||||
?.Select(Sticker.Parse)
|
||||
.ToArray() ?? [];
|
||||
|
||||
var timestamp =
|
||||
messageJson.GetPropertyOrNull("timestamp")?.GetDateTimeOffsetOrNull()
|
||||
?? DateTimeOffset.MinValue;
|
||||
|
||||
var editedTimestamp = messageJson
|
||||
.GetPropertyOrNull("edited_timestamp")
|
||||
?.GetDateTimeOffsetOrNull();
|
||||
|
||||
return new MessageSnapshot(
|
||||
content,
|
||||
attachments,
|
||||
embeds,
|
||||
stickers,
|
||||
timestamp,
|
||||
editedTimestamp
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -530,6 +530,70 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
|||
_writer.WriteString("messageId", message.Reference.MessageId?.ToString());
|
||||
_writer.WriteString("channelId", message.Reference.ChannelId?.ToString());
|
||||
_writer.WriteString("guildId", message.Reference.GuildId?.ToString());
|
||||
_writer.WriteString("type", message.Reference.Kind.ToString());
|
||||
_writer.WriteEndObject();
|
||||
}
|
||||
|
||||
// Forwarded message
|
||||
if (message.ForwardedMessage is not null)
|
||||
{
|
||||
_writer.WriteStartObject("forwardedMessage");
|
||||
|
||||
_writer.WriteString(
|
||||
"content",
|
||||
await FormatMarkdownAsync(message.ForwardedMessage.Content, cancellationToken)
|
||||
);
|
||||
|
||||
_writer.WriteString(
|
||||
"timestamp",
|
||||
message.ForwardedMessage.Timestamp != DateTimeOffset.MinValue
|
||||
? Context.NormalizeDate(message.ForwardedMessage.Timestamp)
|
||||
: null
|
||||
);
|
||||
|
||||
_writer.WriteString(
|
||||
"timestampEdited",
|
||||
message.ForwardedMessage.EditedTimestamp?.Pipe(Context.NormalizeDate)
|
||||
);
|
||||
|
||||
// Forwarded attachments
|
||||
_writer.WriteStartArray("attachments");
|
||||
foreach (var attachment in message.ForwardedMessage.Attachments)
|
||||
{
|
||||
_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();
|
||||
|
||||
// Forwarded embeds
|
||||
_writer.WriteStartArray("embeds");
|
||||
foreach (var embed in message.ForwardedMessage.Embeds)
|
||||
await WriteEmbedAsync(embed, cancellationToken);
|
||||
_writer.WriteEndArray();
|
||||
|
||||
// Forwarded stickers
|
||||
_writer.WriteStartArray("stickers");
|
||||
foreach (var sticker in message.ForwardedMessage.Stickers)
|
||||
{
|
||||
_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();
|
||||
|
||||
_writer.WriteEndObject();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -262,6 +262,96 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
@* Forwarded message content *@
|
||||
@if (message.IsForwarded && message.ForwardedMessage is not null)
|
||||
{
|
||||
var fwd = message.ForwardedMessage;
|
||||
<div class="chatlog__forwarded">
|
||||
<div class="chatlog__forwarded-header">
|
||||
<svg class="chatlog__forwarded-icon">
|
||||
<use href="#forward-icon"></use>
|
||||
</svg>
|
||||
<em>Forwarded</em>
|
||||
</div>
|
||||
|
||||
@* Forwarded text content *@
|
||||
@if (!string.IsNullOrWhiteSpace(fwd.Content))
|
||||
{
|
||||
<div class="chatlog__forwarded-content chatlog__markdown">
|
||||
<span class="chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatMarkdownAsync(fwd.Content))<!--/wmm:ignore--></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Forwarded attachments *@
|
||||
@if (fwd.Attachments.Any())
|
||||
{
|
||||
<div class="chatlog__forwarded-attachments">
|
||||
@foreach (var attachment in fwd.Attachments)
|
||||
{
|
||||
@if (attachment.IsImage)
|
||||
{
|
||||
<a href="@await ResolveAssetUrlAsync(attachment.Url)">
|
||||
<img class="chatlog__forwarded-attachment" src="@await ResolveAssetUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Image attachment")" title="Image: @attachment.FileName (@attachment.FileSize)" loading="lazy">
|
||||
</a>
|
||||
}
|
||||
else if (attachment.IsVideo)
|
||||
{
|
||||
<video class="chatlog__forwarded-attachment" controls>
|
||||
<source src="@await ResolveAssetUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Video attachment")" title="Video: @attachment.FileName (@attachment.FileSize)">
|
||||
</video>
|
||||
}
|
||||
else if (attachment.IsAudio)
|
||||
{
|
||||
<audio class="chatlog__forwarded-attachment" controls>
|
||||
<source src="@await ResolveAssetUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Audio attachment")" title="Audio: @attachment.FileName (@attachment.FileSize)">
|
||||
</audio>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="chatlog__attachment-generic">
|
||||
<svg class="chatlog__attachment-generic-icon">
|
||||
<use href="#attachment-icon"/>
|
||||
</svg>
|
||||
<div class="chatlog__attachment-generic-name">
|
||||
<a href="@await ResolveAssetUrlAsync(attachment.Url)">
|
||||
@attachment.FileName
|
||||
</a>
|
||||
</div>
|
||||
<div class="chatlog__attachment-generic-size">
|
||||
@attachment.FileSize
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Forwarded stickers *@
|
||||
@foreach (var sticker in fwd.Stickers)
|
||||
{
|
||||
<div class="chatlog__sticker" title="@sticker.Name">
|
||||
@if (sticker.IsImage)
|
||||
{
|
||||
<img class="chatlog__sticker--media" src="@await ResolveAssetUrlAsync(sticker.SourceUrl)" alt="Sticker">
|
||||
}
|
||||
else if (sticker.Format == StickerFormat.Lottie)
|
||||
{
|
||||
<div class="chatlog__sticker--media" data-source="@await ResolveAssetUrlAsync(sticker.SourceUrl)"></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Forwarded timestamp *@
|
||||
<div class="chatlog__forwarded-timestamp">
|
||||
<span title="@FormatDate(fwd.Timestamp, "f")">Originally sent: @FormatDate(fwd.Timestamp)</span>
|
||||
@if (fwd.EditedTimestamp is not null)
|
||||
{
|
||||
<span title="@FormatDate(fwd.EditedTimestamp.Value, "f")"> (edited)</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Attachments *@
|
||||
@foreach (var attachment in message.Attachments)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
|
@ -224,6 +225,34 @@ internal class PlainTextMessageWriter(Stream stream, ExportContext context)
|
|||
await _writer.WriteLineAsync();
|
||||
}
|
||||
|
||||
private async ValueTask WriteForwardedMessageAsync(
|
||||
MessageSnapshot forwardedMessage,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
await _writer.WriteLineAsync("{Forwarded Message}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(forwardedMessage.Content))
|
||||
{
|
||||
await _writer.WriteLineAsync(
|
||||
await FormatMarkdownAsync(forwardedMessage.Content, cancellationToken)
|
||||
);
|
||||
}
|
||||
|
||||
if (forwardedMessage.Timestamp != DateTimeOffset.MinValue)
|
||||
{
|
||||
await _writer.WriteLineAsync(
|
||||
$"Originally sent: {Context.FormatDate(forwardedMessage.Timestamp)}"
|
||||
);
|
||||
}
|
||||
|
||||
await WriteAttachmentsAsync(forwardedMessage.Attachments, cancellationToken);
|
||||
await WriteEmbedsAsync(forwardedMessage.Embeds, cancellationToken);
|
||||
await WriteStickersAsync(forwardedMessage.Stickers, cancellationToken);
|
||||
|
||||
await _writer.WriteLineAsync();
|
||||
}
|
||||
|
||||
public override async ValueTask WriteMessageAsync(
|
||||
Message message,
|
||||
CancellationToken cancellationToken = default
|
||||
|
|
@ -248,6 +277,12 @@ internal class PlainTextMessageWriter(Stream stream, ExportContext context)
|
|||
|
||||
await _writer.WriteLineAsync();
|
||||
|
||||
// Forwarded message content
|
||||
if (message.ForwardedMessage is not null)
|
||||
{
|
||||
await WriteForwardedMessageAsync(message.ForwardedMessage, cancellationToken);
|
||||
}
|
||||
|
||||
// Attachments, embeds, reactions, etc.
|
||||
await WriteAttachmentsAsync(message.Attachments, cancellationToken);
|
||||
await WriteEmbedsAsync(message.Embeds, cancellationToken);
|
||||
|
|
|
|||
|
|
@ -304,6 +304,52 @@
|
|||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
.chatlog__forwarded {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.15rem;
|
||||
padding: 0.5rem;
|
||||
border-left: 4px solid @Themed("#4f545c", "#c7ccd1");
|
||||
border-radius: 4px;
|
||||
background-color: @Themed("rgba(46, 48, 54, 0.3)", "rgba(249, 249, 249, 0.3)");
|
||||
}
|
||||
|
||||
.chatlog__forwarded-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
color: @Themed("#b5b6b8", "#5f5f60");
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatlog__forwarded-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.chatlog__forwarded-content {
|
||||
color: @Themed("#dcddde", "#2e3338");
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.chatlog__forwarded-attachments {
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.chatlog__forwarded-attachment {
|
||||
max-width: 300px;
|
||||
max-height: 200px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__forwarded-timestamp {
|
||||
margin-top: 0.25rem;
|
||||
color: @Themed("#a3a6aa", "#5e6772");
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.chatlog__system-notification-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
|
@ -999,6 +1045,9 @@
|
|||
<path fill="#b9bbbe" d="M5.43309 21C5.35842 21 5.30189 20.9325 5.31494 20.859L5.99991 17H2.14274C2.06819 17 2.01168 16.9327 2.02453 16.8593L2.33253 15.0993C2.34258 15.0419 2.39244 15 2.45074 15H6.34991L7.40991 9H3.55274C3.47819 9 3.42168 8.93274 3.43453 8.85931L3.74253 7.09931C3.75258 7.04189 3.80244 7 3.86074 7H7.75991L8.45234 3.09903C8.46251 3.04174 8.51231 3 8.57049 3H10.3267C10.4014 3 10.4579 3.06746 10.4449 3.14097L9.75991 7H15.7599L16.4523 3.09903C16.4625 3.04174 16.5123 3 16.5705 3H18.3267C18.4014 3 18.4579 3.06746 18.4449 3.14097L17.7599 7H21.6171C21.6916 7 21.7481 7.06725 21.7353 7.14069L21.4273 8.90069C21.4172 8.95811 21.3674 9 21.3091 9H17.4099L17.0495 11.04H15.05L15.4104 9H9.41035L8.35035 15H10.5599V17H7.99991L7.30749 20.901C7.29732 20.9583 7.24752 21 7.18934 21H5.43309Z" />
|
||||
<path fill="#b9bbbe" d="M13.4399 12.96C12.9097 12.96 12.4799 13.3898 12.4799 13.92V20.2213C12.4799 20.7515 12.9097 21.1813 13.4399 21.1813H14.3999C14.5325 21.1813 14.6399 21.2887 14.6399 21.4213V23.4597C14.6399 23.6677 14.8865 23.7773 15.0408 23.6378L17.4858 21.4289C17.6622 21.2695 17.8916 21.1813 18.1294 21.1813H22.5599C23.0901 21.1813 23.5199 20.7515 23.5199 20.2213V13.92C23.5199 13.3898 23.0901 12.96 22.5599 12.96H13.4399Z" />
|
||||
</symbol>
|
||||
<symbol id="forward-icon" viewBox="0 0 24 24">
|
||||
<path fill="#b9bbbe" d="M13 4L21 12L13 20V15C8 15 4 17 1 22C2 16 6 10 13 9V4Z" />
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ public class DialogManager : IDisposable
|
|||
}
|
||||
);
|
||||
|
||||
// Yield to allow DialogHost to reset its state
|
||||
// before another dialog can be shown
|
||||
await Task.Yield();
|
||||
|
||||
return dialog.DialogResult;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue