Remove `Channel.ParentNameWithFallback`

pull/1131/head
Tyrrrz 10 months ago
parent 59344cedbe
commit 5abe74894c

@ -200,7 +200,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
try
{
await progressContext.StartTaskAsync(
$"{channel.ParentNameWithFallback} / {channel.Name}",
channel.GetHierarchicalName(),
async progress =>
{
var guild = await Discord.GetGuildAsync(
@ -263,10 +263,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
foreach (var (channel, error) in errorsByChannel)
{
await console.Error.WriteAsync(
$"{channel.ParentNameWithFallback} / {channel.Name}: "
);
await console.Error.WriteAsync($"{channel.GetHierarchicalName()}: ");
using (console.WithForegroundColor(ConsoleColor.Red))
await console.Error.WriteLineAsync(error);
}
@ -294,7 +291,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
var channel = await Discord.GetChannelAsync(channelId, cancellationToken);
// Unwrap categories
if (channel.Kind == ChannelKind.GuildCategory)
if (channel.IsCategory)
{
var guildChannels =
channelsByGuild.GetValueOrDefault(channel.GuildId)

@ -60,10 +60,10 @@ public class ExportAllCommand : ExportCommandBase
var channel in Discord.GetGuildChannelsAsync(guild.Id, cancellationToken)
)
{
if (channel.Kind == ChannelKind.GuildCategory)
if (channel.IsCategory)
continue;
if (!IncludeVoiceChannels && channel.Kind.IsVoice())
if (!IncludeVoiceChannels && channel.IsVoice)
continue;
channels.Add(channel);
@ -129,15 +129,15 @@ public class ExportAllCommand : ExportCommandBase
// Filter out unwanted channels
if (!IncludeDirectChannels)
channels.RemoveAll(c => c.Kind.IsDirect());
channels.RemoveAll(c => c.IsDirect);
if (!IncludeGuildChannels)
channels.RemoveAll(c => c.Kind.IsGuild());
channels.RemoveAll(c => c.IsGuild);
if (!IncludeVoiceChannels)
channels.RemoveAll(c => c.Kind.IsVoice());
channels.RemoveAll(c => c.IsVoice);
if (ThreadInclusionMode == ThreadInclusionMode.None)
channels.RemoveAll(c => c.Kind.IsThread());
channels.RemoveAll(c => c.IsThread);
if (ThreadInclusionMode != ThreadInclusionMode.All)
channels.RemoveAll(c => c.Kind.IsThread() && c.IsArchived);
channels.RemoveAll(c => c is { IsThread: true, IsArchived: true });
await ExportAsync(console, channels);
}

@ -38,10 +38,10 @@ public class ExportGuildCommand : ExportCommandBase
// Regular channels
await foreach (var channel in Discord.GetGuildChannelsAsync(GuildId, cancellationToken))
{
if (channel.Kind == ChannelKind.GuildCategory)
if (channel.IsCategory)
continue;
if (!IncludeVoiceChannels && channel.Kind.IsVoice())
if (!IncludeVoiceChannels && channel.IsVoice)
continue;
channels.Add(channel);

@ -35,8 +35,8 @@ public class GetChannelsCommand : DiscordCommandBase
var cancellationToken = console.RegisterCancellationHandler();
var channels = (await Discord.GetGuildChannelsAsync(GuildId, cancellationToken))
.Where(c => c.Kind != ChannelKind.GuildCategory)
.Where(c => IncludeVoiceChannels || !c.Kind.IsVoice())
.Where(c => !c.IsCategory)
.Where(c => IncludeVoiceChannels || !c.IsVoice)
.OrderBy(c => c.Parent?.Position)
.ThenBy(c => c.Name)
.ToArray();
@ -72,11 +72,9 @@ public class GetChannelsCommand : DiscordCommandBase
using (console.WithForegroundColor(ConsoleColor.DarkGray))
await console.Output.WriteAsync(" | ");
// Channel category / name
// Channel name
using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteLineAsync(
$"{channel.ParentNameWithFallback} / {channel.Name}"
);
await console.Output.WriteLineAsync(channel.GetHierarchicalName());
var channelThreads = threads.Where(t => t.Parent?.Id == channel.Id).ToArray();
var channelThreadIdMaxLength = channelThreads

@ -21,7 +21,6 @@ public class GetDirectChannelsCommand : DiscordCommandBase
var channels = (
await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken)
)
.Where(c => c.Kind != ChannelKind.GuildCategory)
.OrderByDescending(c => c.LastMessageId)
.ThenBy(c => c.Name)
.ToArray();
@ -42,11 +41,9 @@ public class GetDirectChannelsCommand : DiscordCommandBase
using (console.WithForegroundColor(ConsoleColor.DarkGray))
await console.Output.WriteAsync(" | ");
// Channel category / name
// Channel name
using (console.WithForegroundColor(ConsoleColor.White))
await console.Output.WriteLineAsync(
$"{channel.ParentNameWithFallback} / {channel.Name}"
);
await console.Output.WriteLineAsync(channel.GetHierarchicalName());
}
}
}

@ -1,4 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data.Common;
using DiscordChatExporter.Core.Utils.Extensions;
@ -20,32 +21,36 @@ public partial record Channel(
Snowflake? LastMessageId
) : IHasId
{
// Used for visual backwards-compatibility with old exports, where
// channels without a parent (i.e. mostly DM channels) or channels
// with an inaccessible parent (i.e. inside private categories) had
// a fallback category created for them.
public string ParentNameWithFallback =>
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"
};
public bool IsDirect => Kind is ChannelKind.DirectTextChat or ChannelKind.DirectGroupTextChat;
public bool IsGuild => !IsDirect;
public bool IsCategory => Kind == ChannelKind.GuildCategory;
public bool IsVoice => Kind is ChannelKind.GuildVoiceChat or ChannelKind.GuildStageVoice;
public bool IsThread =>
Kind
is ChannelKind.GuildNewsThread
or ChannelKind.GuildPublicThread
or ChannelKind.GuildPrivateThread;
public bool IsEmpty => LastMessageId is null;
// Only needed for WPF data binding. Don't use anywhere else.
public bool IsVoice => Kind.IsVoice();
public IEnumerable<Channel> GetParents()
{
var current = Parent;
while (current is not null)
{
yield return current;
current = current.Parent;
}
}
public Channel? TryGetRootParent() => GetParents().LastOrDefault();
// Only needed for WPF data binding. Don't use anywhere else.
public bool IsThread => Kind.IsThread();
public string GetHierarchicalName() =>
string.Join(" / ", GetParents().Reverse().Select(c => c.Name).Append(Name));
public bool MayHaveMessagesAfter(Snowflake messageId) => !IsEmpty && messageId < LastMessageId;

@ -16,20 +16,3 @@ public enum ChannelKind
GuildDirectory = 14,
GuildForum = 15
}
public static class ChannelKindExtensions
{
public static bool IsDirect(this ChannelKind kind) =>
kind is ChannelKind.DirectTextChat or ChannelKind.DirectGroupTextChat;
public static bool IsGuild(this ChannelKind kind) => !kind.IsDirect();
public static bool IsVoice(this ChannelKind kind) =>
kind is ChannelKind.GuildVoiceChat or ChannelKind.GuildStageVoice;
public static bool IsThread(this ChannelKind kind) =>
kind
is ChannelKind.GuildNewsThread
or ChannelKind.GuildPublicThread
or ChannelKind.GuildPrivateThread;
}

@ -6,7 +6,12 @@ using JsonExtensions.Reading;
namespace DiscordChatExporter.Core.Discord.Data;
// https://discord.com/developers/docs/resources/guild#guild-object
public record Guild(Snowflake Id, string Name, string IconUrl) : IHasId
public partial record Guild(Snowflake Id, string Name, string IconUrl) : IHasId
{
public bool IsDirect => Id == DirectMessages.Id;
}
public partial record Guild
{
// Direct messages are encapsulated within a special pseudo-guild for consistency
public static Guild DirectMessages { get; } =

@ -30,7 +30,13 @@ public partial record Message(
Interaction? Interaction
) : IHasId
{
public bool IsReplyLike => Kind == MessageKind.Reply || Interaction is not null;
public bool IsSystemNotification =>
Kind is >= MessageKind.RecipientAdd and <= MessageKind.ThreadCreated;
public bool IsReply => Kind == MessageKind.Reply;
// App interactions are rendered as replies in the Discord client, but they are not actually replies
public bool IsReplyLike => IsReply || Interaction is not null;
public IEnumerable<User> GetReferencedUsers()
{

@ -14,8 +14,3 @@ public enum MessageKind
ThreadCreated = 18,
Reply = 19
}
public static class MessageKindExtensions
{
public static bool IsSystemNotification(this MessageKind kind) => (int)kind is >= 1 and <= 18;
}

@ -286,11 +286,12 @@ public class DiscordClient
yield break;
var tokenKind = _resolvedTokenKind ??= await GetTokenKindAsync(cancellationToken);
var channels = (await GetGuildChannelsAsync(guildId, cancellationToken))
// Categories cannot have threads
.Where(c => c.Kind != ChannelKind.GuildCategory)
.Where(c => !c.IsCategory)
// Voice channels cannot have threads
.Where(c => !c.Kind.IsVoice())
.Where(c => !c.IsVoice)
// Empty channels cannot have threads
.Where(c => !c.IsEmpty)
// If the 'before' boundary is specified, skip channels that don't have messages

@ -92,7 +92,7 @@ internal partial class CsvMessageWriter : MessageWriter
await _writer.WriteAsync(',');
// Message content
if (message.Kind.IsSystemNotification())
if (message.IsSystemNotification)
{
await _writer.WriteAsync(CsvEncode(message.GetFallbackContent()));
}

@ -99,10 +99,21 @@ public partial class ExportRequest
{
var buffer = new StringBuilder();
// Guild and channel names
buffer.Append(
$"{guild.Name} - {channel.ParentNameWithFallback} - {channel.Name} [{channel.Id}]"
);
// Guild name
buffer.Append(guild.Name);
// Parent name
if (channel.Parent is not null)
buffer.Append(" - ").Append(channel.Parent.Name);
// Channel name and ID
buffer
.Append(" - ")
.Append(channel.Name)
.Append(' ')
.Append('[')
.Append(channel.Id)
.Append(']');
// Date range
if (after is not null || before is not null)
@ -142,9 +153,8 @@ public partial class ExportRequest
Channel channel,
Snowflake? after,
Snowflake? before
)
{
return Regex.Replace(
) =>
Regex.Replace(
path,
"%.",
m =>
@ -153,14 +163,18 @@ public partial class ExportRequest
{
"%g" => guild.Id.ToString(),
"%G" => guild.Name,
"%t" => channel.Parent?.Id.ToString() ?? "",
"%T" => channel.Parent?.Name ?? "",
"%c" => channel.Id.ToString(),
"%C" => channel.Name,
"%p" => channel.Position?.ToString(CultureInfo.InvariantCulture) ?? "0",
"%P"
=> channel.Parent?.Position?.ToString(CultureInfo.InvariantCulture)
?? "0",
"%a"
=> after?.ToDate().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
?? "",
@ -172,12 +186,12 @@ public partial class ExportRequest
"yyyy-MM-dd",
CultureInfo.InvariantCulture
),
"%%" => "%",
_ => m.Value
}
)
);
}
private static string GetOutputBaseFilePath(
Guild guild,

@ -280,7 +280,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
else if (mention.Kind == MentionKind.Channel)
{
var channel = mention.TargetId?.Pipe(_context.TryGetChannel);
var symbol = channel?.Kind.IsVoice() == true ? "🔊" : "#";
var symbol = channel?.IsVoice == true ? "🔊" : "#";
var name = channel?.Name ?? "deleted-channel";
_buffer.Append(

@ -30,22 +30,22 @@ internal class HtmlMessageWriter : MessageWriter
if (_messageGroup.LastOrDefault() is not { } lastMessage)
return true;
// Reply messages cannot join existing groups because they need to appear first
if (message.Kind == MessageKind.Reply)
// Reply-like messages cannot join existing groups because they need to appear first
if (message.IsReplyLike)
return false;
// Grouping for system notifications
if (message.Kind.IsSystemNotification())
if (message.IsSystemNotification)
{
// Can only be grouped with other system notifications
if (!lastMessage.Kind.IsSystemNotification())
if (!lastMessage.IsSystemNotification)
return false;
}
// Grouping for normal messages
else
{
// Can only be grouped with other normal messages
if (lastMessage.Kind.IsSystemNotification())
if (lastMessage.IsSystemNotification)
return false;
// Messages must be within 7 minutes of each other

@ -273,8 +273,11 @@ internal class JsonMessageWriter : MessageWriter
_writer.WriteStartObject("channel");
_writer.WriteString("id", Context.Request.Channel.Id.ToString());
_writer.WriteString("type", Context.Request.Channel.Kind.ToString());
// Original schema did not account for threads, so 'category' actually refers to the parent channel
_writer.WriteString("categoryId", Context.Request.Channel.Parent?.Id.ToString());
_writer.WriteString("category", Context.Request.Channel.ParentNameWithFallback);
_writer.WriteString("category", Context.Request.Channel.Parent?.Name);
_writer.WriteString("name", Context.Request.Channel.Name);
_writer.WriteString("topic", Context.Request.Channel.Topic);
@ -329,7 +332,7 @@ internal class JsonMessageWriter : MessageWriter
_writer.WriteBoolean("isPinned", message.IsPinned);
// Content
if (message.Kind.IsSystemNotification())
if (message.IsSystemNotification)
{
_writer.WriteString("content", message.GetFallbackContent());
}

@ -47,7 +47,7 @@
<div id="chatlog__message-container-@message.Id" class="chatlog__message-container @(message.IsPinned ? "chatlog__message-container--pinned" : null)" data-message-id="@message.Id">
<div class="chatlog__message">
@* System notification *@
@if (message.Kind.IsSystemNotification())
@if (message.IsSystemNotification)
{
<div class="chatlog__message-aside">
<svg class="chatlog__system-notification-icon">
@ -329,7 +329,7 @@
<div class="chatlog__embed">
<div class="chatlog__embed-invite-container">
<div class="chatlog__embed-invite-title">@(invite.Channel?.Kind.IsDirect() == true ? "Invite to join a group DM" : "Invite to join a server")</div>
<div class="chatlog__embed-invite-title">@(invite.Channel?.IsDirect == true ? "Invite to join a group DM" : "Invite to join a server")</div>
<div class="chatlog__embed-invite">
<div class="chatlog__embed-invite-guild-icon-container">
<img class="chatlog__embed-invite-guild-icon" src="@await ResolveAssetUrlAsync(invite.Channel?.IconUrl ?? invite.Guild.IconUrl)" alt="Guild icon" loading="lazy">

@ -72,7 +72,7 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
_buffer.Append($"#{name}");
// Voice channel marker
if (channel?.Kind.IsVoice() == true)
if (channel?.IsVoice == true)
_buffer.Append(" [voice]");
}
else if (mention.Kind == MentionKind.Role)

@ -200,9 +200,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.ParentNameWithFallback} / {Context.Request.Channel.Name}"
);
await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.GetHierarchicalName()}");
if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
{
@ -238,7 +236,7 @@ internal class PlainTextMessageWriter : MessageWriter
await WriteMessageHeaderAsync(message);
// Content
if (message.Kind.IsSystemNotification())
if (message.IsSystemNotification)
{
await _writer.WriteLineAsync(message.GetFallbackContent());
}

@ -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.ParentNameWithFallback / @Context.Request.Channel.Name</div>
<div class="preamble__entry">@Context.Request.Channel.GetHierarchicalName()</div>
@if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
{

@ -13,10 +13,10 @@ public class ChannelToGroupKeyConverter : IValueConverter
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
value switch
{
Channel channel when channel.Kind.IsThread()
=> $"Threads in #{channel.ParentNameWithFallback}",
Channel { IsThread: true, Parent: not null } channel
=> $"Threads in #{channel.Parent.Name}",
Channel channel => channel.ParentNameWithFallback,
Channel channel => channel.Parent?.Name ?? "???",
_ => null
};

@ -43,8 +43,6 @@ public class DashboardViewModel : PropertyChangedBase
public Guild? SelectedGuild { get; set; }
public bool IsDirectMessageGuildSelected => SelectedGuild?.Id == Guild.DirectMessages.Id;
public IReadOnlyList<Channel>? AvailableChannels { get; private set; }
public IReadOnlyList<Channel>? SelectedChannels { get; set; }
@ -164,7 +162,7 @@ public class DashboardViewModel : PropertyChangedBase
// Regular channels
await foreach (var channel in _discord.GetGuildChannelsAsync(SelectedGuild.Id))
{
if (channel.Kind == ChannelKind.GuildCategory)
if (channel.IsCategory)
continue;
channels.Add(channel);

@ -313,10 +313,10 @@
<ListBox.Style>
<Style BasedOn="{StaticResource {x:Type ListBox}}" TargetType="{x:Type ListBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsDirectMessageGuildSelected}" Value="True">
<DataTrigger Binding="{Binding SelectedGuild.IsDirect}" Value="True">
<Setter Property="ItemsSource" Value="{Binding Source={StaticResource AvailableDirectChannelsViewSource}}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsDirectMessageGuildSelected}" Value="False">
<DataTrigger Binding="{Binding SelectedGuild.IsDirect}" Value="False">
<Setter Property="ItemsSource" Value="{Binding Source={StaticResource AvailableChannelsViewSource}}" />
</DataTrigger>
</Style.Triggers>

@ -60,8 +60,10 @@
FontWeight="Light"
TextTrimming="CharacterEllipsis"
Visibility="{Binding IsSingleChannel, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
<Run Text="{Binding Channels[0].ParentNameWithFallback, Mode=OneWay}" ToolTip="{Binding Channels[0].ParentNameWithFallback, Mode=OneWay}" />
<Run Text="/" />
<TextBlock Visibility="{Binding Channels[0].Parent, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
<Run Text="{Binding Channels[0].Parent.Name, Mode=OneWay}" ToolTip="{Binding Channels[0].Parent.Name, Mode=OneWay}" />
<Run Text="/" />
</TextBlock>
<Run
FontWeight="SemiBold"
Text="{Binding Channels[0].Name, Mode=OneWay}"

Loading…
Cancel
Save