Refactor using c# 12 features

This commit is contained in:
Tyrrrz 2023-12-10 22:32:45 +02:00
parent 174b92cbb0
commit 619fe9ccf7
30 changed files with 155 additions and 290 deletions

View file

@ -5,11 +5,9 @@ using PathEx = System.IO.Path;
namespace DiscordChatExporter.Cli.Tests.Utils; namespace DiscordChatExporter.Cli.Tests.Utils;
internal partial class TempDir : IDisposable internal partial class TempDir(string path) : IDisposable
{ {
public string Path { get; } public string Path { get; } = path;
public TempDir(string path) => Path = path;
public void Dispose() public void Dispose()
{ {

View file

@ -5,11 +5,9 @@ using PathEx = System.IO.Path;
namespace DiscordChatExporter.Cli.Tests.Utils; namespace DiscordChatExporter.Cli.Tests.Utils;
internal partial class TempFile : IDisposable internal partial class TempFile(string path) : IDisposable
{ {
public string Path { get; } public string Path { get; } = path;
public TempFile(string path) => Path = path;
public void Dispose() public void Dispose()
{ {

View file

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Infrastructure; using CliFx.Infrastructure;

View file

@ -18,15 +18,11 @@ using JsonExtensions.Reading;
namespace DiscordChatExporter.Core.Discord; namespace DiscordChatExporter.Core.Discord;
public class DiscordClient public class DiscordClient(string token)
{ {
private readonly string _token;
private readonly Uri _baseUri = new("https://discord.com/api/v10/", UriKind.Absolute); private readonly Uri _baseUri = new("https://discord.com/api/v10/", UriKind.Absolute);
private TokenKind? _resolvedTokenKind; private TokenKind? _resolvedTokenKind;
public DiscordClient(string token) => _token = token;
private async ValueTask<HttpResponseMessage> GetResponseAsync( private async ValueTask<HttpResponseMessage> GetResponseAsync(
string url, string url,
TokenKind tokenKind, TokenKind tokenKind,
@ -44,7 +40,7 @@ public class DiscordClient
.Headers .Headers
.TryAddWithoutValidation( .TryAddWithoutValidation(
"Authorization", "Authorization",
tokenKind == TokenKind.Bot ? $"Bot {_token}" : _token tokenKind == TokenKind.Bot ? $"Bot {token}" : token
); );
var response = await Http.Client.SendAsync( var response = await Http.Client.SendAsync(

View file

@ -8,11 +8,9 @@ using JsonExtensions.Reading;
namespace DiscordChatExporter.Core.Discord.Dump; namespace DiscordChatExporter.Core.Discord.Dump;
public partial class DataDump public partial class DataDump(IReadOnlyList<DataDumpChannel> channels)
{ {
public IReadOnlyList<DataDumpChannel> Channels { get; } public IReadOnlyList<DataDumpChannel> Channels { get; } = channels;
public DataDump(IReadOnlyList<DataDumpChannel> channels) => Channels = channels;
} }
public partial class DataDump public partial class DataDump

View file

@ -2,17 +2,11 @@
namespace DiscordChatExporter.Core.Exceptions; namespace DiscordChatExporter.Core.Exceptions;
public class DiscordChatExporterException : Exception public class DiscordChatExporterException(
{
public bool IsFatal { get; }
public DiscordChatExporterException(
string message, string message,
bool isFatal = false, bool isFatal = false,
Exception? innerException = null Exception? innerException = null
) ) : Exception(message, innerException)
: base(message, innerException) {
{ public bool IsFatal { get; } = isFatal;
IsFatal = isFatal;
}
} }

View file

@ -10,8 +10,6 @@ namespace DiscordChatExporter.Core.Exporting;
public class ChannelExporter(DiscordClient discord) public class ChannelExporter(DiscordClient discord)
{ {
private readonly DiscordClient _discord = discord;
public async ValueTask ExportChannelAsync( public async ValueTask ExportChannelAsync(
ExportRequest request, ExportRequest request,
IProgress<Percentage>? progress = null, IProgress<Percentage>? progress = null,
@ -63,13 +61,13 @@ public class ChannelExporter(DiscordClient discord)
} }
// Build context // Build context
var context = new ExportContext(_discord, request); var context = new ExportContext(discord, request);
await context.PopulateChannelsAndRolesAsync(cancellationToken); await context.PopulateChannelsAndRolesAsync(cancellationToken);
// Export messages // Export messages
await using var messageExporter = new MessageExporter(context); await using var messageExporter = new MessageExporter(context);
await foreach ( await foreach (
var message in _discord.GetMessagesAsync( var message in discord.GetMessagesAsync(
request.Channel.Id, request.Channel.Id,
request.After, request.After,
request.Before, request.Before,

View file

@ -23,9 +23,6 @@ internal partial class ExportAssetDownloader(string workingDirPath, bool reuse)
o.PoolInitialFill = 1; o.PoolInitialFill = 1;
}); });
private readonly string _workingDirPath = workingDirPath;
private readonly bool _reuse = reuse;
// File paths of the previously downloaded assets // File paths of the previously downloaded assets
private readonly Dictionary<string, string> _previousPathsByUrl = new(StringComparer.Ordinal); private readonly Dictionary<string, string> _previousPathsByUrl = new(StringComparer.Ordinal);
@ -35,7 +32,7 @@ internal partial class ExportAssetDownloader(string workingDirPath, bool reuse)
) )
{ {
var fileName = GetFileNameFromUrl(url); var fileName = GetFileNameFromUrl(url);
var filePath = Path.Combine(_workingDirPath, fileName); var filePath = Path.Combine(workingDirPath, fileName);
using var _ = await Locker.LockAsync(filePath, cancellationToken); using var _ = await Locker.LockAsync(filePath, cancellationToken);
@ -43,10 +40,10 @@ internal partial class ExportAssetDownloader(string workingDirPath, bool reuse)
return cachedFilePath; return cachedFilePath;
// Reuse existing files if we're allowed to // Reuse existing files if we're allowed to
if (_reuse && File.Exists(filePath)) if (reuse && File.Exists(filePath))
return _previousPathsByUrl[url] = filePath; return _previousPathsByUrl[url] = filePath;
Directory.CreateDirectory(_workingDirPath); Directory.CreateDirectory(workingDirPath);
await Http.ResiliencePipeline.ExecuteAsync( await Http.ResiliencePipeline.ExecuteAsync(
async innerCancellationToken => async innerCancellationToken =>

View file

@ -3,28 +3,17 @@ using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Core.Exporting.Filtering; namespace DiscordChatExporter.Core.Exporting.Filtering;
internal class BinaryExpressionMessageFilter : MessageFilter internal class BinaryExpressionMessageFilter(
{
private readonly MessageFilter _first;
private readonly MessageFilter _second;
private readonly BinaryExpressionKind _kind;
public BinaryExpressionMessageFilter(
MessageFilter first, MessageFilter first,
MessageFilter second, MessageFilter second,
BinaryExpressionKind kind BinaryExpressionKind kind
) ) : MessageFilter
{ {
_first = first;
_second = second;
_kind = kind;
}
public override bool IsMatch(Message message) => public override bool IsMatch(Message message) =>
_kind switch kind switch
{ {
BinaryExpressionKind.Or => _first.IsMatch(message) || _second.IsMatch(message), BinaryExpressionKind.Or => first.IsMatch(message) || second.IsMatch(message),
BinaryExpressionKind.And => _first.IsMatch(message) && _second.IsMatch(message), BinaryExpressionKind.And => first.IsMatch(message) && second.IsMatch(message),
_ => throw new InvalidOperationException($"Unknown binary expression kind '{_kind}'.") _ => throw new InvalidOperationException($"Unknown binary expression kind '{kind}'.")
}; };
} }

View file

@ -4,12 +4,8 @@ using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Core.Exporting.Filtering; namespace DiscordChatExporter.Core.Exporting.Filtering;
internal class ContainsMessageFilter : MessageFilter internal class ContainsMessageFilter(string text) : MessageFilter
{ {
private readonly string _text;
public ContainsMessageFilter(string text) => _text = text;
// Match content within word boundaries, between spaces, or as the whole input. // Match content within word boundaries, between spaces, or as the whole input.
// For example, "max" shouldn't match on content "our maximum effort", // For example, "max" shouldn't match on content "our maximum effort",
// but should match on content "our max effort". // but should match on content "our max effort".
@ -20,7 +16,7 @@ internal class ContainsMessageFilter : MessageFilter
!string.IsNullOrWhiteSpace(content) !string.IsNullOrWhiteSpace(content)
&& Regex.IsMatch( && Regex.IsMatch(
content, content,
@"(?:\b|\s|^)" + Regex.Escape(_text) + @"(?:\b|\s|$)", @"(?:\b|\s|^)" + Regex.Escape(text) + @"(?:\b|\s|$)",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
); );

View file

@ -3,15 +3,11 @@ using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Core.Exporting.Filtering; namespace DiscordChatExporter.Core.Exporting.Filtering;
internal class FromMessageFilter : MessageFilter internal class FromMessageFilter(string value) : MessageFilter
{ {
private readonly string _value;
public FromMessageFilter(string value) => _value = value;
public override bool IsMatch(Message message) => public override bool IsMatch(Message message) =>
string.Equals(_value, message.Author.Name, StringComparison.OrdinalIgnoreCase) string.Equals(value, message.Author.Name, StringComparison.OrdinalIgnoreCase)
|| string.Equals(_value, message.Author.DisplayName, StringComparison.OrdinalIgnoreCase) || string.Equals(value, message.Author.DisplayName, StringComparison.OrdinalIgnoreCase)
|| string.Equals(_value, message.Author.FullName, StringComparison.OrdinalIgnoreCase) || string.Equals(value, message.Author.FullName, StringComparison.OrdinalIgnoreCase)
|| string.Equals(_value, message.Author.Id.ToString(), StringComparison.OrdinalIgnoreCase); || string.Equals(value, message.Author.Id.ToString(), StringComparison.OrdinalIgnoreCase);
} }

View file

@ -5,14 +5,10 @@ using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Core.Exporting.Filtering; namespace DiscordChatExporter.Core.Exporting.Filtering;
internal class HasMessageFilter : MessageFilter internal class HasMessageFilter(MessageContentMatchKind kind) : MessageFilter
{ {
private readonly MessageContentMatchKind _kind;
public HasMessageFilter(MessageContentMatchKind kind) => _kind = kind;
public override bool IsMatch(Message message) => public override bool IsMatch(Message message) =>
_kind switch kind switch
{ {
MessageContentMatchKind.Link MessageContentMatchKind.Link
=> Regex.IsMatch(message.Content, "https?://\\S*[^\\.,:;\"\'\\s]"), => Regex.IsMatch(message.Content, "https?://\\S*[^\\.,:;\"\'\\s]"),
@ -24,7 +20,7 @@ internal class HasMessageFilter : MessageFilter
MessageContentMatchKind.Pin => message.IsPinned, MessageContentMatchKind.Pin => message.IsPinned,
_ _
=> throw new InvalidOperationException( => throw new InvalidOperationException(
$"Unknown message content match kind '{_kind}'." $"Unknown message content match kind '{kind}'."
) )
}; };
} }

View file

@ -4,20 +4,16 @@ using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Core.Exporting.Filtering; namespace DiscordChatExporter.Core.Exporting.Filtering;
internal class MentionsMessageFilter : MessageFilter internal class MentionsMessageFilter(string value) : MessageFilter
{ {
private readonly string _value;
public MentionsMessageFilter(string value) => _value = value;
public override bool IsMatch(Message message) => public override bool IsMatch(Message message) =>
message message
.MentionedUsers .MentionedUsers
.Any( .Any(
user => user =>
string.Equals(_value, user.Name, StringComparison.OrdinalIgnoreCase) string.Equals(value, user.Name, StringComparison.OrdinalIgnoreCase)
|| string.Equals(_value, user.DisplayName, StringComparison.OrdinalIgnoreCase) || string.Equals(value, user.DisplayName, StringComparison.OrdinalIgnoreCase)
|| string.Equals(_value, user.FullName, StringComparison.OrdinalIgnoreCase) || string.Equals(value, user.FullName, StringComparison.OrdinalIgnoreCase)
|| string.Equals(_value, user.Id.ToString(), StringComparison.OrdinalIgnoreCase) || string.Equals(value, user.Id.ToString(), StringComparison.OrdinalIgnoreCase)
); );
} }

View file

@ -2,11 +2,7 @@
namespace DiscordChatExporter.Core.Exporting.Filtering; namespace DiscordChatExporter.Core.Exporting.Filtering;
internal class NegatedMessageFilter : MessageFilter internal class NegatedMessageFilter(MessageFilter filter) : MessageFilter
{ {
private readonly MessageFilter _filter; public override bool IsMatch(Message message) => !filter.IsMatch(message);
public NegatedMessageFilter(MessageFilter filter) => _filter = filter;
public override bool IsMatch(Message message) => !_filter.IsMatch(message);
} }

View file

@ -4,23 +4,15 @@ using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Core.Exporting.Filtering; namespace DiscordChatExporter.Core.Exporting.Filtering;
internal class ReactionMessageFilter : MessageFilter internal class ReactionMessageFilter(string value) : MessageFilter
{ {
private readonly string _value;
public ReactionMessageFilter(string value) => _value = value;
public override bool IsMatch(Message message) => public override bool IsMatch(Message message) =>
message message
.Reactions .Reactions
.Any( .Any(
r => r =>
string.Equals( string.Equals(value, r.Emoji.Id?.ToString(), StringComparison.OrdinalIgnoreCase)
_value, || string.Equals(value, r.Emoji.Name, StringComparison.OrdinalIgnoreCase)
r.Emoji.Id?.ToString(), || string.Equals(value, r.Emoji.Code, StringComparison.OrdinalIgnoreCase)
StringComparison.OrdinalIgnoreCase
)
|| string.Equals(_value, r.Emoji.Name, StringComparison.OrdinalIgnoreCase)
|| string.Equals(_value, r.Emoji.Code, StringComparison.OrdinalIgnoreCase)
); );
} }

View file

@ -18,16 +18,12 @@ internal partial class HtmlMarkdownVisitor(
bool isJumbo bool isJumbo
) : MarkdownVisitor ) : MarkdownVisitor
{ {
private readonly ExportContext _context = context;
private readonly StringBuilder _buffer = buffer;
private readonly bool _isJumbo = isJumbo;
protected override ValueTask VisitTextAsync( protected override ValueTask VisitTextAsync(
TextNode text, TextNode text,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
_buffer.Append(HtmlEncode(text.Text)); buffer.Append(HtmlEncode(text.Text));
return default; return default;
} }
@ -92,9 +88,9 @@ internal partial class HtmlMarkdownVisitor(
) )
}; };
_buffer.Append(openingTag); buffer.Append(openingTag);
await VisitAsync(formatting.Children, cancellationToken); await VisitAsync(formatting.Children, cancellationToken);
_buffer.Append(closingTag); buffer.Append(closingTag);
} }
protected override async ValueTask VisitHeadingAsync( protected override async ValueTask VisitHeadingAsync(
@ -102,14 +98,14 @@ internal partial class HtmlMarkdownVisitor(
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
_buffer.Append( buffer.Append(
// lang=html // lang=html
$"<h{heading.Level}>" $"<h{heading.Level}>"
); );
await VisitAsync(heading.Children, cancellationToken); await VisitAsync(heading.Children, cancellationToken);
_buffer.Append( buffer.Append(
// lang=html // lang=html
$"</h{heading.Level}>" $"</h{heading.Level}>"
); );
@ -120,14 +116,14 @@ internal partial class HtmlMarkdownVisitor(
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
_buffer.Append( buffer.Append(
// lang=html // lang=html
"<ul>" "<ul>"
); );
await VisitAsync(list.Items, cancellationToken); await VisitAsync(list.Items, cancellationToken);
_buffer.Append( buffer.Append(
// lang=html // lang=html
"</ul>" "</ul>"
); );
@ -138,14 +134,14 @@ internal partial class HtmlMarkdownVisitor(
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
_buffer.Append( buffer.Append(
// lang=html // lang=html
"<li>" "<li>"
); );
await VisitAsync(listItem.Children, cancellationToken); await VisitAsync(listItem.Children, cancellationToken);
_buffer.Append( buffer.Append(
// lang=html // lang=html
"</li>" "</li>"
); );
@ -156,7 +152,7 @@ internal partial class HtmlMarkdownVisitor(
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
_buffer.Append( buffer.Append(
// lang=html // lang=html
$""" $"""
<code class="chatlog__markdown-pre chatlog__markdown-pre--inline">{HtmlEncode(inlineCodeBlock.Code)}</code> <code class="chatlog__markdown-pre chatlog__markdown-pre--inline">{HtmlEncode(inlineCodeBlock.Code)}</code>
@ -175,7 +171,7 @@ internal partial class HtmlMarkdownVisitor(
? $"language-{multiLineCodeBlock.Language}" ? $"language-{multiLineCodeBlock.Language}"
: "nohighlight"; : "nohighlight";
_buffer.Append( buffer.Append(
// lang=html // lang=html
$""" $"""
<code class="chatlog__markdown-pre chatlog__markdown-pre--multiline {highlightClass}">{HtmlEncode(multiLineCodeBlock.Code)}</code> <code class="chatlog__markdown-pre chatlog__markdown-pre--multiline {highlightClass}">{HtmlEncode(multiLineCodeBlock.Code)}</code>
@ -196,7 +192,7 @@ internal partial class HtmlMarkdownVisitor(
.Groups[1] .Groups[1]
.Value; .Value;
_buffer.Append( buffer.Append(
!string.IsNullOrWhiteSpace(linkedMessageId) !string.IsNullOrWhiteSpace(linkedMessageId)
// lang=html // lang=html
? $"""<a href="{HtmlEncode(link.Url)}" onclick="scrollToMessage(event, '{linkedMessageId}')">""" ? $"""<a href="{HtmlEncode(link.Url)}" onclick="scrollToMessage(event, '{linkedMessageId}')">"""
@ -206,7 +202,7 @@ internal partial class HtmlMarkdownVisitor(
await VisitAsync(link.Children, cancellationToken); await VisitAsync(link.Children, cancellationToken);
_buffer.Append( buffer.Append(
// lang=html // lang=html
"</a>" "</a>"
); );
@ -218,9 +214,9 @@ internal partial class HtmlMarkdownVisitor(
) )
{ {
var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated); var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated);
var jumboClass = _isJumbo ? "chatlog__emoji--large" : ""; var jumboClass = isJumbo ? "chatlog__emoji--large" : "";
_buffer.Append( buffer.Append(
// lang=html // lang=html
$""" $"""
<img <img
@ -228,7 +224,7 @@ internal partial class HtmlMarkdownVisitor(
class="chatlog__emoji {jumboClass}" class="chatlog__emoji {jumboClass}"
alt="{emoji.Name}" alt="{emoji.Name}"
title="{emoji.Code}" title="{emoji.Code}"
src="{await _context.ResolveAssetUrlAsync(emojiImageUrl, cancellationToken)}"> src="{await context.ResolveAssetUrlAsync(emojiImageUrl, cancellationToken)}">
""" """
); );
} }
@ -240,7 +236,7 @@ internal partial class HtmlMarkdownVisitor(
{ {
if (mention.Kind == MentionKind.Everyone) if (mention.Kind == MentionKind.Everyone)
{ {
_buffer.Append( buffer.Append(
// lang=html // lang=html
""" """
<span class="chatlog__markdown-mention">@everyone</span> <span class="chatlog__markdown-mention">@everyone</span>
@ -249,7 +245,7 @@ internal partial class HtmlMarkdownVisitor(
} }
else if (mention.Kind == MentionKind.Here) else if (mention.Kind == MentionKind.Here)
{ {
_buffer.Append( buffer.Append(
// lang=html // lang=html
""" """
<span class="chatlog__markdown-mention">@here</span> <span class="chatlog__markdown-mention">@here</span>
@ -262,13 +258,13 @@ internal partial class HtmlMarkdownVisitor(
// which means they need to be populated on demand. // which means they need to be populated on demand.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/304 // https://github.com/Tyrrrz/DiscordChatExporter/issues/304
if (mention.TargetId is not null) if (mention.TargetId is not null)
await _context.PopulateMemberAsync(mention.TargetId.Value, cancellationToken); await context.PopulateMemberAsync(mention.TargetId.Value, cancellationToken);
var member = mention.TargetId?.Pipe(_context.TryGetMember); var member = mention.TargetId?.Pipe(context.TryGetMember);
var fullName = member?.User.FullName ?? "Unknown"; var fullName = member?.User.FullName ?? "Unknown";
var displayName = member?.DisplayName ?? member?.User.DisplayName ?? "Unknown"; var displayName = member?.DisplayName ?? member?.User.DisplayName ?? "Unknown";
_buffer.Append( buffer.Append(
// lang=html // lang=html
$""" $"""
<span class="chatlog__markdown-mention" title="{HtmlEncode(fullName)}">@{HtmlEncode(displayName)}</span> <span class="chatlog__markdown-mention" title="{HtmlEncode(fullName)}">@{HtmlEncode(displayName)}</span>
@ -277,11 +273,11 @@ internal partial class HtmlMarkdownVisitor(
} }
else if (mention.Kind == MentionKind.Channel) else if (mention.Kind == MentionKind.Channel)
{ {
var channel = mention.TargetId?.Pipe(_context.TryGetChannel); var channel = mention.TargetId?.Pipe(context.TryGetChannel);
var symbol = channel?.IsVoice == true ? "🔊" : "#"; var symbol = channel?.IsVoice == true ? "🔊" : "#";
var name = channel?.Name ?? "deleted-channel"; var name = channel?.Name ?? "deleted-channel";
_buffer.Append( buffer.Append(
// lang=html // lang=html
$""" $"""
<span class="chatlog__markdown-mention">{symbol}{HtmlEncode(name)}</span> <span class="chatlog__markdown-mention">{symbol}{HtmlEncode(name)}</span>
@ -290,7 +286,7 @@ internal partial class HtmlMarkdownVisitor(
} }
else if (mention.Kind == MentionKind.Role) else if (mention.Kind == MentionKind.Role)
{ {
var role = mention.TargetId?.Pipe(_context.TryGetRole); var role = mention.TargetId?.Pipe(context.TryGetRole);
var name = role?.Name ?? "deleted-role"; var name = role?.Name ?? "deleted-role";
var color = role?.Color; var color = role?.Color;
@ -300,7 +296,7 @@ internal partial class HtmlMarkdownVisitor(
""" """
: null; : null;
_buffer.Append( buffer.Append(
// lang=html // lang=html
$""" $"""
<span class="chatlog__markdown-mention" style="{style}">@{HtmlEncode(name)}</span> <span class="chatlog__markdown-mention" style="{style}">@{HtmlEncode(name)}</span>
@ -315,14 +311,14 @@ internal partial class HtmlMarkdownVisitor(
) )
{ {
var formatted = timestamp.Instant is not null var formatted = timestamp.Instant is not null
? _context.FormatDate(timestamp.Instant.Value, timestamp.Format ?? "g") ? context.FormatDate(timestamp.Instant.Value, timestamp.Format ?? "g")
: "Invalid date"; : "Invalid date";
var formattedLong = timestamp.Instant is not null var formattedLong = timestamp.Instant is not null
? _context.FormatDate(timestamp.Instant.Value, "f") ? context.FormatDate(timestamp.Instant.Value, "f")
: ""; : "";
_buffer.Append( buffer.Append(
// lang=html // lang=html
$""" $"""
<span class="chatlog__markdown-timestamp" title="{HtmlEncode(formattedLong)}">{HtmlEncode(formatted)}</span> <span class="chatlog__markdown-timestamp" title="{HtmlEncode(formattedLong)}">{HtmlEncode(formatted)}</span>

View file

@ -13,7 +13,6 @@ internal class HtmlMessageWriter(Stream stream, ExportContext context, string th
: MessageWriter(stream, context) : MessageWriter(stream, context)
{ {
private readonly TextWriter _writer = new StreamWriter(stream); private readonly TextWriter _writer = new StreamWriter(stream);
private readonly string _themeName = themeName;
private readonly HtmlMinifier _minifier = new(); private readonly HtmlMinifier _minifier = new();
private readonly List<Message> _messageGroup = new(); private readonly List<Message> _messageGroup = new();
@ -74,11 +73,9 @@ internal class HtmlMessageWriter(Stream stream, ExportContext context, string th
{ {
await _writer.WriteLineAsync( await _writer.WriteLineAsync(
Minify( Minify(
await new PreambleTemplate await new PreambleTemplate { Context = Context, ThemeName = themeName }.RenderAsync(
{ cancellationToken
Context = Context, )
ThemeName = _themeName
}.RenderAsync(cancellationToken)
) )
); );
} }

View file

@ -15,7 +15,8 @@ namespace DiscordChatExporter.Core.Exporting;
internal class JsonMessageWriter(Stream stream, ExportContext context) internal class JsonMessageWriter(Stream stream, ExportContext context)
: MessageWriter(stream, context) : MessageWriter(stream, context)
{ {
private readonly Utf8JsonWriter _writer = new Utf8JsonWriter( private readonly Utf8JsonWriter _writer =
new(
stream, stream,
new JsonWriterOptions new JsonWriterOptions
{ {

View file

@ -8,8 +8,6 @@ namespace DiscordChatExporter.Core.Exporting;
internal partial class MessageExporter(ExportContext context) : IAsyncDisposable internal partial class MessageExporter(ExportContext context) : IAsyncDisposable
{ {
private readonly ExportContext _context = context;
private int _partitionIndex; private int _partitionIndex;
private MessageWriter? _writer; private MessageWriter? _writer;
@ -39,7 +37,7 @@ internal partial class MessageExporter(ExportContext context) : IAsyncDisposable
// Ensure that the partition limit has not been reached // Ensure that the partition limit has not been reached
if ( if (
_writer is not null _writer is not null
&& _context && context
.Request .Request
.PartitionLimit .PartitionLimit
.IsReached(_writer.MessagesWritten, _writer.BytesWritten) .IsReached(_writer.MessagesWritten, _writer.BytesWritten)
@ -53,10 +51,10 @@ internal partial class MessageExporter(ExportContext context) : IAsyncDisposable
if (_writer is not null) if (_writer is not null)
return _writer; return _writer;
Directory.CreateDirectory(_context.Request.OutputDirPath); Directory.CreateDirectory(context.Request.OutputDirPath);
var filePath = GetPartitionFilePath(_context.Request.OutputFilePath, _partitionIndex); var filePath = GetPartitionFilePath(context.Request.OutputFilePath, _partitionIndex);
var writer = CreateMessageWriter(filePath, _context.Request.Format, _context); var writer = CreateMessageWriter(filePath, context.Request.Format, context);
await writer.WritePreambleAsync(cancellationToken); await writer.WritePreambleAsync(cancellationToken);
return _writer = writer; return _writer = writer;

View file

@ -1,11 +1,7 @@
namespace DiscordChatExporter.Core.Exporting.Partitioning; namespace DiscordChatExporter.Core.Exporting.Partitioning;
internal class FileSizePartitionLimit : PartitionLimit internal class FileSizePartitionLimit(long limit) : PartitionLimit
{ {
private readonly long _limit;
public FileSizePartitionLimit(long limit) => _limit = limit;
public override bool IsReached(long messagesWritten, long bytesWritten) => public override bool IsReached(long messagesWritten, long bytesWritten) =>
bytesWritten >= _limit; bytesWritten >= limit;
} }

View file

@ -1,11 +1,7 @@
namespace DiscordChatExporter.Core.Exporting.Partitioning; namespace DiscordChatExporter.Core.Exporting.Partitioning;
internal class MessageCountPartitionLimit : PartitionLimit internal class MessageCountPartitionLimit(long limit) : PartitionLimit
{ {
private readonly long _limit;
public MessageCountPartitionLimit(long limit) => _limit = limit;
public override bool IsReached(long messagesWritten, long bytesWritten) => public override bool IsReached(long messagesWritten, long bytesWritten) =>
messagesWritten >= _limit; messagesWritten >= limit;
} }

View file

@ -1,7 +1,6 @@
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Markdown; using DiscordChatExporter.Core.Markdown;
using DiscordChatExporter.Core.Markdown.Parsing; using DiscordChatExporter.Core.Markdown.Parsing;
using DiscordChatExporter.Core.Utils.Extensions; using DiscordChatExporter.Core.Utils.Extensions;
@ -11,15 +10,12 @@ namespace DiscordChatExporter.Core.Exporting;
internal partial class PlainTextMarkdownVisitor(ExportContext context, StringBuilder buffer) internal partial class PlainTextMarkdownVisitor(ExportContext context, StringBuilder buffer)
: MarkdownVisitor : MarkdownVisitor
{ {
private readonly ExportContext _context = context;
private readonly StringBuilder _buffer = buffer;
protected override ValueTask VisitTextAsync( protected override ValueTask VisitTextAsync(
TextNode text, TextNode text,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
_buffer.Append(text.Text); buffer.Append(text.Text);
return default; return default;
} }
@ -28,7 +24,7 @@ internal partial class PlainTextMarkdownVisitor(ExportContext context, StringBui
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
_buffer.Append(emoji.IsCustomEmoji ? $":{emoji.Name}:" : emoji.Name); buffer.Append(emoji.IsCustomEmoji ? $":{emoji.Name}:" : emoji.Name);
return default; return default;
} }
@ -40,11 +36,11 @@ internal partial class PlainTextMarkdownVisitor(ExportContext context, StringBui
{ {
if (mention.Kind == MentionKind.Everyone) if (mention.Kind == MentionKind.Everyone)
{ {
_buffer.Append("@everyone"); buffer.Append("@everyone");
} }
else if (mention.Kind == MentionKind.Here) else if (mention.Kind == MentionKind.Here)
{ {
_buffer.Append("@here"); buffer.Append("@here");
} }
else if (mention.Kind == MentionKind.User) else if (mention.Kind == MentionKind.User)
{ {
@ -52,30 +48,30 @@ internal partial class PlainTextMarkdownVisitor(ExportContext context, StringBui
// which means they need to be populated on demand. // which means they need to be populated on demand.
// https://github.com/Tyrrrz/DiscordChatExporter/issues/304 // https://github.com/Tyrrrz/DiscordChatExporter/issues/304
if (mention.TargetId is not null) if (mention.TargetId is not null)
await _context.PopulateMemberAsync(mention.TargetId.Value, cancellationToken); await context.PopulateMemberAsync(mention.TargetId.Value, cancellationToken);
var member = mention.TargetId?.Pipe(_context.TryGetMember); var member = mention.TargetId?.Pipe(context.TryGetMember);
var displayName = member?.DisplayName ?? member?.User.DisplayName ?? "Unknown"; var displayName = member?.DisplayName ?? member?.User.DisplayName ?? "Unknown";
_buffer.Append($"@{displayName}"); buffer.Append($"@{displayName}");
} }
else if (mention.Kind == MentionKind.Channel) else if (mention.Kind == MentionKind.Channel)
{ {
var channel = mention.TargetId?.Pipe(_context.TryGetChannel); var channel = mention.TargetId?.Pipe(context.TryGetChannel);
var name = channel?.Name ?? "deleted-channel"; var name = channel?.Name ?? "deleted-channel";
_buffer.Append($"#{name}"); buffer.Append($"#{name}");
// Voice channel marker // Voice channel marker
if (channel?.IsVoice == true) if (channel?.IsVoice == true)
_buffer.Append(" [voice]"); buffer.Append(" [voice]");
} }
else if (mention.Kind == MentionKind.Role) else if (mention.Kind == MentionKind.Role)
{ {
var role = mention.TargetId?.Pipe(_context.TryGetRole); var role = mention.TargetId?.Pipe(context.TryGetRole);
var name = role?.Name ?? "deleted-role"; var name = role?.Name ?? "deleted-role";
_buffer.Append($"@{name}"); buffer.Append($"@{name}");
} }
} }
@ -84,9 +80,9 @@ internal partial class PlainTextMarkdownVisitor(ExportContext context, StringBui
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
_buffer.Append( buffer.Append(
timestamp.Instant is not null timestamp.Instant is not null
? _context.FormatDate(timestamp.Instant.Value, timestamp.Format ?? "g") ? context.FormatDate(timestamp.Instant.Value, timestamp.Format ?? "g")
: "Invalid date" : "Invalid date"
); );

View file

@ -2,15 +2,8 @@
namespace DiscordChatExporter.Core.Markdown.Parsing; namespace DiscordChatExporter.Core.Markdown.Parsing;
internal class AggregateMatcher<T> : IMatcher<T> internal class AggregateMatcher<T>(IReadOnlyList<IMatcher<T>> matchers) : IMatcher<T>
{ {
private readonly IReadOnlyList<IMatcher<T>> _matchers;
public AggregateMatcher(IReadOnlyList<IMatcher<T>> matchers)
{
_matchers = matchers;
}
public AggregateMatcher(params IMatcher<T>[] matchers) public AggregateMatcher(params IMatcher<T>[] matchers)
: this((IReadOnlyList<IMatcher<T>>)matchers) { } : this((IReadOnlyList<IMatcher<T>>)matchers) { }
@ -19,7 +12,7 @@ internal class AggregateMatcher<T> : IMatcher<T>
ParsedMatch<T>? earliestMatch = null; ParsedMatch<T>? earliestMatch = null;
// Try to match the input with each matcher and get the match with the lowest start index // Try to match the input with each matcher and get the match with the lowest start index
foreach (var matcher in _matchers) foreach (var matcher in matchers)
{ {
// Try to match // Try to match
var match = matcher.TryMatch(segment); var match = matcher.TryMatch(segment);

View file

@ -1,14 +1,8 @@
namespace DiscordChatExporter.Core.Markdown.Parsing; namespace DiscordChatExporter.Core.Markdown.Parsing;
internal class ParsedMatch<T> internal class ParsedMatch<T>(StringSegment segment, T value)
{ {
public StringSegment Segment { get; } public StringSegment Segment { get; } = segment;
public T Value { get; } public T Value { get; } = value;
public ParsedMatch(StringSegment segment, T value)
{
Segment = segment;
Value = value;
}
} }

View file

@ -3,20 +3,11 @@ using System.Text.RegularExpressions;
namespace DiscordChatExporter.Core.Markdown.Parsing; namespace DiscordChatExporter.Core.Markdown.Parsing;
internal class RegexMatcher<T> : IMatcher<T> internal class RegexMatcher<T>(Regex regex, Func<StringSegment, Match, T?> transform) : IMatcher<T>
{ {
private readonly Regex _regex;
private readonly Func<StringSegment, Match, T?> _transform;
public RegexMatcher(Regex regex, Func<StringSegment, Match, T?> transform)
{
_regex = regex;
_transform = transform;
}
public ParsedMatch<T>? TryMatch(StringSegment segment) public ParsedMatch<T>? TryMatch(StringSegment segment)
{ {
var match = _regex.Match(segment.Source, segment.StartIndex, segment.Length); var match = regex.Match(segment.Source, segment.StartIndex, segment.Length);
if (!match.Success) if (!match.Success)
return null; return null;
@ -25,11 +16,11 @@ internal class RegexMatcher<T> : IMatcher<T>
// Which is super weird because regex.Match(string, int) takes the whole input in context. // Which is super weird because regex.Match(string, int) takes the whole input in context.
// So in order to properly account for ^/$ regex tokens, we need to make sure that // So in order to properly account for ^/$ regex tokens, we need to make sure that
// the expression also matches on the bigger part of the input. // the expression also matches on the bigger part of the input.
if (!_regex.IsMatch(segment.Source[..segment.EndIndex], segment.StartIndex)) if (!regex.IsMatch(segment.Source[..segment.EndIndex], segment.StartIndex))
return null; return null;
var segmentMatch = segment.Relocate(match); var segmentMatch = segment.Relocate(match);
var value = _transform(segmentMatch, match); var value = transform(segmentMatch, match);
return value is not null ? new ParsedMatch<T>(segmentMatch, value) : null; return value is not null ? new ParsedMatch<T>(segmentMatch, value) : null;
} }

View file

@ -2,37 +2,24 @@
namespace DiscordChatExporter.Core.Markdown.Parsing; namespace DiscordChatExporter.Core.Markdown.Parsing;
internal class StringMatcher<T> : IMatcher<T> internal class StringMatcher<T>(
{
private readonly string _needle;
private readonly StringComparison _comparison;
private readonly Func<StringSegment, T?> _transform;
public StringMatcher(
string needle, string needle,
StringComparison comparison, StringComparison comparison,
Func<StringSegment, T?> transform Func<StringSegment, T?> transform
) ) : IMatcher<T>
{ {
_needle = needle;
_comparison = comparison;
_transform = transform;
}
public StringMatcher(string needle, Func<StringSegment, T> transform) public StringMatcher(string needle, Func<StringSegment, T> transform)
: this(needle, StringComparison.Ordinal, transform) { } : this(needle, StringComparison.Ordinal, transform) { }
public ParsedMatch<T>? TryMatch(StringSegment segment) public ParsedMatch<T>? TryMatch(StringSegment segment)
{ {
var index = segment var index = segment.Source.IndexOf(needle, segment.StartIndex, segment.Length, comparison);
.Source
.IndexOf(_needle, segment.StartIndex, segment.Length, _comparison);
if (index < 0) if (index < 0)
return null; return null;
var segmentMatch = segment.Relocate(index, _needle.Length); var segmentMatch = segment.Relocate(index, needle.Length);
var value = _transform(segmentMatch); var value = transform(segmentMatch);
return value is not null ? new ParsedMatch<T>(segmentMatch, value) : null; return value is not null ? new ParsedMatch<T>(segmentMatch, value) : null;
} }

View file

@ -8,7 +8,8 @@ using Microsoft.Win32;
namespace DiscordChatExporter.Gui.Services; namespace DiscordChatExporter.Gui.Services;
public partial class SettingsService : SettingsBase public partial class SettingsService()
: SettingsBase(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Settings.dat"))
{ {
public bool IsUkraineSupportMessageEnabled { get; set; } = true; public bool IsUkraineSupportMessageEnabled { get; set; } = true;
@ -44,9 +45,6 @@ public partial class SettingsService : SettingsBase
public string? LastAssetsDirPath { get; set; } public string? LastAssetsDirPath { get; set; }
public SettingsService()
: base(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Settings.dat")) { }
public override void Save() public override void Save()
{ {
// Clear the token if it's not supposed to be persisted // Clear the token if it's not supposed to be persisted

View file

@ -6,27 +6,20 @@ using Onova.Services;
namespace DiscordChatExporter.Gui.Services; namespace DiscordChatExporter.Gui.Services;
public class UpdateService : IDisposable public class UpdateService(SettingsService settingsService) : IDisposable
{ {
private readonly IUpdateManager _updateManager = new UpdateManager( private readonly IUpdateManager _updateManager = new UpdateManager(
new GithubPackageResolver("Tyrrrz", "DiscordChatExporter", "DiscordChatExporter.zip"), new GithubPackageResolver("Tyrrrz", "DiscordChatExporter", "DiscordChatExporter.zip"),
new ZipPackageExtractor() new ZipPackageExtractor()
); );
private readonly SettingsService _settingsService;
private Version? _updateVersion; private Version? _updateVersion;
private bool _updatePrepared; private bool _updatePrepared;
private bool _updaterLaunched; private bool _updaterLaunched;
public UpdateService(SettingsService settingsService)
{
_settingsService = settingsService;
}
public async ValueTask<Version?> CheckForUpdatesAsync() public async ValueTask<Version?> CheckForUpdatesAsync()
{ {
if (!_settingsService.IsAutoUpdateEnabled) if (!settingsService.IsAutoUpdateEnabled)
return null; return null;
var check = await _updateManager.CheckForUpdatesAsync(); var check = await _updateManager.CheckForUpdatesAsync();
@ -35,7 +28,7 @@ public class UpdateService : IDisposable
public async ValueTask PrepareUpdateAsync(Version version) public async ValueTask PrepareUpdateAsync(Version version)
{ {
if (!_settingsService.IsAutoUpdateEnabled) if (!settingsService.IsAutoUpdateEnabled)
return; return;
try try
@ -55,7 +48,7 @@ public class UpdateService : IDisposable
public void FinalizeUpdate(bool needRestart) public void FinalizeUpdate(bool needRestart)
{ {
if (!_settingsService.IsAutoUpdateEnabled) if (!settingsService.IsAutoUpdateEnabled)
return; return;
if (_updateVersion is null || !_updatePrepared || _updaterLaunched) if (_updateVersion is null || !_updatePrepared || _updaterLaunched)

View file

@ -8,26 +8,24 @@ using DiscordChatExporter.Gui.ViewModels.Framework;
namespace DiscordChatExporter.Gui.ViewModels.Dialogs; namespace DiscordChatExporter.Gui.ViewModels.Dialogs;
public class SettingsViewModel : DialogScreen public class SettingsViewModel(SettingsService settingsService) : DialogScreen
{ {
private readonly SettingsService _settingsService;
public bool IsAutoUpdateEnabled public bool IsAutoUpdateEnabled
{ {
get => _settingsService.IsAutoUpdateEnabled; get => settingsService.IsAutoUpdateEnabled;
set => _settingsService.IsAutoUpdateEnabled = value; set => settingsService.IsAutoUpdateEnabled = value;
} }
public bool IsDarkModeEnabled public bool IsDarkModeEnabled
{ {
get => _settingsService.IsDarkModeEnabled; get => settingsService.IsDarkModeEnabled;
set => _settingsService.IsDarkModeEnabled = value; set => settingsService.IsDarkModeEnabled = value;
} }
public bool IsTokenPersisted public bool IsTokenPersisted
{ {
get => _settingsService.IsTokenPersisted; get => settingsService.IsTokenPersisted;
set => _settingsService.IsTokenPersisted = value; set => settingsService.IsTokenPersisted = value;
} }
public IReadOnlyList<ThreadInclusionMode> AvailableThreadInclusions { get; } = public IReadOnlyList<ThreadInclusionMode> AvailableThreadInclusions { get; } =
@ -35,8 +33,8 @@ public class SettingsViewModel : DialogScreen
public ThreadInclusionMode ThreadInclusionMode public ThreadInclusionMode ThreadInclusionMode
{ {
get => _settingsService.ThreadInclusionMode; get => settingsService.ThreadInclusionMode;
set => _settingsService.ThreadInclusionMode = value; set => settingsService.ThreadInclusionMode = value;
} }
public IReadOnlyList<string> AvailableLocales { get; } = new[] public IReadOnlyList<string> AvailableLocales { get; } = new[]
@ -77,21 +75,19 @@ public class SettingsViewModel : DialogScreen
public string Locale public string Locale
{ {
get => _settingsService.Locale; get => settingsService.Locale;
set => _settingsService.Locale = value; set => settingsService.Locale = value;
} }
public bool IsUtcNormalizationEnabled public bool IsUtcNormalizationEnabled
{ {
get => _settingsService.IsUtcNormalizationEnabled; get => settingsService.IsUtcNormalizationEnabled;
set => _settingsService.IsUtcNormalizationEnabled = value; set => settingsService.IsUtcNormalizationEnabled = value;
} }
public int ParallelLimit public int ParallelLimit
{ {
get => _settingsService.ParallelLimit; get => settingsService.ParallelLimit;
set => _settingsService.ParallelLimit = Math.Clamp(value, 1, 10); set => settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
} }
public SettingsViewModel(SettingsService settingsService) => _settingsService = settingsService;
} }

View file

@ -8,19 +8,13 @@ using Stylet;
namespace DiscordChatExporter.Gui.ViewModels.Framework; namespace DiscordChatExporter.Gui.ViewModels.Framework;
public class DialogManager : IDisposable public class DialogManager(IViewManager viewManager) : IDisposable
{ {
private readonly IViewManager _viewManager;
private readonly SemaphoreSlim _dialogLock = new(1, 1); private readonly SemaphoreSlim _dialogLock = new(1, 1);
public DialogManager(IViewManager viewManager)
{
_viewManager = viewManager;
}
public async ValueTask<T?> ShowDialogAsync<T>(DialogScreen<T> dialogScreen) public async ValueTask<T?> ShowDialogAsync<T>(DialogScreen<T> dialogScreen)
{ {
var view = _viewManager.CreateAndBindViewForModelIfNecessary(dialogScreen); var view = viewManager.CreateAndBindViewForModelIfNecessary(dialogScreen);
void OnDialogOpened(object? openSender, DialogOpenedEventArgs openArgs) void OnDialogOpened(object? openSender, DialogOpenedEventArgs openArgs)
{ {