mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-27 13:14:17 -04:00
Better support for exporting threads (#1046)
This commit is contained in:
parent
c69211797f
commit
8776a6955b
12 changed files with 78 additions and 37 deletions
|
@ -11,6 +11,7 @@ using DiscordChatExporter.Cli.Commands.Converters;
|
|||
using DiscordChatExporter.Cli.Utils.Extensions;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||
using DiscordChatExporter.Core.Exceptions;
|
||||
using DiscordChatExporter.Core.Exporting;
|
||||
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||
|
@ -136,7 +137,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
private ChannelExporter? _channelExporter;
|
||||
protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord);
|
||||
|
||||
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Channel> channels)
|
||||
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<IChannel> channels)
|
||||
{
|
||||
// Asset reuse can only be enabled if the download assets option is set
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/425
|
||||
|
@ -175,9 +176,9 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
);
|
||||
}
|
||||
|
||||
// Export
|
||||
// Export channels
|
||||
var cancellationToken = console.RegisterCancellationHandler();
|
||||
var errors = new ConcurrentDictionary<Channel, string>();
|
||||
var channelErrors = new ConcurrentDictionary<IChannel, string>();
|
||||
|
||||
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
|
||||
await console.CreateProgressTicker().StartAsync(async progressContext =>
|
||||
|
@ -194,7 +195,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
try
|
||||
{
|
||||
await progressContext.StartTaskAsync(
|
||||
$"{channel.Category.Name} / {channel.Name}",
|
||||
$"{channel.ParentName} / {channel.Name}",
|
||||
async progress =>
|
||||
{
|
||||
var guild = await Discord.GetGuildAsync(channel.GuildId, innerCancellationToken);
|
||||
|
@ -225,7 +226,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
errors[channel] = ex.Message;
|
||||
channelErrors[channel] = ex.Message;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -235,25 +236,25 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
using (console.WithForegroundColor(ConsoleColor.White))
|
||||
{
|
||||
await console.Output.WriteLineAsync(
|
||||
$"Successfully exported {channels.Count - errors.Count} channel(s)."
|
||||
$"Successfully exported {channels.Count - channelErrors.Count} channel(s)."
|
||||
);
|
||||
}
|
||||
|
||||
// Print errors
|
||||
if (errors.Any())
|
||||
if (channelErrors.Any())
|
||||
{
|
||||
await console.Output.WriteLineAsync();
|
||||
|
||||
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||
{
|
||||
await console.Error.WriteLineAsync(
|
||||
$"Failed to export {errors.Count} channel(s):"
|
||||
$"Failed to export {channelErrors.Count} channel(s):"
|
||||
);
|
||||
}
|
||||
|
||||
foreach (var (channel, error) in errors)
|
||||
foreach (var (channel, error) in channelErrors)
|
||||
{
|
||||
await console.Error.WriteAsync($"{channel.Category.Name} / {channel.Name}: ");
|
||||
await console.Error.WriteAsync($"{channel.ParentName} / {channel.Name}: ");
|
||||
|
||||
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||
await console.Error.WriteLineAsync(error);
|
||||
|
@ -264,7 +265,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||
|
||||
// Fail the command only if ALL channels failed to export.
|
||||
// If only some channels failed to export, it's okay.
|
||||
if (errors.Count >= channels.Count)
|
||||
if (channelErrors.Count >= channels.Count)
|
||||
throw new CommandException("Export failed.");
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using DiscordChatExporter.Cli.Commands.Base;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
|
@ -18,13 +19,14 @@ public class ExportGuildCommand : ExportCommandBase
|
|||
Description = "Guild ID."
|
||||
)]
|
||||
public required Snowflake GuildId { get; init; }
|
||||
|
||||
|
||||
[CommandOption(
|
||||
"include-vc",
|
||||
Description = "Include voice channels."
|
||||
)]
|
||||
public bool IncludeVoiceChannels { get; init; } = true;
|
||||
|
||||
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
await base.ExecuteAsync(console);
|
||||
|
|
|
@ -11,12 +11,15 @@ public partial record Channel(
|
|||
Snowflake Id,
|
||||
ChannelKind Kind,
|
||||
Snowflake GuildId,
|
||||
Snowflake ParentId,
|
||||
string? ParentName,
|
||||
int? ParentPosition,
|
||||
ChannelCategory Category,
|
||||
string Name,
|
||||
int? Position,
|
||||
string? IconUrl,
|
||||
string? Topic,
|
||||
Snowflake? LastMessageId) : IHasId
|
||||
Snowflake? LastMessageId) : IChannel
|
||||
{
|
||||
// Only needed for WPF data binding. Don't use anywhere else.
|
||||
public bool IsVoice => Kind.IsVoice();
|
||||
|
@ -24,7 +27,7 @@ public partial record Channel(
|
|||
|
||||
public partial record Channel
|
||||
{
|
||||
public static Channel Parse(JsonElement json, ChannelCategory? categoryHint = null, int? positionHint = null)
|
||||
public static Channel Parse(JsonElement json, ChannelCategory? categoryHint = null, int? positionHint = null, string? parentName = null, int? parentPosition = null)
|
||||
{
|
||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var kind = (ChannelKind)json.GetProperty("type").GetInt32();
|
||||
|
@ -33,6 +36,8 @@ public partial record Channel
|
|||
json.GetPropertyOrNull("guild_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse) ??
|
||||
Guild.DirectMessages.Id;
|
||||
|
||||
var parentId = json.GetProperty("parent_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
|
||||
var category = categoryHint ?? ChannelCategory.CreateDefault(kind);
|
||||
|
||||
var name =
|
||||
|
@ -70,6 +75,9 @@ public partial record Channel
|
|||
id,
|
||||
kind,
|
||||
guildId,
|
||||
parentId,
|
||||
parentName,
|
||||
parentPosition,
|
||||
category,
|
||||
name,
|
||||
position,
|
||||
|
|
|
@ -11,11 +11,17 @@ public record ChannelThread(
|
|||
ChannelKind Kind,
|
||||
Snowflake GuildId,
|
||||
Snowflake ParentId,
|
||||
string? ParentName,
|
||||
string Name,
|
||||
bool IsActive,
|
||||
Snowflake? LastMessageId) : IHasId
|
||||
Snowflake? LastMessageId) : IChannel
|
||||
{
|
||||
public static ChannelThread Parse(JsonElement json)
|
||||
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();
|
||||
|
@ -23,6 +29,7 @@ public record ChannelThread(
|
|||
var parentId = json.GetProperty("parent_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var name = json.GetProperty("name").GetNonWhiteSpaceString();
|
||||
|
||||
|
||||
var isActive = !json
|
||||
.GetPropertyOrNull("thread_metadata")?
|
||||
.GetPropertyOrNull("archived")?
|
||||
|
@ -38,6 +45,7 @@ public record ChannelThread(
|
|||
kind,
|
||||
guildId,
|
||||
parentId,
|
||||
parentName,
|
||||
name,
|
||||
isActive,
|
||||
lastMessageId
|
||||
|
|
15
DiscordChatExporter.Core/Discord/Data/Common/IChannel.cs
Normal file
15
DiscordChatExporter.Core/Discord/Data/Common/IChannel.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
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; }
|
||||
}
|
|
@ -238,7 +238,7 @@ public class DiscordClient
|
|||
? categories.GetValueOrDefault(parentId)
|
||||
: null;
|
||||
|
||||
yield return Channel.Parse(channelJson, category, position);
|
||||
yield return Channel.Parse(channelJson, category, position, category?.Name, category?.Position);
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
@ -270,7 +270,7 @@ public class DiscordClient
|
|||
|
||||
foreach (var threadJson in response.Value.GetProperty("threads").EnumerateArray())
|
||||
{
|
||||
yield return ChannelThread.Parse(threadJson);
|
||||
yield return ChannelThread.Parse(threadJson, channel.Name);
|
||||
currentOffset++;
|
||||
}
|
||||
|
||||
|
@ -286,7 +286,11 @@ public class DiscordClient
|
|||
{
|
||||
var response = await GetJsonResponseAsync($"guilds/{guildId}/threads/active", cancellationToken);
|
||||
foreach (var threadJson in response.GetProperty("threads").EnumerateArray())
|
||||
yield return ChannelThread.Parse(threadJson);
|
||||
{
|
||||
var parentId = threadJson.GetProperty("parent_id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var parentChannel = channels.First(t => t.Id == parentId);
|
||||
yield return ChannelThread.Parse(threadJson, parentChannel.Name);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var channel in channels)
|
||||
|
@ -299,7 +303,7 @@ public class DiscordClient
|
|||
);
|
||||
|
||||
foreach (var threadJson in response.GetProperty("threads").EnumerateArray())
|
||||
yield return ChannelThread.Parse(threadJson);
|
||||
yield return ChannelThread.Parse(threadJson, channel.Name);
|
||||
}
|
||||
|
||||
// Private archived threads
|
||||
|
@ -310,7 +314,7 @@ public class DiscordClient
|
|||
);
|
||||
|
||||
foreach (var threadJson in response.GetProperty("threads").EnumerateArray())
|
||||
yield return ChannelThread.Parse(threadJson);
|
||||
yield return ChannelThread.Parse(threadJson, channel.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -380,7 +384,7 @@ public class DiscordClient
|
|||
? await GetChannelCategoryAsync(parentId.Value, cancellationToken)
|
||||
: null;
|
||||
|
||||
return Channel.Parse(response, category);
|
||||
return Channel.Parse(response, category, parentName: category?.Name, parentPosition: category?.Position);
|
||||
}
|
||||
|
||||
private async ValueTask<Message?> TryGetLastMessageAsync(
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Text;
|
|||
using System.Text.RegularExpressions;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||
using DiscordChatExporter.Core.Utils;
|
||||
|
@ -14,7 +15,7 @@ public partial class ExportRequest
|
|||
{
|
||||
public Guild Guild { get; }
|
||||
|
||||
public Channel Channel { get; }
|
||||
public IChannel Channel { get; }
|
||||
|
||||
public string OutputFilePath { get; }
|
||||
|
||||
|
@ -42,7 +43,7 @@ public partial class ExportRequest
|
|||
|
||||
public ExportRequest(
|
||||
Guild guild,
|
||||
Channel channel,
|
||||
IChannel channel,
|
||||
string outputPath,
|
||||
string? assetsDirPath,
|
||||
ExportFormat format,
|
||||
|
@ -94,7 +95,7 @@ public partial class ExportRequest
|
|||
{
|
||||
public static string GetDefaultOutputFileName(
|
||||
Guild guild,
|
||||
Channel channel,
|
||||
IChannel channel,
|
||||
ExportFormat format,
|
||||
Snowflake? after = null,
|
||||
Snowflake? before = null)
|
||||
|
@ -102,7 +103,7 @@ public partial class ExportRequest
|
|||
var buffer = new StringBuilder();
|
||||
|
||||
// Guild and channel names
|
||||
buffer.Append($"{guild.Name} - {channel.Category.Name} - {channel.Name} [{channel.Id}]");
|
||||
buffer.Append($"{guild.Name} - {channel.ParentName} - {channel.Name} [{channel.Id}]");
|
||||
|
||||
// Date range
|
||||
if (after is not null || before is not null)
|
||||
|
@ -137,7 +138,7 @@ public partial class ExportRequest
|
|||
private static string FormatPath(
|
||||
string path,
|
||||
Guild guild,
|
||||
Channel channel,
|
||||
IChannel channel,
|
||||
Snowflake? after,
|
||||
Snowflake? before)
|
||||
{
|
||||
|
@ -148,12 +149,12 @@ public partial class ExportRequest
|
|||
{
|
||||
"%g" => guild.Id.ToString(),
|
||||
"%G" => guild.Name,
|
||||
"%t" => channel.Category.Id.ToString(),
|
||||
"%T" => channel.Category.Name,
|
||||
"%t" => channel.ParentId.ToString(),
|
||||
"%T" => channel.ParentName ?? "",
|
||||
"%c" => channel.Id.ToString(),
|
||||
"%C" => channel.Name,
|
||||
"%p" => channel.Position?.ToString() ?? "0",
|
||||
"%P" => channel.Category.Position?.ToString() ?? "0",
|
||||
"%P" => channel.ParentPosition?.ToString() ?? "0",
|
||||
"%a" => after?.ToDate().ToString("yyyy-MM-dd") ?? "",
|
||||
"%b" => before?.ToDate().ToString("yyyy-MM-dd") ?? "",
|
||||
"%d" => DateTimeOffset.Now.ToString("yyyy-MM-dd"),
|
||||
|
@ -165,7 +166,7 @@ public partial class ExportRequest
|
|||
|
||||
private static string GetOutputBaseFilePath(
|
||||
Guild guild,
|
||||
Channel channel,
|
||||
IChannel channel,
|
||||
string outputPath,
|
||||
ExportFormat format,
|
||||
Snowflake? after = null,
|
||||
|
|
|
@ -241,8 +241,8 @@ internal class JsonMessageWriter : MessageWriter
|
|||
_writer.WriteStartObject("channel");
|
||||
_writer.WriteString("id", Context.Request.Channel.Id.ToString());
|
||||
_writer.WriteString("type", Context.Request.Channel.Kind.ToString());
|
||||
_writer.WriteString("categoryId", Context.Request.Channel.Category.Id.ToString());
|
||||
_writer.WriteString("category", Context.Request.Channel.Category.Name);
|
||||
_writer.WriteString("categoryId", Context.Request.Channel.ParentId.ToString());
|
||||
_writer.WriteString("category", Context.Request.Channel.ParentName);
|
||||
_writer.WriteString("name", Context.Request.Channel.Name);
|
||||
_writer.WriteString("topic", Context.Request.Channel.Topic);
|
||||
|
||||
|
|
|
@ -193,7 +193,7 @@ internal class PlainTextMessageWriter : MessageWriter
|
|||
{
|
||||
await _writer.WriteLineAsync(new string('=', 62));
|
||||
await _writer.WriteLineAsync($"Guild: {Context.Request.Guild.Name}");
|
||||
await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.Category.Name} / {Context.Request.Channel.Name}");
|
||||
await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.ParentName} / {Context.Request.Channel.Name}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
|
||||
{
|
||||
|
|
|
@ -1004,7 +1004,7 @@
|
|||
</div>
|
||||
<div class="preamble__entries-container">
|
||||
<div class="preamble__entry">@Context.Request.Guild.Name</div>
|
||||
<div class="preamble__entry">@Context.Request.Channel.Category.Name / @Context.Request.Channel.Name</div>
|
||||
<div class="preamble__entry">@Context.Request.Channel.ParentName / @Context.Request.Channel.Name</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue