diff --git a/DiscordChatExporter.Core/Discord/Data/Channel.cs b/DiscordChatExporter.Core/Discord/Data/Channel.cs index 0a21c21..02ceb4e 100644 --- a/DiscordChatExporter.Core/Discord/Data/Channel.cs +++ b/DiscordChatExporter.Core/Discord/Data/Channel.cs @@ -23,19 +23,6 @@ public partial record Channel( public partial record Channel { - private static ChannelCategory GetFallbackCategory(ChannelKind channelKind) => new( - Snowflake.Zero, - channelKind switch - { - ChannelKind.GuildTextChat => "Text", - ChannelKind.DirectTextChat => "Private", - ChannelKind.DirectGroupTextChat => "Group", - ChannelKind.GuildNews => "News", - _ => "Default" - }, - null - ); - public static Channel Parse(JsonElement json, ChannelCategory? categoryHint = null, int? positionHint = null) { var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); @@ -45,7 +32,7 @@ public partial record Channel json.GetPropertyOrNull("guild_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse) ?? Guild.DirectMessages.Id; - var category = categoryHint ?? GetFallbackCategory(kind); + var category = categoryHint ?? ChannelCategory.CreateDefault(kind); var name = // Guild channel diff --git a/DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs b/DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs index f7a9087..56bfe65 100644 --- a/DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs +++ b/DiscordChatExporter.Core/Discord/Data/ChannelCategory.cs @@ -7,6 +7,19 @@ namespace DiscordChatExporter.Core.Discord.Data; public record ChannelCategory(Snowflake Id, string Name, int? Position) : IHasId { + public static ChannelCategory CreateDefault(ChannelKind channelKind) => new( + Snowflake.Zero, + channelKind switch + { + ChannelKind.GuildTextChat => "Text", + ChannelKind.DirectTextChat => "Private", + ChannelKind.DirectGroupTextChat => "Group", + ChannelKind.GuildNews => "News", + _ => "Default" + }, + null + ); + public static ChannelCategory Parse(JsonElement json, int? positionHint = null) { var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); diff --git a/DiscordChatExporter.Core/Discord/Data/Member.cs b/DiscordChatExporter.Core/Discord/Data/Member.cs index e65fd3c..b84547f 100644 --- a/DiscordChatExporter.Core/Discord/Data/Member.cs +++ b/DiscordChatExporter.Core/Discord/Data/Member.cs @@ -20,6 +20,8 @@ public partial record Member( public partial record Member { + public static Member CreateDefault(User user) => new(user, null, null, Array.Empty()); + public static Member Parse(JsonElement json, Snowflake? guildId = null) { var user = json.GetProperty("user").Pipe(User.Parse); diff --git a/DiscordChatExporter.Core/Discord/DiscordClient.cs b/DiscordChatExporter.Core/Discord/DiscordClient.cs index 5bc0a1a..ef573e2 100644 --- a/DiscordChatExporter.Core/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Core/Discord/DiscordClient.cs @@ -150,6 +150,14 @@ public class DiscordClient : null; } + public async ValueTask TryGetUserAsync( + Snowflake userId, + CancellationToken cancellationToken = default) + { + var response = await TryGetJsonResponseAsync($"users/{userId}", cancellationToken); + return response?.Pipe(User.Parse); + } + public async IAsyncEnumerable GetUserGuildsAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default) { diff --git a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs index 527445b..75c319e 100644 --- a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs +++ b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs @@ -34,7 +34,7 @@ public class ChannelExporter { // Resolve members for referenced users foreach (var user in message.GetReferencedUsers()) - await context.PopulateMemberAsync(user.Id, cancellationToken); + await context.PopulateMemberAsync(user, cancellationToken); // Export the message if (request.MessageFilter.IsMatch(message)) diff --git a/DiscordChatExporter.Core/Exporting/ExportContext.cs b/DiscordChatExporter.Core/Exporting/ExportContext.cs index 9416627..1b359a4 100644 --- a/DiscordChatExporter.Core/Exporting/ExportContext.cs +++ b/DiscordChatExporter.Core/Exporting/ExportContext.cs @@ -20,6 +20,7 @@ internal class ExportContext private readonly ExportAssetDownloader _assetDownloader; public DiscordClient Discord { get; } + public ExportRequest Request { get; } public ExportContext(DiscordClient discord, @@ -43,18 +44,35 @@ internal class ExportContext _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) + // Because members cannot be pulled in bulk, we need to populate them on demand + private async ValueTask PopulateMemberAsync( + Snowflake id, + User? fallbackUser, + CancellationToken cancellationToken = default) { if (_members.ContainsKey(id)) return; var member = await Discord.TryGetGuildMemberAsync(Request.Guild.Id, id, cancellationToken); + // User may have left the guild since they were mentioned + if (member is null) + { + var user = fallbackUser ?? await Discord.TryGetUserAsync(id, cancellationToken); + if (user is not null) + member = Member.CreateDefault(user); + } + // Store the result even if it's null, to avoid re-fetching non-existing members _members[id] = member; } + public async ValueTask PopulateMemberAsync(Snowflake id, CancellationToken cancellationToken = default) => + await PopulateMemberAsync(id, null, cancellationToken); + + public async ValueTask PopulateMemberAsync(User user, CancellationToken cancellationToken = default) => + await PopulateMemberAsync(user.Id, user, cancellationToken); + public string FormatDate(DateTimeOffset instant) => Request.DateFormat switch { "unix" => instant.ToUnixTimeSeconds().ToString(),