mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2024-09-19 12:18:48 -04:00
Refactor using c# 12 features
This commit is contained in:
parent
174b92cbb0
commit
619fe9ccf7
30 changed files with 155 additions and 290 deletions
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -2,17 +2,11 @@
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Exceptions;
|
namespace DiscordChatExporter.Core.Exceptions;
|
||||||
|
|
||||||
public class DiscordChatExporterException : Exception
|
public class DiscordChatExporterException(
|
||||||
|
string message,
|
||||||
|
bool isFatal = false,
|
||||||
|
Exception? innerException = null
|
||||||
|
) : Exception(message, innerException)
|
||||||
{
|
{
|
||||||
public bool IsFatal { get; }
|
public bool IsFatal { get; } = isFatal;
|
||||||
|
|
||||||
public DiscordChatExporterException(
|
|
||||||
string message,
|
|
||||||
bool isFatal = false,
|
|
||||||
Exception? innerException = null
|
|
||||||
)
|
|
||||||
: base(message, innerException)
|
|
||||||
{
|
|
||||||
IsFatal = isFatal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 =>
|
||||||
|
|
|
@ -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(
|
||||||
|
MessageFilter first,
|
||||||
|
MessageFilter second,
|
||||||
|
BinaryExpressionKind kind
|
||||||
|
) : MessageFilter
|
||||||
{
|
{
|
||||||
private readonly MessageFilter _first;
|
|
||||||
private readonly MessageFilter _second;
|
|
||||||
private readonly BinaryExpressionKind _kind;
|
|
||||||
|
|
||||||
public BinaryExpressionMessageFilter(
|
|
||||||
MessageFilter first,
|
|
||||||
MessageFilter second,
|
|
||||||
BinaryExpressionKind kind
|
|
||||||
)
|
|
||||||
{
|
|
||||||
_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}'.")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}'."
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,18 +15,19 @@ 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 =
|
||||||
stream,
|
new(
|
||||||
new JsonWriterOptions
|
stream,
|
||||||
{
|
new JsonWriterOptions
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/450
|
{
|
||||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/450
|
||||||
Indented = true,
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
// Validation errors may mask actual failures
|
Indented = true,
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/413
|
// Validation errors may mask actual failures
|
||||||
SkipValidation = true
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/413
|
||||||
}
|
SkipValidation = true
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
private async ValueTask<string> FormatMarkdownAsync(
|
private async ValueTask<string> FormatMarkdownAsync(
|
||||||
string markdown,
|
string markdown,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,37 +2,24 @@
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown.Parsing;
|
namespace DiscordChatExporter.Core.Markdown.Parsing;
|
||||||
|
|
||||||
internal class StringMatcher<T> : IMatcher<T>
|
internal class StringMatcher<T>(
|
||||||
|
string needle,
|
||||||
|
StringComparison comparison,
|
||||||
|
Func<StringSegment, T?> transform
|
||||||
|
) : IMatcher<T>
|
||||||
{
|
{
|
||||||
private readonly string _needle;
|
|
||||||
private readonly StringComparison _comparison;
|
|
||||||
private readonly Func<StringSegment, T?> _transform;
|
|
||||||
|
|
||||||
public StringMatcher(
|
|
||||||
string needle,
|
|
||||||
StringComparison comparison,
|
|
||||||
Func<StringSegment, T?> transform
|
|
||||||
)
|
|
||||||
{
|
|
||||||
_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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue