diff --git a/DiscordChatExporter.Core/Discord/DiscordClient.cs b/DiscordChatExporter.Core/Discord/DiscordClient.cs index 8003227..9a44cc2 100644 --- a/DiscordChatExporter.Core/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Core/Discord/DiscordClient.cs @@ -484,4 +484,46 @@ public class DiscordClient } } } + + public async IAsyncEnumerable GetMessageReactionsAsync( + Snowflake channelId, + Snowflake messageId, + Emoji emoji, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + + var reactionName = Uri.EscapeDataString( + emoji.Id is not null + // Custom emoji + ? emoji.Name + ':' + emoji.Id + // Standard emoji + : emoji.Name + ); + + var currentAfter = Snowflake.Zero; + + while (true) + { + var url = new UrlBuilder() + .SetPath($"channels/{channelId}/messages/{messageId}/reactions/{reactionName}") + .SetQueryParameter("limit", "100") + .SetQueryParameter("after", currentAfter.ToString()) + .Build(); + + var response = await GetJsonResponseAsync(url, cancellationToken); + + var users = response.EnumerateArray().Select(User.Parse).ToArray(); + if (!users.Any()) + yield break; + foreach (var user in users) + { + yield return user; + currentAfter = user.Id; + } + // Each batch can contain up to 100 users. + // If we got fewer, then it's definitely the last batch. + if (users.Length < 100) + yield break; + } + } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs b/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs index 903e005..03f7061 100644 --- a/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs @@ -360,6 +360,37 @@ internal class JsonMessageWriter : MessageWriter _writer.WriteNumber("count", reaction.Count); + _writer.WriteStartArray("users"); + var users = await Context.Discord.GetMessageReactionsAsync(Context.Request.Channel.Id, message.Id, reaction.Emoji, cancellationToken); + foreach (var user in users) { + + // write limited user information without color and roles, + // because if we would write the full user information, + // we would have to fetch the guild member information for each user + // which would be a lot of requests + + _writer.WriteStartObject(); + + _writer.WriteString("id", user.Id.ToString()); + _writer.WriteString("name", user.Name); + _writer.WriteString("discriminator", user.DiscriminatorFormatted); + _writer.WriteString("nickname", Context.TryGetMember(user.Id)?.DisplayName ?? user.DisplayName); + _writer.WriteBoolean("isBot", user.IsBot); + + _writer.WriteString( + "avatarUrl", + await Context.ResolveAssetUrlAsync( + Context.TryGetMember(user.Id)?.AvatarUrl ?? user.AvatarUrl, + cancellationToken + ) + ); + + _writer.WriteEndObject(); + await _writer.FlushAsync(cancellationToken); + } + + _writer.WriteEndArray(); + _writer.WriteEndObject(); }