mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-02-14 07:43:31 -07:00
Refactor
This commit is contained in:
parent
0ee1107638
commit
e175c93038
|
|
@ -11,7 +11,6 @@ using DiscordChatExporter.Cli.Commands.Converters;
|
||||||
using DiscordChatExporter.Cli.Utils.Extensions;
|
using DiscordChatExporter.Cli.Utils.Extensions;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
|
||||||
using DiscordChatExporter.Core.Exceptions;
|
using DiscordChatExporter.Core.Exceptions;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using DiscordChatExporter.Core.Exporting.Filtering;
|
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||||
|
|
@ -137,7 +136,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||||
private ChannelExporter? _channelExporter;
|
private ChannelExporter? _channelExporter;
|
||||||
protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord);
|
protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord);
|
||||||
|
|
||||||
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<IChannel> channels)
|
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Channel> channels)
|
||||||
{
|
{
|
||||||
// Asset reuse can only be enabled if the download assets option is set
|
// Asset reuse can only be enabled if the download assets option is set
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/425
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/425
|
||||||
|
|
@ -176,9 +175,9 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export channels
|
// Export
|
||||||
var cancellationToken = console.RegisterCancellationHandler();
|
var cancellationToken = console.RegisterCancellationHandler();
|
||||||
var channelErrors = new ConcurrentDictionary<IChannel, string>();
|
var errors = new ConcurrentDictionary<Channel, string>();
|
||||||
|
|
||||||
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
|
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
|
||||||
await console.CreateProgressTicker().StartAsync(async progressContext =>
|
await console.CreateProgressTicker().StartAsync(async progressContext =>
|
||||||
|
|
@ -195,7 +194,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await progressContext.StartTaskAsync(
|
await progressContext.StartTaskAsync(
|
||||||
$"{channel.ParentName} / {channel.Name}",
|
$"{channel.Category} / {channel.Name}",
|
||||||
async progress =>
|
async progress =>
|
||||||
{
|
{
|
||||||
var guild = await Discord.GetGuildAsync(channel.GuildId, innerCancellationToken);
|
var guild = await Discord.GetGuildAsync(channel.GuildId, innerCancellationToken);
|
||||||
|
|
@ -226,7 +225,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||||
}
|
}
|
||||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||||
{
|
{
|
||||||
channelErrors[channel] = ex.Message;
|
errors[channel] = ex.Message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -236,25 +235,25 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||||
using (console.WithForegroundColor(ConsoleColor.White))
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
{
|
{
|
||||||
await console.Output.WriteLineAsync(
|
await console.Output.WriteLineAsync(
|
||||||
$"Successfully exported {channels.Count - channelErrors.Count} channel(s)."
|
$"Successfully exported {channels.Count - errors.Count} channel(s)."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print errors
|
// Print errors
|
||||||
if (channelErrors.Any())
|
if (errors.Any())
|
||||||
{
|
{
|
||||||
await console.Output.WriteLineAsync();
|
await console.Output.WriteLineAsync();
|
||||||
|
|
||||||
using (console.WithForegroundColor(ConsoleColor.Red))
|
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||||
{
|
{
|
||||||
await console.Error.WriteLineAsync(
|
await console.Error.WriteLineAsync(
|
||||||
$"Failed to export {channelErrors.Count} channel(s):"
|
$"Failed to export {errors.Count} channel(s):"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (channel, error) in channelErrors)
|
foreach (var (channel, error) in errors)
|
||||||
{
|
{
|
||||||
await console.Error.WriteAsync($"{channel.ParentName} / {channel.Name}: ");
|
await console.Error.WriteAsync($"{channel.Category} / {channel.Name}: ");
|
||||||
|
|
||||||
using (console.WithForegroundColor(ConsoleColor.Red))
|
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||||
await console.Error.WriteLineAsync(error);
|
await console.Error.WriteLineAsync(error);
|
||||||
|
|
@ -265,7 +264,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||||
|
|
||||||
// Fail the command only if ALL channels failed to export.
|
// Fail the command only if ALL channels failed to export.
|
||||||
// If only some channels failed to export, it's okay.
|
// If only some channels failed to export, it's okay.
|
||||||
if (channelErrors.Count >= channels.Count)
|
if (errors.Count >= channels.Count)
|
||||||
throw new CommandException("Export failed.");
|
throw new CommandException("Export failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,7 +290,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||||
|
|
||||||
foreach (var guildChannel in guildChannels)
|
foreach (var guildChannel in guildChannels)
|
||||||
{
|
{
|
||||||
if (guildChannel.Category.Id == channel.Id)
|
if (guildChannel.Parent?.Id == channel.Id)
|
||||||
channels.Add(guildChannel);
|
channels.Add(guildChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System.Globalization;
|
||||||
using System.Globalization;
|
|
||||||
using CliFx.Extensibility;
|
using CliFx.Extensibility;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands.Converters;
|
namespace DiscordChatExporter.Cli.Commands.Converters;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System.Linq;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
|
|
@ -19,7 +18,7 @@ public class ExportGuildCommand : ExportCommandBase
|
||||||
Description = "Guild ID."
|
Description = "Guild ID."
|
||||||
)]
|
)]
|
||||||
public required Snowflake GuildId { get; init; }
|
public required Snowflake GuildId { get; init; }
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"include-vc",
|
"include-vc",
|
||||||
Description = "Include voice channels."
|
Description = "Include voice channels."
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ public class GetChannelsCommand : DiscordCommandBase
|
||||||
|
|
||||||
var channels = (await Discord.GetGuildChannelsAsync(GuildId, cancellationToken))
|
var channels = (await Discord.GetGuildChannelsAsync(GuildId, cancellationToken))
|
||||||
.Where(c => c.Kind != ChannelKind.GuildCategory)
|
.Where(c => c.Kind != ChannelKind.GuildCategory)
|
||||||
.OrderBy(c => c.Category.Position)
|
.OrderBy(c => c.Parent?.Position)
|
||||||
.ThenBy(c => c.Name)
|
.ThenBy(c => c.Name)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ public class GetChannelsCommand : DiscordCommandBase
|
||||||
|
|
||||||
var threads = IncludeThreads
|
var threads = IncludeThreads
|
||||||
? (await Discord.GetGuildThreadsAsync(GuildId, cancellationToken)).OrderBy(c => c.Name).ToArray()
|
? (await Discord.GetGuildThreadsAsync(GuildId, cancellationToken)).OrderBy(c => c.Name).ToArray()
|
||||||
: Array.Empty<ChannelThread>();
|
: Array.Empty<Channel>();
|
||||||
|
|
||||||
foreach (var channel in channels)
|
foreach (var channel in channels)
|
||||||
{
|
{
|
||||||
|
|
@ -58,22 +58,22 @@ public class GetChannelsCommand : DiscordCommandBase
|
||||||
|
|
||||||
// Channel category / name
|
// Channel category / name
|
||||||
using (console.WithForegroundColor(ConsoleColor.White))
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
await console.Output.WriteLineAsync($"{channel.Category.Name} / {channel.Name}");
|
await console.Output.WriteLineAsync($"{channel.Category} / {channel.Name}");
|
||||||
|
|
||||||
var channelThreads = threads.Where(t => t.ParentId == channel.Id).ToArray();
|
var channelThreads = threads.Where(t => t.Parent?.Id == channel.Id).ToArray();
|
||||||
var channelThreadIdMaxLength = channelThreads
|
var channelThreadIdMaxLength = channelThreads
|
||||||
.Select(t => t.Id.ToString().Length)
|
.Select(t => t.Id.ToString().Length)
|
||||||
.OrderDescending()
|
.OrderDescending()
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
foreach (var thread in channelThreads)
|
foreach (var channelThread in channelThreads)
|
||||||
{
|
{
|
||||||
// Indent
|
// Indent
|
||||||
await console.Output.WriteAsync(" * ");
|
await console.Output.WriteAsync(" * ");
|
||||||
|
|
||||||
// Thread ID
|
// Thread ID
|
||||||
await console.Output.WriteAsync(
|
await console.Output.WriteAsync(
|
||||||
thread.Id.ToString().PadRight(channelThreadIdMaxLength, ' ')
|
channelThread.Id.ToString().PadRight(channelThreadIdMaxLength, ' ')
|
||||||
);
|
);
|
||||||
|
|
||||||
// Separator
|
// Separator
|
||||||
|
|
@ -82,7 +82,7 @@ public class GetChannelsCommand : DiscordCommandBase
|
||||||
|
|
||||||
// Thread name
|
// Thread name
|
||||||
using (console.WithForegroundColor(ConsoleColor.White))
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
await console.Output.WriteAsync($"Thread / {thread.Name}");
|
await console.Output.WriteAsync($"Thread / {channelThread.Name}");
|
||||||
|
|
||||||
// Separator
|
// Separator
|
||||||
using (console.WithForegroundColor(ConsoleColor.DarkGray))
|
using (console.WithForegroundColor(ConsoleColor.DarkGray))
|
||||||
|
|
@ -90,7 +90,7 @@ public class GetChannelsCommand : DiscordCommandBase
|
||||||
|
|
||||||
// Thread status
|
// Thread status
|
||||||
using (console.WithForegroundColor(ConsoleColor.White))
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
await console.Output.WriteLineAsync(thread.IsActive ? "Active" : "Archived");
|
await console.Output.WriteLineAsync(channelThread.IsActive ? "Active" : "Archived");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ public class GetDirectChannelsCommand : DiscordCommandBase
|
||||||
|
|
||||||
// Channel category / name
|
// Channel category / name
|
||||||
using (console.WithForegroundColor(ConsoleColor.White))
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
await console.Output.WriteLineAsync($"{channel.Category.Name} / {channel.Name}");
|
await console.Output.WriteLineAsync($"{channel.Category} / {channel.Name}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,23 +11,37 @@ public partial record Channel(
|
||||||
Snowflake Id,
|
Snowflake Id,
|
||||||
ChannelKind Kind,
|
ChannelKind Kind,
|
||||||
Snowflake GuildId,
|
Snowflake GuildId,
|
||||||
Snowflake? ParentId,
|
Channel? Parent,
|
||||||
string? ParentName,
|
|
||||||
int? ParentPosition,
|
|
||||||
ChannelCategory Category,
|
|
||||||
string Name,
|
string Name,
|
||||||
int? Position,
|
int? Position,
|
||||||
string? IconUrl,
|
string? IconUrl,
|
||||||
string? Topic,
|
string? Topic,
|
||||||
Snowflake? LastMessageId) : IChannel
|
bool IsActive,
|
||||||
|
Snowflake? LastMessageId)
|
||||||
{
|
{
|
||||||
|
// Used for visual backwards-compatibility with old exports, where
|
||||||
|
// channels without a parent (i.e. mostly DM channels) had a fallback
|
||||||
|
// category created for them.
|
||||||
|
public string Category => Parent?.Name ?? Kind switch
|
||||||
|
{
|
||||||
|
ChannelKind.GuildCategory => "Category",
|
||||||
|
ChannelKind.GuildTextChat => "Text",
|
||||||
|
ChannelKind.DirectTextChat => "Private",
|
||||||
|
ChannelKind.DirectGroupTextChat => "Group",
|
||||||
|
ChannelKind.GuildPrivateThread => "Private Thread",
|
||||||
|
ChannelKind.GuildPublicThread => "Public Thread",
|
||||||
|
ChannelKind.GuildNews => "News",
|
||||||
|
ChannelKind.GuildNewsThread => "News Thread",
|
||||||
|
_ => "Default"
|
||||||
|
};
|
||||||
|
|
||||||
// Only needed for WPF data binding. Don't use anywhere else.
|
// Only needed for WPF data binding. Don't use anywhere else.
|
||||||
public bool IsVoice => Kind.IsVoice();
|
public bool IsVoice => Kind.IsVoice();
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial record Channel
|
public partial record Channel
|
||||||
{
|
{
|
||||||
public static Channel Parse(JsonElement json, ChannelCategory? categoryHint = null, int? positionHint = null, string? parentName = null, int? parentPosition = null)
|
public static Channel Parse(JsonElement json, Channel? parent = null, int? positionHint = null)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||||
var kind = (ChannelKind)json.GetProperty("type").GetInt32();
|
var kind = (ChannelKind)json.GetProperty("type").GetInt32();
|
||||||
|
|
@ -36,10 +50,6 @@ public partial record Channel
|
||||||
json.GetPropertyOrNull("guild_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse) ??
|
json.GetPropertyOrNull("guild_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse) ??
|
||||||
Guild.DirectMessages.Id;
|
Guild.DirectMessages.Id;
|
||||||
|
|
||||||
var parentId = json.GetPropertyOrNull("parent_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse);
|
|
||||||
|
|
||||||
var category = categoryHint ?? ChannelCategory.CreateDefault(kind);
|
|
||||||
|
|
||||||
var name =
|
var name =
|
||||||
// Guild channel
|
// Guild channel
|
||||||
json.GetPropertyOrNull("name")?.GetNonWhiteSpaceStringOrNull() ??
|
json.GetPropertyOrNull("name")?.GetNonWhiteSpaceStringOrNull() ??
|
||||||
|
|
@ -66,6 +76,11 @@ public partial record Channel
|
||||||
|
|
||||||
var topic = json.GetPropertyOrNull("topic")?.GetStringOrNull();
|
var topic = json.GetPropertyOrNull("topic")?.GetStringOrNull();
|
||||||
|
|
||||||
|
var isActive = !json
|
||||||
|
.GetPropertyOrNull("thread_metadata")?
|
||||||
|
.GetPropertyOrNull("archived")?
|
||||||
|
.GetBooleanOrNull() ?? true;
|
||||||
|
|
||||||
var lastMessageId = json
|
var lastMessageId = json
|
||||||
.GetPropertyOrNull("last_message_id")?
|
.GetPropertyOrNull("last_message_id")?
|
||||||
.GetNonWhiteSpaceStringOrNull()?
|
.GetNonWhiteSpaceStringOrNull()?
|
||||||
|
|
@ -75,14 +90,12 @@ public partial record Channel
|
||||||
id,
|
id,
|
||||||
kind,
|
kind,
|
||||||
guildId,
|
guildId,
|
||||||
parentId,
|
parent,
|
||||||
parentName,
|
|
||||||
parentPosition,
|
|
||||||
category,
|
|
||||||
name,
|
name,
|
||||||
position,
|
position,
|
||||||
iconUrl,
|
iconUrl,
|
||||||
topic,
|
topic,
|
||||||
|
isActive,
|
||||||
lastMessageId
|
lastMessageId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
using System.Text.Json;
|
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
var name =
|
|
||||||
json.GetPropertyOrNull("name")?.GetNonWhiteSpaceStringOrNull() ??
|
|
||||||
id.ToString();
|
|
||||||
|
|
||||||
var position =
|
|
||||||
positionHint ??
|
|
||||||
json.GetPropertyOrNull("position")?.GetInt32OrNull();
|
|
||||||
|
|
||||||
return new ChannelCategory(id, name, position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
using System.Text.Json;
|
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
|
||||||
|
|
||||||
// https://discord.com/developers/docs/resources/channel#channel-object-example-thread-channel
|
|
||||||
public record ChannelThread(
|
|
||||||
Snowflake Id,
|
|
||||||
ChannelKind Kind,
|
|
||||||
Snowflake GuildId,
|
|
||||||
Snowflake? ParentId,
|
|
||||||
string? ParentName,
|
|
||||||
string Name,
|
|
||||||
bool IsActive,
|
|
||||||
Snowflake? LastMessageId) : IChannel
|
|
||||||
{
|
|
||||||
public int? ParentPosition => null;
|
|
||||||
public int? Position => null;
|
|
||||||
public string? IconUrl => null;
|
|
||||||
public string? Topic => null;
|
|
||||||
|
|
||||||
public static ChannelThread Parse(JsonElement json, string parentName)
|
|
||||||
{
|
|
||||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
|
||||||
var kind = (ChannelKind)json.GetProperty("type").GetInt32();
|
|
||||||
var guildId = json.GetProperty("guild_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
|
||||||
var parentId = json.GetProperty("parent_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
|
||||||
var name = json.GetProperty("name").GetNonWhiteSpaceString();
|
|
||||||
|
|
||||||
var isActive = !json
|
|
||||||
.GetPropertyOrNull("thread_metadata")?
|
|
||||||
.GetPropertyOrNull("archived")?
|
|
||||||
.GetBooleanOrNull() ?? true;
|
|
||||||
|
|
||||||
var lastMessageId = json
|
|
||||||
.GetPropertyOrNull("last_message_id")?
|
|
||||||
.GetNonWhiteSpaceStringOrNull()?
|
|
||||||
.Pipe(Snowflake.Parse);
|
|
||||||
|
|
||||||
return new ChannelThread(
|
|
||||||
id,
|
|
||||||
kind,
|
|
||||||
guildId,
|
|
||||||
parentId,
|
|
||||||
parentName,
|
|
||||||
name,
|
|
||||||
isActive,
|
|
||||||
lastMessageId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
namespace DiscordChatExporter.Core.Discord.Data.Common;
|
|
||||||
|
|
||||||
public interface IChannel : IHasId
|
|
||||||
{
|
|
||||||
ChannelKind Kind { get; }
|
|
||||||
Snowflake GuildId { get; }
|
|
||||||
Snowflake? ParentId { get; }
|
|
||||||
string? ParentName { get; }
|
|
||||||
int? ParentPosition { get; }
|
|
||||||
string Name { get; }
|
|
||||||
int? Position { get; }
|
|
||||||
string? IconUrl { get; }
|
|
||||||
string? Topic { get; }
|
|
||||||
Snowflake? LastMessageId { get; }
|
|
||||||
}
|
|
||||||
|
|
@ -20,7 +20,8 @@ public partial record Member(
|
||||||
|
|
||||||
public partial record Member
|
public partial record Member
|
||||||
{
|
{
|
||||||
public static Member CreateDefault(User user) => new(user, null, null, Array.Empty<Snowflake>());
|
public static Member CreateFallback(User user) =>
|
||||||
|
new(user, null, null, Array.Empty<Snowflake>());
|
||||||
|
|
||||||
public static Member Parse(JsonElement json, Snowflake? guildId = null)
|
public static Member Parse(JsonElement json, Snowflake? guildId = null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -215,14 +215,14 @@ public class DiscordClient
|
||||||
|
|
||||||
var channelsJson = response
|
var channelsJson = response
|
||||||
.EnumerateArray()
|
.EnumerateArray()
|
||||||
.OrderBy(j => j.GetProperty("position").GetInt32())
|
.OrderBy(c => c.GetProperty("position").GetInt32())
|
||||||
.ThenBy(j => j.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse))
|
.ThenBy(j => j.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var categories = channelsJson
|
var parentsById = channelsJson
|
||||||
.Where(j => j.GetProperty("type").GetInt32() == (int)ChannelKind.GuildCategory)
|
.Where(j => j.GetProperty("type").GetInt32() == (int)ChannelKind.GuildCategory)
|
||||||
.Select((j, index) => ChannelCategory.Parse(j, index + 1))
|
.Select((j, i) => Channel.Parse(j, null, i + 1))
|
||||||
.ToDictionary(j => j.Id.ToString(), StringComparer.Ordinal);
|
.ToDictionary(j => j.Id);
|
||||||
|
|
||||||
// Discord channel positions are relative, so we need to normalize them
|
// Discord channel positions are relative, so we need to normalize them
|
||||||
// so that the user may refer to them more easily in file name templates.
|
// so that the user may refer to them more easily in file name templates.
|
||||||
|
|
@ -230,21 +230,19 @@ public class DiscordClient
|
||||||
|
|
||||||
foreach (var channelJson in channelsJson)
|
foreach (var channelJson in channelsJson)
|
||||||
{
|
{
|
||||||
var parentId = channelJson
|
var parent = channelJson
|
||||||
.GetPropertyOrNull("parent_id")?
|
.GetPropertyOrNull("parent_id")?
|
||||||
.GetNonWhiteSpaceStringOrNull();
|
.GetNonWhiteSpaceStringOrNull()?
|
||||||
|
.Pipe(Snowflake.Parse)
|
||||||
|
.Pipe(parentsById.GetValueOrDefault);
|
||||||
|
|
||||||
var category = !string.IsNullOrWhiteSpace(parentId)
|
yield return Channel.Parse(channelJson, parent, position);
|
||||||
? categories.GetValueOrDefault(parentId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
yield return Channel.Parse(channelJson, category, position, category?.Name, category?.Position);
|
|
||||||
position++;
|
position++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<ChannelThread> GetGuildThreadsAsync(
|
public async IAsyncEnumerable<Channel> GetGuildThreadsAsync(
|
||||||
Snowflake guildId,
|
Snowflake guildId,
|
||||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|
@ -264,13 +262,14 @@ public class DiscordClient
|
||||||
.SetQueryParameter("offset", currentOffset.ToString())
|
.SetQueryParameter("offset", currentOffset.ToString())
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
// Can be null on channels that the user cannot access
|
||||||
var response = await TryGetJsonResponseAsync(url, cancellationToken);
|
var response = await TryGetJsonResponseAsync(url, cancellationToken);
|
||||||
if (response is null)
|
if (response is null)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
foreach (var threadJson in response.Value.GetProperty("threads").EnumerateArray())
|
foreach (var threadJson in response.Value.GetProperty("threads").EnumerateArray())
|
||||||
{
|
{
|
||||||
yield return ChannelThread.Parse(threadJson, channel.Name);
|
yield return Channel.Parse(threadJson, channel);
|
||||||
currentOffset++;
|
currentOffset++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,12 +283,18 @@ public class DiscordClient
|
||||||
{
|
{
|
||||||
// Active threads
|
// Active threads
|
||||||
{
|
{
|
||||||
|
var parentsById = channels.ToDictionary(c => c.Id);
|
||||||
|
|
||||||
var response = await GetJsonResponseAsync($"guilds/{guildId}/threads/active", cancellationToken);
|
var response = await GetJsonResponseAsync($"guilds/{guildId}/threads/active", cancellationToken);
|
||||||
foreach (var threadJson in response.GetProperty("threads").EnumerateArray())
|
foreach (var threadJson in response.GetProperty("threads").EnumerateArray())
|
||||||
{
|
{
|
||||||
var parentId = threadJson.GetProperty("parent_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
var parent = threadJson
|
||||||
var parentChannel = channels.First(t => t.Id == parentId);
|
.GetPropertyOrNull("parent_id")?
|
||||||
yield return ChannelThread.Parse(threadJson, parentChannel.Name);
|
.GetNonWhiteSpaceStringOrNull()?
|
||||||
|
.Pipe(Snowflake.Parse)
|
||||||
|
.Pipe(parentsById.GetValueOrDefault);
|
||||||
|
|
||||||
|
yield return Channel.Parse(threadJson, parent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,7 +308,7 @@ public class DiscordClient
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach (var threadJson in response.GetProperty("threads").EnumerateArray())
|
foreach (var threadJson in response.GetProperty("threads").EnumerateArray())
|
||||||
yield return ChannelThread.Parse(threadJson, channel.Name);
|
yield return Channel.Parse(threadJson, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private archived threads
|
// Private archived threads
|
||||||
|
|
@ -314,7 +319,7 @@ public class DiscordClient
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach (var threadJson in response.GetProperty("threads").EnumerateArray())
|
foreach (var threadJson in response.GetProperty("threads").EnumerateArray())
|
||||||
yield return ChannelThread.Parse(threadJson, channel.Name);
|
yield return Channel.Parse(threadJson, channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -352,41 +357,33 @@ public class DiscordClient
|
||||||
return response?.Pipe(Invite.Parse);
|
return response?.Pipe(Invite.Parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<ChannelCategory> GetChannelCategoryAsync(
|
public async ValueTask<Channel?> TryGetChannelAsync(
|
||||||
Snowflake channelId,
|
Snowflake channelId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
var response = await TryGetJsonResponseAsync($"channels/{channelId}", cancellationToken);
|
||||||
{
|
if (response is null)
|
||||||
var response = await GetJsonResponseAsync($"channels/{channelId}", cancellationToken);
|
return null;
|
||||||
return ChannelCategory.Parse(response);
|
|
||||||
}
|
|
||||||
// In some cases, Discord API returns an empty body when requesting a channel.
|
|
||||||
// Use an empty channel category as fallback for these cases.
|
|
||||||
catch (DiscordChatExporterException)
|
|
||||||
{
|
|
||||||
return new ChannelCategory(channelId, "Unknown Category", 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<Channel> GetChannelAsync(
|
|
||||||
Snowflake channelId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var response = await GetJsonResponseAsync($"channels/{channelId}", cancellationToken);
|
|
||||||
|
|
||||||
var parentId = response
|
var parentId = response
|
||||||
|
.Value
|
||||||
.GetPropertyOrNull("parent_id")?
|
.GetPropertyOrNull("parent_id")?
|
||||||
.GetNonWhiteSpaceStringOrNull()?
|
.GetNonWhiteSpaceStringOrNull()?
|
||||||
.Pipe(Snowflake.Parse);
|
.Pipe(Snowflake.Parse);
|
||||||
|
|
||||||
var category = parentId is not null
|
var parent = parentId is not null
|
||||||
? await GetChannelCategoryAsync(parentId.Value, cancellationToken)
|
? await TryGetChannelAsync(parentId.Value, cancellationToken)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Channel.Parse(response, category, parentName: category?.Name, parentPosition: category?.Position);
|
return Channel.Parse(response.Value, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Channel> GetChannelAsync(
|
||||||
|
Snowflake channelId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
await TryGetChannelAsync(channelId, cancellationToken) ??
|
||||||
|
throw new InvalidOperationException($"Channel {channelId} not found.");
|
||||||
|
|
||||||
private async ValueTask<Message?> TryGetLastMessageAsync(
|
private async ValueTask<Message?> TryGetLastMessageAsync(
|
||||||
Snowflake channelId,
|
Snowflake channelId,
|
||||||
Snowflake? before = null,
|
Snowflake? before = null,
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ internal class ExportContext
|
||||||
|
|
||||||
// User may have been deleted since they were mentioned
|
// User may have been deleted since they were mentioned
|
||||||
if (user is not null)
|
if (user is not null)
|
||||||
member = Member.CreateDefault(user);
|
member = Member.CreateFallback(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the result even if it's null, to avoid re-fetching non-existing members
|
// Store the result even if it's null, to avoid re-fetching non-existing members
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
|
||||||
using DiscordChatExporter.Core.Exporting.Filtering;
|
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||||
using DiscordChatExporter.Core.Utils;
|
using DiscordChatExporter.Core.Utils;
|
||||||
|
|
@ -15,7 +14,7 @@ public partial class ExportRequest
|
||||||
{
|
{
|
||||||
public Guild Guild { get; }
|
public Guild Guild { get; }
|
||||||
|
|
||||||
public IChannel Channel { get; }
|
public Channel Channel { get; }
|
||||||
|
|
||||||
public string OutputFilePath { get; }
|
public string OutputFilePath { get; }
|
||||||
|
|
||||||
|
|
@ -43,7 +42,7 @@ public partial class ExportRequest
|
||||||
|
|
||||||
public ExportRequest(
|
public ExportRequest(
|
||||||
Guild guild,
|
Guild guild,
|
||||||
IChannel channel,
|
Channel channel,
|
||||||
string outputPath,
|
string outputPath,
|
||||||
string? assetsDirPath,
|
string? assetsDirPath,
|
||||||
ExportFormat format,
|
ExportFormat format,
|
||||||
|
|
@ -95,7 +94,7 @@ public partial class ExportRequest
|
||||||
{
|
{
|
||||||
public static string GetDefaultOutputFileName(
|
public static string GetDefaultOutputFileName(
|
||||||
Guild guild,
|
Guild guild,
|
||||||
IChannel channel,
|
Channel channel,
|
||||||
ExportFormat format,
|
ExportFormat format,
|
||||||
Snowflake? after = null,
|
Snowflake? after = null,
|
||||||
Snowflake? before = null)
|
Snowflake? before = null)
|
||||||
|
|
@ -103,7 +102,7 @@ public partial class ExportRequest
|
||||||
var buffer = new StringBuilder();
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
// Guild and channel names
|
// Guild and channel names
|
||||||
buffer.Append($"{guild.Name} - {channel.ParentName} - {channel.Name} [{channel.Id}]");
|
buffer.Append($"{guild.Name} - {channel.Category} - {channel.Name} [{channel.Id}]");
|
||||||
|
|
||||||
// Date range
|
// Date range
|
||||||
if (after is not null || before is not null)
|
if (after is not null || before is not null)
|
||||||
|
|
@ -138,7 +137,7 @@ public partial class ExportRequest
|
||||||
private static string FormatPath(
|
private static string FormatPath(
|
||||||
string path,
|
string path,
|
||||||
Guild guild,
|
Guild guild,
|
||||||
IChannel channel,
|
Channel channel,
|
||||||
Snowflake? after,
|
Snowflake? after,
|
||||||
Snowflake? before)
|
Snowflake? before)
|
||||||
{
|
{
|
||||||
|
|
@ -149,12 +148,12 @@ public partial class ExportRequest
|
||||||
{
|
{
|
||||||
"%g" => guild.Id.ToString(),
|
"%g" => guild.Id.ToString(),
|
||||||
"%G" => guild.Name,
|
"%G" => guild.Name,
|
||||||
"%t" => channel.ParentId.ToString() ?? "",
|
"%t" => channel.Parent?.Id.ToString() ?? "",
|
||||||
"%T" => channel.ParentName ?? "",
|
"%T" => channel.Parent?.Name ?? "",
|
||||||
"%c" => channel.Id.ToString(),
|
"%c" => channel.Id.ToString(),
|
||||||
"%C" => channel.Name,
|
"%C" => channel.Name,
|
||||||
"%p" => channel.Position?.ToString() ?? "0",
|
"%p" => channel.Position?.ToString() ?? "0",
|
||||||
"%P" => channel.ParentPosition?.ToString() ?? "0",
|
"%P" => channel.Parent?.Position?.ToString() ?? "0",
|
||||||
"%a" => after?.ToDate().ToString("yyyy-MM-dd") ?? "",
|
"%a" => after?.ToDate().ToString("yyyy-MM-dd") ?? "",
|
||||||
"%b" => before?.ToDate().ToString("yyyy-MM-dd") ?? "",
|
"%b" => before?.ToDate().ToString("yyyy-MM-dd") ?? "",
|
||||||
"%d" => DateTimeOffset.Now.ToString("yyyy-MM-dd"),
|
"%d" => DateTimeOffset.Now.ToString("yyyy-MM-dd"),
|
||||||
|
|
@ -166,7 +165,7 @@ public partial class ExportRequest
|
||||||
|
|
||||||
private static string GetOutputBaseFilePath(
|
private static string GetOutputBaseFilePath(
|
||||||
Guild guild,
|
Guild guild,
|
||||||
IChannel channel,
|
Channel channel,
|
||||||
string outputPath,
|
string outputPath,
|
||||||
ExportFormat format,
|
ExportFormat format,
|
||||||
Snowflake? after = null,
|
Snowflake? after = null,
|
||||||
|
|
|
||||||
|
|
@ -241,8 +241,8 @@ internal class JsonMessageWriter : MessageWriter
|
||||||
_writer.WriteStartObject("channel");
|
_writer.WriteStartObject("channel");
|
||||||
_writer.WriteString("id", Context.Request.Channel.Id.ToString());
|
_writer.WriteString("id", Context.Request.Channel.Id.ToString());
|
||||||
_writer.WriteString("type", Context.Request.Channel.Kind.ToString());
|
_writer.WriteString("type", Context.Request.Channel.Kind.ToString());
|
||||||
_writer.WriteString("categoryId", Context.Request.Channel.ParentId.ToString());
|
_writer.WriteString("categoryId", Context.Request.Channel.Parent?.Id.ToString());
|
||||||
_writer.WriteString("category", Context.Request.Channel.ParentName);
|
_writer.WriteString("category", Context.Request.Channel.Category);
|
||||||
_writer.WriteString("name", Context.Request.Channel.Name);
|
_writer.WriteString("name", Context.Request.Channel.Name);
|
||||||
_writer.WriteString("topic", Context.Request.Channel.Topic);
|
_writer.WriteString("topic", Context.Request.Channel.Topic);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ internal class PlainTextMessageWriter : MessageWriter
|
||||||
{
|
{
|
||||||
await _writer.WriteLineAsync(new string('=', 62));
|
await _writer.WriteLineAsync(new string('=', 62));
|
||||||
await _writer.WriteLineAsync($"Guild: {Context.Request.Guild.Name}");
|
await _writer.WriteLineAsync($"Guild: {Context.Request.Guild.Name}");
|
||||||
await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.ParentName} / {Context.Request.Channel.Name}");
|
await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.Category} / {Context.Request.Channel.Name}");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
|
if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1004,7 +1004,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="preamble__entries-container">
|
<div class="preamble__entries-container">
|
||||||
<div class="preamble__entry">@Context.Request.Guild.Name</div>
|
<div class="preamble__entry">@Context.Request.Guild.Name</div>
|
||||||
<div class="preamble__entry">@Context.Request.Channel.ParentName / @Context.Request.Channel.Name</div>
|
<div class="preamble__entry">@Context.Request.Channel.Category / @Context.Request.Channel.Name</div>
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
|
@if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
FontWeight="Light"
|
FontWeight="Light"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
Visibility="{Binding IsSingleChannel, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
Visibility="{Binding IsSingleChannel, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||||
<Run Text="{Binding Channels[0].Category.Name, Mode=OneWay}" ToolTip="{Binding Channels[0].Category.Name, Mode=OneWay}" />
|
<Run Text="{Binding Channels[0].Category, Mode=OneWay}" ToolTip="{Binding Channels[0].Category, Mode=OneWay}" />
|
||||||
<Run Text="/" />
|
<Run Text="/" />
|
||||||
<Run
|
<Run
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue