Resolve mentioned members on demand inside of markdown

Closes #304
pull/1003/head
Tyrrrz 2 years ago
parent 2a81abb1a6
commit 290729158b

@ -251,16 +251,17 @@ public class DiscordClient
yield return Role.Parse(roleJson);
}
public async ValueTask<Member> GetGuildMemberAsync(
public async ValueTask<Member?> TryGetGuildMemberAsync(
Snowflake guildId,
User user,
Snowflake memberId,
CancellationToken cancellationToken = default)
{
if (guildId == Guild.DirectMessages.Id)
return Member.CreateForUser(user);
return null;
var response = await TryGetJsonResponseAsync($"guilds/{guildId}/members/{user.Id}", cancellationToken);
return response?.Pipe(Member.Parse) ?? Member.CreateForUser(user);
var response = await TryGetJsonResponseAsync($"guilds/{guildId}/members/{memberId}", cancellationToken);
return response?.Pipe(Member.Parse);
}
public async ValueTask<ChannelCategory> GetChannelCategoryAsync(

@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exceptions;
using DiscordChatExporter.Core.Utils.Extensions;
using Gress;
namespace DiscordChatExporter.Core.Exporting;
@ -23,21 +20,8 @@ public class ChannelExporter
CancellationToken cancellationToken = default)
{
// Build context
var contextMembers = new Dictionary<Snowflake, Member>();
var contextChannels = (await _discord.GetGuildChannelsAsync(request.Guild.Id, cancellationToken))
.ToDictionary(c => c.Id);
var contextRoles = (await _discord.GetGuildRolesAsync(request.Guild.Id, cancellationToken))
.ToDictionary(r => r.Id);
var context = new ExportContext(
_discord,
request,
contextMembers,
contextChannels,
contextRoles
);
var context = new ExportContext(_discord, request);
await context.PopulateChannelsAndRolesAsync(cancellationToken);
// Export messages
await using var messageExporter = new MessageExporter(context);
@ -49,20 +33,9 @@ public class ChannelExporter
progress,
cancellationToken))
{
// Resolve members for referenced users
foreach (var referencedUser in message.MentionedUsers.Prepend(message.Author))
{
if (contextMembers.ContainsKey(referencedUser.Id))
continue;
var member = await _discord.GetGuildMemberAsync(
request.Guild.Id,
referencedUser,
cancellationToken
);
contextMembers[member.Id] = member;
}
// Resolve members for the author and mentioned users
foreach (var user in message.MentionedUsers.Prepend(message.Author))
await context.PopulateMemberAsync(user.Id, cancellationToken);
// Export the message
if (request.MessageFilter.IsMatch(message))

@ -12,17 +12,48 @@ using DiscordChatExporter.Core.Utils.Extensions;
namespace DiscordChatExporter.Core.Exporting;
internal record ExportContext(
DiscordClient Discord,
ExportRequest Request,
IReadOnlyDictionary<Snowflake, Member> Members,
IReadOnlyDictionary<Snowflake, Channel> Channels,
IReadOnlyDictionary<Snowflake, Role> Roles)
internal class ExportContext
{
private readonly ExportAssetDownloader _assetDownloader = new(
Request.OutputAssetsDirPath,
Request.ShouldReuseAssets
private readonly Dictionary<Snowflake, Member?> _members = new();
private readonly Dictionary<Snowflake, Channel> _channels = new();
private readonly Dictionary<Snowflake, Role> _roles = new();
private readonly ExportAssetDownloader _assetDownloader;
public DiscordClient Discord { get; }
public ExportRequest Request { get; }
public ExportContext(DiscordClient discord,
ExportRequest request)
{
Discord = discord;
Request = request;
_assetDownloader = new ExportAssetDownloader(
request.OutputAssetsDirPath,
request.ShouldReuseAssets
);
}
public async ValueTask PopulateChannelsAndRolesAsync(CancellationToken cancellationToken = default)
{
await foreach (var channel in Discord.GetGuildChannelsAsync(Request.Guild.Id, cancellationToken))
_channels[channel.Id] = channel;
await foreach (var role in Discord.GetGuildRolesAsync(Request.Guild.Id, cancellationToken))
_roles[role.Id] = role;
}
// Because members are not pulled in bulk, we need to populate them on demand
public async ValueTask PopulateMemberAsync(Snowflake id, CancellationToken cancellationToken = default)
{
if (_members.ContainsKey(id))
return;
var member = await Discord.TryGetGuildMemberAsync(Request.Guild.Id, id, cancellationToken);
// Store the result even if it's null, to avoid re-fetching non-existing members
_members[id] = member;
}
public string FormatDate(DateTimeOffset instant) => Request.DateFormat switch
{
@ -31,11 +62,11 @@ internal record ExportContext(
var format => instant.ToLocalString(format)
};
public Member? TryGetMember(Snowflake id) => Members.GetValueOrDefault(id);
public Member? TryGetMember(Snowflake id) => _members.GetValueOrDefault(id);
public Channel? TryGetChannel(Snowflake id) => Channels.GetValueOrDefault(id);
public Channel? TryGetChannel(Snowflake id) => _channels.GetValueOrDefault(id);
public Role? TryGetRole(Snowflake id) => Roles.GetValueOrDefault(id);
public Role? TryGetRole(Snowflake id) => _roles.GetValueOrDefault(id);
public Color? TryGetUserColor(Snowflake id)
{

@ -195,6 +195,12 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
}
else if (mention.Kind == MentionKind.User)
{
// User mentions are not always included in the message object,
// which means they need to be populated on demand.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/304
if (mention.TargetId is not null)
await _context.PopulateMemberAsync(mention.TargetId.Value, cancellationToken);
var member = mention.TargetId?.Pipe(_context.TryGetMember);
var fullName = member?.User.FullName ?? "Unknown";
var nick = member?.Nick ?? "Unknown";

@ -53,6 +53,12 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
}
else if (mention.Kind == MentionKind.User)
{
// User mentions are not always included in the message object,
// which means they need to be populated on demand.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/304
if (mention.TargetId is not null)
await _context.PopulateMemberAsync(mention.TargetId.Value, cancellationToken);
var member = mention.TargetId?.Pipe(_context.TryGetMember);
var name = member?.User.Name ?? "Unknown";

Loading…
Cancel
Save