mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-08-24 10:15:23 -05:00
Merge 38f9b9c676 into 8bc9fe7c72
This commit is contained in:
@@ -13,4 +13,5 @@ public enum MessageKind
|
||||
GuildMemberJoin = 7,
|
||||
ThreadCreated = 18,
|
||||
Reply = 19,
|
||||
ThreadStarterMessage = 21,
|
||||
}
|
||||
|
||||
@@ -629,6 +629,33 @@ public class DiscordClient(
|
||||
return response.EnumerateArray().Select(Message.Parse).LastOrDefault();
|
||||
}
|
||||
|
||||
public async ValueTask<Message?> TryGetMessageAsync(
|
||||
Snowflake channelId,
|
||||
Snowflake messageId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// Use the regular message listing endpoint with the 'around' parameter instead of the
|
||||
// dedicated single-message endpoint, because the latter is not accessible to user tokens.
|
||||
var url = new UrlBuilder()
|
||||
.SetPath($"channels/{channelId}/messages")
|
||||
.SetQueryParameter("around", messageId.ToString())
|
||||
.SetQueryParameter("limit", "1")
|
||||
.Build();
|
||||
|
||||
// Can be null on channels that the user cannot access
|
||||
var response = await TryGetJsonResponseAsync(url, cancellationToken);
|
||||
if (response is null)
|
||||
return null;
|
||||
|
||||
// The endpoint returns messages around the requested ID, so make sure to only return
|
||||
// the message that exactly matches it (it may be absent if it has been deleted).
|
||||
return response
|
||||
.Value.EnumerateArray()
|
||||
.Select(Message.Parse)
|
||||
.FirstOrDefault(m => m.Id == messageId);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<Message> GetMessagesAsync(
|
||||
Snowflake channelId,
|
||||
Snowflake? after = null,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
@@ -80,7 +82,63 @@ public class ChannelExporter(DiscordClient discord)
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
await foreach (var message in messages)
|
||||
// Threads created from a message don't include that message in their own history, even
|
||||
// though it logically belongs to the thread as its very first message. Because the thread
|
||||
// shares its ID with that starter message, we can fetch it from the parent channel and
|
||||
// export it together with the rest of the thread's messages.
|
||||
// This doesn't apply to forum/media posts, whose starter message is already part of the
|
||||
// thread's own history.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1265
|
||||
var starterMessage =
|
||||
request.Channel.IsThread
|
||||
&& request.Channel.Parent is { Kind: not ChannelKind.GuildForum } parent
|
||||
? await discord.TryGetMessageAsync(parent.Id, request.Channel.Id, cancellationToken)
|
||||
: null;
|
||||
|
||||
// Only include the starter message if it falls within the requested range
|
||||
if (
|
||||
starterMessage is not null
|
||||
&& (
|
||||
(request.After is not null && starterMessage.Id < request.After.Value)
|
||||
|| (request.Before is not null && starterMessage.Id > request.Before.Value)
|
||||
)
|
||||
)
|
||||
{
|
||||
starterMessage = null;
|
||||
}
|
||||
|
||||
// Prepend the starter message to the thread's history, or append it when exporting in
|
||||
// reverse order, so that it remains the oldest message in the output either way.
|
||||
async IAsyncEnumerable<Message> GetMessagesWithStarter(
|
||||
[EnumeratorCancellation] CancellationToken innerCancellationToken
|
||||
)
|
||||
{
|
||||
// If the message order is not reversed, the starter message should be the first one in the output, so we yield it before fetching the rest of the thread's history.
|
||||
if (starterMessage is not null && !request.IsReverseMessageOrder)
|
||||
yield return starterMessage;
|
||||
|
||||
// Fetch the rest of the thread's history and yield messages one by one
|
||||
await foreach (var message in messages.WithCancellation(innerCancellationToken))
|
||||
{
|
||||
// A thread created from a message has an empty THREAD_STARTER_MESSAGE placeholder
|
||||
// at the top of its history that merely points back to the originating message.
|
||||
// It never carries any renderable content of its own and is superseded by the
|
||||
// actual starter message fetched above, so skip it.
|
||||
if (message.Kind == MessageKind.ThreadStarterMessage)
|
||||
continue;
|
||||
|
||||
// Avoid exporting the starter message twice in case it's also returned as part
|
||||
// of the thread's own history.
|
||||
if (message.Id != starterMessage?.Id)
|
||||
yield return message;
|
||||
}
|
||||
|
||||
// If the message order is reversed, the starter message should be the last one in the output, so we yield it after fetching the rest of the thread's history.
|
||||
if (starterMessage is not null && request.IsReverseMessageOrder)
|
||||
yield return starterMessage;
|
||||
}
|
||||
|
||||
await foreach (var message in GetMessagesWithStarter(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user