mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-15 07:34:24 -04:00
parent
c405121c0b
commit
20a952aec5
12 changed files with 87 additions and 42 deletions
|
@ -80,6 +80,12 @@ public abstract class ExportCommandBase : TokenCommandBase
|
||||||
)]
|
)]
|
||||||
public int ParallelLimit { get; init; } = 1;
|
public int ParallelLimit { get; init; } = 1;
|
||||||
|
|
||||||
|
[CommandOption(
|
||||||
|
"markdown",
|
||||||
|
Description = "Process markdown, mentions, and other special tokens."
|
||||||
|
)]
|
||||||
|
public bool ShouldFormatMarkdown { get; init; } = true;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"media",
|
"media",
|
||||||
Description = "Download assets referenced by the export (user avatars, attached files, embedded images, etc.)."
|
Description = "Download assets referenced by the export (user avatars, attached files, embedded images, etc.)."
|
||||||
|
@ -171,6 +177,7 @@ public abstract class ExportCommandBase : TokenCommandBase
|
||||||
Before,
|
Before,
|
||||||
PartitionLimit,
|
PartitionLimit,
|
||||||
MessageFilter,
|
MessageFilter,
|
||||||
|
ShouldFormatMarkdown,
|
||||||
ShouldDownloadAssets,
|
ShouldDownloadAssets,
|
||||||
ShouldReuseAssets,
|
ShouldReuseAssets,
|
||||||
DateFormat
|
DateFormat
|
||||||
|
|
|
@ -18,10 +18,12 @@ internal partial class CsvMessageWriter : MessageWriter
|
||||||
_writer = new StreamWriter(stream);
|
_writer = new StreamWriter(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ValueTask<string> FormatMarkdownAsync(
|
private async ValueTask<string> FormatMarkdownAsync(
|
||||||
string markdown,
|
string markdown,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken);
|
Context.Request.ShouldFormatMarkdown
|
||||||
|
? await PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken)
|
||||||
|
: markdown;
|
||||||
|
|
||||||
public override async ValueTask WritePreambleAsync(CancellationToken cancellationToken = default) =>
|
public override async ValueTask WritePreambleAsync(CancellationToken cancellationToken = default) =>
|
||||||
await _writer.WriteLineAsync("AuthorID,Author,Date,Content,Attachments,Reactions");
|
await _writer.WriteLineAsync("AuthorID,Author,Date,Content,Attachments,Reactions");
|
||||||
|
|
|
@ -19,6 +19,7 @@ public partial record ExportRequest(
|
||||||
Snowflake? Before,
|
Snowflake? Before,
|
||||||
PartitionLimit PartitionLimit,
|
PartitionLimit PartitionLimit,
|
||||||
MessageFilter MessageFilter,
|
MessageFilter MessageFilter,
|
||||||
|
bool ShouldFormatMarkdown,
|
||||||
bool ShouldDownloadAssets,
|
bool ShouldDownloadAssets,
|
||||||
bool ShouldReuseAssets,
|
bool ShouldReuseAssets,
|
||||||
string DateFormat)
|
string DateFormat)
|
||||||
|
|
|
@ -75,7 +75,7 @@ internal class HtmlMessageWriter : MessageWriter
|
||||||
Minify(
|
Minify(
|
||||||
await new PreambleTemplate
|
await new PreambleTemplate
|
||||||
{
|
{
|
||||||
ExportContext = Context,
|
Context = Context,
|
||||||
ThemeName = _themeName
|
ThemeName = _themeName
|
||||||
}.RenderAsync(cancellationToken)
|
}.RenderAsync(cancellationToken)
|
||||||
)
|
)
|
||||||
|
@ -90,7 +90,7 @@ internal class HtmlMessageWriter : MessageWriter
|
||||||
Minify(
|
Minify(
|
||||||
await new MessageGroupTemplate
|
await new MessageGroupTemplate
|
||||||
{
|
{
|
||||||
ExportContext = Context,
|
Context = Context,
|
||||||
Messages = messages
|
Messages = messages
|
||||||
}.RenderAsync(cancellationToken)
|
}.RenderAsync(cancellationToken)
|
||||||
)
|
)
|
||||||
|
|
|
@ -28,10 +28,12 @@ internal class JsonMessageWriter : MessageWriter
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private ValueTask<string> FormatMarkdownAsync(
|
private async ValueTask<string> FormatMarkdownAsync(
|
||||||
string markdown,
|
string markdown,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken);
|
Context.Request.ShouldFormatMarkdown
|
||||||
|
? await PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken)
|
||||||
|
: markdown;
|
||||||
|
|
||||||
private async ValueTask WriteAttachmentAsync(
|
private async ValueTask WriteAttachmentAsync(
|
||||||
Attachment attachment,
|
Attachment attachment,
|
||||||
|
|
|
@ -10,40 +10,44 @@
|
||||||
@inherits RazorBlade.HtmlTemplate
|
@inherits RazorBlade.HtmlTemplate
|
||||||
|
|
||||||
@functions {
|
@functions {
|
||||||
public required ExportContext ExportContext { get; init; }
|
public required ExportContext Context { get; init; }
|
||||||
|
|
||||||
public required IReadOnlyList<Message> Messages { get; init; }
|
public required IReadOnlyList<Message> Messages { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
||||||
ExportContext.ResolveAssetUrlAsync(url, CancellationToken);
|
Context.ResolveAssetUrlAsync(url, CancellationToken);
|
||||||
|
|
||||||
string FormatDate(DateTimeOffset instant) =>
|
string FormatDate(DateTimeOffset instant) =>
|
||||||
ExportContext.FormatDate(instant);
|
Context.FormatDate(instant);
|
||||||
|
|
||||||
ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
async ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
||||||
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken);
|
Context.Request.ShouldFormatMarkdown
|
||||||
|
? await HtmlMarkdownVisitor.FormatAsync(Context, markdown, true, CancellationToken)
|
||||||
|
: markdown;
|
||||||
|
|
||||||
ValueTask<string> FormatEmbedMarkdownAsync(string markdown) =>
|
async ValueTask<string> FormatEmbedMarkdownAsync(string markdown) =>
|
||||||
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, false, CancellationToken);
|
Context.Request.ShouldFormatMarkdown
|
||||||
|
? await HtmlMarkdownVisitor.FormatAsync(Context, markdown, false, CancellationToken)
|
||||||
|
: markdown;
|
||||||
|
|
||||||
var firstMessage = Messages.First();
|
var firstMessage = Messages.First();
|
||||||
|
|
||||||
var userMember = ExportContext.TryGetMember(firstMessage.Author.Id);
|
var userMember = Context.TryGetMember(firstMessage.Author.Id);
|
||||||
|
|
||||||
var userColor = ExportContext.TryGetUserColor(firstMessage.Author.Id);
|
var userColor = Context.TryGetUserColor(firstMessage.Author.Id);
|
||||||
|
|
||||||
var userNick = firstMessage.Author.IsBot
|
var userNick = firstMessage.Author.IsBot
|
||||||
? firstMessage.Author.Name
|
? firstMessage.Author.Name
|
||||||
: userMember?.Nick ?? firstMessage.Author.Name;
|
: userMember?.Nick ?? firstMessage.Author.Name;
|
||||||
|
|
||||||
var referencedUserMember = firstMessage.ReferencedMessage is not null
|
var referencedUserMember = firstMessage.ReferencedMessage is not null
|
||||||
? ExportContext.TryGetMember(firstMessage.ReferencedMessage.Author.Id)
|
? Context.TryGetMember(firstMessage.ReferencedMessage.Author.Id)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var referencedUserColor = firstMessage.ReferencedMessage is not null
|
var referencedUserColor = firstMessage.ReferencedMessage is not null
|
||||||
? ExportContext.TryGetUserColor(firstMessage.ReferencedMessage.Author.Id)
|
? Context.TryGetUserColor(firstMessage.ReferencedMessage.Author.Id)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var referencedUserNick = firstMessage.ReferencedMessage is not null
|
var referencedUserNick = firstMessage.ReferencedMessage is not null
|
||||||
|
@ -65,8 +69,8 @@
|
||||||
{
|
{
|
||||||
// System notifications are grouped even if the message author is different.
|
// System notifications are grouped even if the message author is different.
|
||||||
// That's why we have to update the user values with the author of the current message.
|
// That's why we have to update the user values with the author of the current message.
|
||||||
userMember = ExportContext.TryGetMember(message.Author.Id);
|
userMember = Context.TryGetMember(message.Author.Id);
|
||||||
userColor = ExportContext.TryGetUserColor(message.Author.Id);
|
userColor = Context.TryGetUserColor(message.Author.Id);
|
||||||
userNick = message.Author.IsBot
|
userNick = message.Author.IsBot
|
||||||
? message.Author.Name
|
? message.Author.Name
|
||||||
: userMember?.Nick ?? message.Author.Name;
|
: userMember?.Nick ?? message.Author.Name;
|
||||||
|
@ -323,7 +327,7 @@
|
||||||
|
|
||||||
foreach (var inviteCode in inviteCodes)
|
foreach (var inviteCode in inviteCodes)
|
||||||
{
|
{
|
||||||
var invite = await ExportContext.Discord.TryGetGuildInviteAsync(inviteCode, CancellationToken);
|
var invite = await Context.Discord.TryGetGuildInviteAsync(inviteCode, CancellationToken);
|
||||||
if (invite is null)
|
if (invite is null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -18,10 +18,12 @@ internal class PlainTextMessageWriter : MessageWriter
|
||||||
_writer = new StreamWriter(stream);
|
_writer = new StreamWriter(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ValueTask<string> FormatMarkdownAsync(
|
private async ValueTask<string> FormatMarkdownAsync(
|
||||||
string markdown,
|
string markdown,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken);
|
Context.Request.ShouldFormatMarkdown
|
||||||
|
? await PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken)
|
||||||
|
: markdown;
|
||||||
|
|
||||||
private async ValueTask WriteMessageHeaderAsync(Message message)
|
private async ValueTask WriteMessageHeaderAsync(Message message)
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
@inherits RazorBlade.HtmlTemplate
|
@inherits RazorBlade.HtmlTemplate
|
||||||
|
|
||||||
@functions {
|
@functions {
|
||||||
public required ExportContext ExportContext { get; init; }
|
public required ExportContext Context { get; init; }
|
||||||
|
|
||||||
public required string ThemeName { get; init; }
|
public required string ThemeName { get; init; }
|
||||||
}
|
}
|
||||||
|
@ -19,20 +19,22 @@
|
||||||
$"https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-{weight}.woff";
|
$"https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-{weight}.woff";
|
||||||
|
|
||||||
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
||||||
ExportContext.ResolveAssetUrlAsync(url, CancellationToken);
|
Context.ResolveAssetUrlAsync(url, CancellationToken);
|
||||||
|
|
||||||
string FormatDate(DateTimeOffset instant) =>
|
string FormatDate(DateTimeOffset instant) =>
|
||||||
ExportContext.FormatDate(instant);
|
Context.FormatDate(instant);
|
||||||
|
|
||||||
ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
async ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
||||||
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken);
|
Context.Request.ShouldFormatMarkdown
|
||||||
|
? await HtmlMarkdownVisitor.FormatAsync(Context, markdown, true, CancellationToken)
|
||||||
|
: markdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>@ExportContext.Request.Guild.Name - @ExportContext.Request.Channel.Name</title>
|
<title>@Context.Request.Guild.Name - @Context.Request.Channel.Name</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
|
|
||||||
|
@ -914,31 +916,31 @@
|
||||||
|
|
||||||
<div class="preamble">
|
<div class="preamble">
|
||||||
<div class="preamble__guild-icon-container">
|
<div class="preamble__guild-icon-container">
|
||||||
<img class="preamble__guild-icon" src="@await ResolveAssetUrlAsync(ExportContext.Request.Channel.IconUrl ?? ExportContext.Request.Guild.IconUrl)" alt="Guild icon" loading="lazy">
|
<img class="preamble__guild-icon" src="@await ResolveAssetUrlAsync(Context.Request.Channel.IconUrl ?? Context.Request.Guild.IconUrl)" alt="Guild icon" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<div class="preamble__entries-container">
|
<div class="preamble__entries-container">
|
||||||
<div class="preamble__entry">@ExportContext.Request.Guild.Name</div>
|
<div class="preamble__entry">@Context.Request.Guild.Name</div>
|
||||||
<div class="preamble__entry">@ExportContext.Request.Channel.Category.Name / @ExportContext.Request.Channel.Name</div>
|
<div class="preamble__entry">@Context.Request.Channel.Category.Name / @Context.Request.Channel.Name</div>
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(ExportContext.Request.Channel.Topic))
|
@if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
|
||||||
{
|
{
|
||||||
<div class="preamble__entry preamble__entry--small">@Html.Raw(await FormatMarkdownAsync(ExportContext.Request.Channel.Topic))</div>
|
<div class="preamble__entry preamble__entry--small">@Html.Raw(await FormatMarkdownAsync(Context.Request.Channel.Topic))</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (ExportContext.Request.After is not null || ExportContext.Request.Before is not null)
|
@if (Context.Request.After is not null || Context.Request.Before is not null)
|
||||||
{
|
{
|
||||||
<div class="preamble__entry preamble__entry--small">
|
<div class="preamble__entry preamble__entry--small">
|
||||||
@if (ExportContext.Request.After is not null && ExportContext.Request.Before is not null)
|
@if (Context.Request.After is not null && Context.Request.Before is not null)
|
||||||
{
|
{
|
||||||
@($"Between {FormatDate(ExportContext.Request.After.Value.ToDate())} and {FormatDate(ExportContext.Request.Before.Value.ToDate())}")
|
@($"Between {FormatDate(Context.Request.After.Value.ToDate())} and {FormatDate(Context.Request.Before.Value.ToDate())}")
|
||||||
}
|
}
|
||||||
else if (ExportContext.Request.After is not null)
|
else if (Context.Request.After is not null)
|
||||||
{
|
{
|
||||||
@($"After {FormatDate(ExportContext.Request.After.Value.ToDate())}")
|
@($"After {FormatDate(Context.Request.After.Value.ToDate())}")
|
||||||
}
|
}
|
||||||
else if (ExportContext.Request.Before is not null)
|
else if (Context.Request.Before is not null)
|
||||||
{
|
{
|
||||||
@($"Before {FormatDate(ExportContext.Request.Before.Value.ToDate())}")
|
@($"Before {FormatDate(Context.Request.Before.Value.ToDate())}")
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@ public partial class SettingsService : SettingsManager
|
||||||
|
|
||||||
public string? LastMessageFilterValue { get; set; }
|
public string? LastMessageFilterValue { get; set; }
|
||||||
|
|
||||||
|
public bool LastShouldFormatMarkdown { get; set; } = true;
|
||||||
|
|
||||||
public bool LastShouldDownloadAssets { get; set; }
|
public bool LastShouldDownloadAssets { get; set; }
|
||||||
|
|
||||||
public SettingsService()
|
public SettingsService()
|
||||||
|
|
|
@ -191,6 +191,7 @@ public class DashboardViewModel : PropertyChangedBase
|
||||||
dialog.Before?.Pipe(Snowflake.FromDate),
|
dialog.Before?.Pipe(Snowflake.FromDate),
|
||||||
dialog.PartitionLimit,
|
dialog.PartitionLimit,
|
||||||
dialog.MessageFilter,
|
dialog.MessageFilter,
|
||||||
|
dialog.ShouldFormatMarkdown,
|
||||||
dialog.ShouldDownloadAssets,
|
dialog.ShouldDownloadAssets,
|
||||||
_settingsService.ShouldReuseAssets,
|
_settingsService.ShouldReuseAssets,
|
||||||
_settingsService.DateFormat
|
_settingsService.DateFormat
|
||||||
|
|
|
@ -59,6 +59,8 @@ public class ExportSetupViewModel : DialogScreen
|
||||||
? MessageFilter.Parse(MessageFilterValue)
|
? MessageFilter.Parse(MessageFilterValue)
|
||||||
: MessageFilter.Null;
|
: MessageFilter.Null;
|
||||||
|
|
||||||
|
public bool ShouldFormatMarkdown { get; set; }
|
||||||
|
|
||||||
public bool ShouldDownloadAssets { get; set; }
|
public bool ShouldDownloadAssets { get; set; }
|
||||||
|
|
||||||
public bool IsAdvancedSectionDisplayed { get; set; }
|
public bool IsAdvancedSectionDisplayed { get; set; }
|
||||||
|
@ -72,6 +74,7 @@ public class ExportSetupViewModel : DialogScreen
|
||||||
SelectedFormat = _settingsService.LastExportFormat;
|
SelectedFormat = _settingsService.LastExportFormat;
|
||||||
PartitionLimitValue = _settingsService.LastPartitionLimitValue;
|
PartitionLimitValue = _settingsService.LastPartitionLimitValue;
|
||||||
MessageFilterValue = _settingsService.LastMessageFilterValue;
|
MessageFilterValue = _settingsService.LastMessageFilterValue;
|
||||||
|
ShouldFormatMarkdown = _settingsService.LastShouldFormatMarkdown;
|
||||||
ShouldDownloadAssets = _settingsService.LastShouldDownloadAssets;
|
ShouldDownloadAssets = _settingsService.LastShouldDownloadAssets;
|
||||||
|
|
||||||
// Show the "advanced options" section by default if any
|
// Show the "advanced options" section by default if any
|
||||||
|
@ -129,6 +132,7 @@ public class ExportSetupViewModel : DialogScreen
|
||||||
_settingsService.LastExportFormat = SelectedFormat;
|
_settingsService.LastExportFormat = SelectedFormat;
|
||||||
_settingsService.LastPartitionLimitValue = PartitionLimitValue;
|
_settingsService.LastPartitionLimitValue = PartitionLimitValue;
|
||||||
_settingsService.LastMessageFilterValue = MessageFilterValue;
|
_settingsService.LastMessageFilterValue = MessageFilterValue;
|
||||||
|
_settingsService.LastShouldFormatMarkdown = ShouldFormatMarkdown;
|
||||||
_settingsService.LastShouldDownloadAssets = ShouldDownloadAssets;
|
_settingsService.LastShouldDownloadAssets = ShouldDownloadAssets;
|
||||||
|
|
||||||
Close(true);
|
Close(true);
|
||||||
|
|
|
@ -230,10 +230,28 @@
|
||||||
materialDesign:HintAssist.IsFloating="True"
|
materialDesign:HintAssist.IsFloating="True"
|
||||||
Style="{DynamicResource MaterialDesignOutlinedTextBox}"
|
Style="{DynamicResource MaterialDesignOutlinedTextBox}"
|
||||||
Text="{Binding MessageFilterValue}"
|
Text="{Binding MessageFilterValue}"
|
||||||
ToolTip="Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image')." />
|
ToolTip="Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image')" />
|
||||||
|
|
||||||
|
<!-- Markdown formatting -->
|
||||||
|
<Grid Margin="16,8" ToolTip="Process markdown, mentions, and other special tokens">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="Format markdown" />
|
||||||
|
<ToggleButton
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding ShouldFormatMarkdown}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!-- Download assets -->
|
<!-- Download assets -->
|
||||||
<Grid Margin="16,16" ToolTip="Download assets referenced by the export (user avatars, attached files, embedded images, etc.)">
|
<Grid Margin="16,8" ToolTip="Download assets referenced by the export (user avatars, attached files, embedded images, etc.)">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue