Improve markdown parsing to make it less volatile

Fixes #18, #20, #40
pull/44/head
Alexey Golub 7 years ago
parent 7ffb799136
commit 7b67cbc385

@ -4,14 +4,13 @@ using System.Resources;
namespace DiscordChatExporter.Core.Internal namespace DiscordChatExporter.Core.Internal
{ {
internal static class AssemblyHelper internal static class Extensions
{ {
public static string GetResourceString(string resourcePath) public static string GetManifestResourceString(this Assembly assembly, string resourceName)
{ {
var assembly = Assembly.GetExecutingAssembly(); var stream = assembly.GetManifestResourceStream(resourceName);
var stream = assembly.GetManifestResourceStream(resourcePath);
if (stream == null) if (stream == null)
throw new MissingManifestResourceException($"Could not find resource [{resourcePath}]."); throw new MissingManifestResourceException($"Could not find resource [{resourceName}].");
using (stream) using (stream)
using (var reader = new StreamReader(stream)) using (var reader = new StreamReader(stream))

@ -1,5 +1,4 @@
using System.Globalization; using Tyrrrz.Extensions;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Models namespace DiscordChatExporter.Core.Models
{ {
@ -11,11 +10,11 @@ namespace DiscordChatExporter.Core.Models
public string Name { get; } public string Name { get; }
public string FullyQualifiedName => $"{Name}#{Discriminator:0000}"; public string FullName => $"{Name}#{Discriminator:0000}";
public string AvatarHash { get; } public string AvatarHash { get; }
public string DefaultAvatarHash => (Discriminator % 5).ToString(CultureInfo.InvariantCulture); public string DefaultAvatarHash => $"{Discriminator % 5}";
public string AvatarUrl => AvatarHash.IsNotBlank() public string AvatarUrl => AvatarHash.IsNotBlank()
? $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.png" ? $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.png"
@ -31,7 +30,7 @@ namespace DiscordChatExporter.Core.Models
public override string ToString() public override string ToString()
{ {
return FullyQualifiedName; return FullName;
} }
} }
} }

@ -0,0 +1,208 @@
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Models;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Services
{
public partial class ExportService
{
private string FormatMessageContentHtml(Message message)
{
// A lot of these regexes were inspired by or taken from MarkdownSharp
var content = message.Content;
// HTML-encode content
content = HtmlEncode(content);
// Encode multiline codeblocks (```text```)
content = Regex.Replace(content,
@"```+(?:[^`]*?\n)?([^`]+)\n?```+",
m => $"\x1AM{Base64Encode(m.Groups[1].Value)}\x1AM");
// Encode inline codeblocks (`text`)
content = Regex.Replace(content,
@"`([^`]+)`",
m => $"\x1AI{Base64Encode(m.Groups[1].Value)}\x1AI");
// Encode URLs
content = Regex.Replace(content,
@"((https?|ftp)://[-a-zA-Z0-9+&@#/%?=~_|!:,\.\[\]\(\);]*[-a-zA-Z0-9+&@#/%=~_|\[\])])(?=$|\W)",
m => $"\x1AL{Base64Encode(m.Groups[1].Value)}\x1AL");
// Process bold (**text**)
content = Regex.Replace(content, @"(\*\*)(?=\S)(.+?[*_]*)(?<=\S)\1", "<b>$2</b>");
// Process underline (__text__)
content = Regex.Replace(content, @"(__)(?=\S)(.+?)(?<=\S)\1", "<u>$2</u>");
// Process italic (*text* or _text_)
content = Regex.Replace(content, @"(\*|_)(?=\S)(.+?)(?<=\S)\1", "<i>$2</i>");
// Process strike through (~~text~~)
content = Regex.Replace(content, @"(~~)(?=\S)(.+?)(?<=\S)\1", "<s>$2</s>");
// Decode and process multiline codeblocks
content = Regex.Replace(content, "\x1AM(.*?)\x1AM",
m => $"<div class=\"pre\">{Base64Decode(m.Groups[1].Value)}</div>");
// Decode and process inline codeblocks
content = Regex.Replace(content, "\x1AI(.*?)\x1AI",
m => $"<span class=\"pre\">{Base64Decode(m.Groups[1].Value)}</span>");
// Decode and process URLs
content = Regex.Replace(content, "\x1AL(.*?)\x1AL",
m => $"<a href=\"{Base64Decode(m.Groups[1].Value)}\">{Base64Decode(m.Groups[1].Value)}</a>");
// New lines
content = content.Replace("\n", "<br />");
// Meta mentions (@everyone)
content = content.Replace("@everyone", "<span class=\"mention\">@everyone</span>");
// Meta mentions (@here)
content = content.Replace("@here", "<span class=\"mention\">@here</span>");
// User mentions (<@id> and <@!id>)
foreach (var mentionedUser in message.MentionedUsers)
{
content = Regex.Replace(content, $"&lt;@!?{mentionedUser.Id}&gt;",
$"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser.FullName)}\">" +
$"@{HtmlEncode(mentionedUser.Name)}" +
"</span>");
}
// Role mentions (<@&id>)
foreach (var mentionedRole in message.MentionedRoles)
{
content = content.Replace($"&lt;@&amp;{mentionedRole.Id}&gt;",
"<span class=\"mention\">" +
$"@{HtmlEncode(mentionedRole.Name)}" +
"</span>");
}
// Channel mentions (<#id>)
foreach (var mentionedChannel in message.MentionedChannels)
{
content = content.Replace($"&lt;#{mentionedChannel.Id}&gt;",
"<span class=\"mention\">" +
$"#{HtmlEncode(mentionedChannel.Name)}" +
"</span>");
}
// Custom emojis (<:name:id>)
content = Regex.Replace(content, "&lt;(:.*?:)(\\d*)&gt;",
"<img class=\"emoji\" title=\"$1\" src=\"https://cdn.discordapp.com/emojis/$2.png\" />");
return content;
}
private async Task ExportAsHtmlAsync(ChannelChatLog log, TextWriter output, string css)
{
// Generation info
await output.WriteLineAsync("<!-- https://github.com/Tyrrrz/DiscordChatExporter -->");
// Html start
await output.WriteLineAsync("<!DOCTYPE html>");
await output.WriteLineAsync("<html lang=\"en\">");
// HEAD
await output.WriteLineAsync("<head>");
await output.WriteLineAsync($"<title>{log.Guild.Name} - {log.Channel.Name}</title>");
await output.WriteLineAsync("<meta charset=\"utf-8\" />");
await output.WriteLineAsync("<meta name=\"viewport\" content=\"width=device-width\" />");
await output.WriteLineAsync($"<style>{css}</style>");
await output.WriteLineAsync("</head>");
// Body start
await output.WriteLineAsync("<body>");
// Guild and channel info
await output.WriteLineAsync("<div id=\"info\">");
await output.WriteLineAsync("<div class=\"info-left\">");
await output.WriteLineAsync($"<img class=\"guild-icon\" src=\"{log.Guild.IconUrl}\" />");
await output.WriteLineAsync("</div>"); // info-left
await output.WriteLineAsync("<div class=\"info-right\">");
await output.WriteLineAsync($"<div class=\"guild-name\">{log.Guild.Name}</div>");
await output.WriteLineAsync($"<div class=\"channel-name\">{log.Channel.Name}</div>");
await output.WriteLineAsync($"<div class=\"channel-topic\">{log.Channel.Topic}</div>");
await output.WriteLineAsync(
$"<div class=\"channel-messagecount\">{log.TotalMessageCount:N0} messages</div>");
await output.WriteLineAsync("</div>"); // info-right
await output.WriteLineAsync("</div>"); // info
// Chat log
await output.WriteLineAsync("<div id=\"log\">");
foreach (var group in log.MessageGroups)
{
await output.WriteLineAsync("<div class=\"msg\">");
await output.WriteLineAsync("<div class=\"msg-left\">");
await output.WriteLineAsync($"<img class=\"msg-avatar\" src=\"{group.Author.AvatarUrl}\" />");
await output.WriteLineAsync("</div>");
await output.WriteLineAsync("<div class=\"msg-right\">");
await output.WriteAsync(
$"<span class=\"msg-user\" title=\"{HtmlEncode(group.Author.FullName)}\">");
await output.WriteAsync(HtmlEncode(group.Author.Name));
await output.WriteLineAsync("</span>");
var timeStampFormatted = HtmlEncode(group.TimeStamp.ToString(_settingsService.DateFormat));
await output.WriteLineAsync($"<span class=\"msg-date\">{timeStampFormatted}</span>");
// Messages
foreach (var message in group.Messages)
{
// Content
if (message.Content.IsNotBlank())
{
await output.WriteLineAsync("<div class=\"msg-content\">");
var contentFormatted = FormatMessageContentHtml(message);
await output.WriteAsync(contentFormatted);
// Edited timestamp
if (message.EditedTimeStamp != null)
{
var editedTimeStampFormatted =
HtmlEncode(message.EditedTimeStamp.Value.ToString(_settingsService.DateFormat));
await output.WriteAsync(
$"<span class=\"msg-edited\" title=\"{editedTimeStampFormatted}\">(edited)</span>");
}
await output.WriteLineAsync("</div>"); // msg-content
}
// Attachments
foreach (var attachment in message.Attachments)
{
if (attachment.Type == AttachmentType.Image)
{
await output.WriteLineAsync("<div class=\"msg-attachment\">");
await output.WriteLineAsync($"<a href=\"{attachment.Url}\">");
await output.WriteLineAsync($"<img class=\"msg-attachment\" src=\"{attachment.Url}\" />");
await output.WriteLineAsync("</a>");
await output.WriteLineAsync("</div>");
}
else
{
await output.WriteLineAsync("<div class=\"msg-attachment\">");
await output.WriteLineAsync($"<a href=\"{attachment.Url}\">");
var fileSizeFormatted = FormatFileSize(attachment.FileSize);
await output.WriteLineAsync($"Attachment: {attachment.FileName} ({fileSizeFormatted})");
await output.WriteLineAsync("</a>");
await output.WriteLineAsync("</div>");
}
}
}
await output.WriteLineAsync("</div>"); // msg-right
await output.WriteLineAsync("</div>"); // msg
}
await output.WriteLineAsync("</div>"); // log
await output.WriteLineAsync("</body>");
await output.WriteLineAsync("</html>");
}
}
}

@ -0,0 +1,79 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Models;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Services
{
public partial class ExportService
{
private string FormatMessageContentPlainText(Message message)
{
var content = message.Content;
// New lines
content = content.Replace("\n", Environment.NewLine);
// User mentions (<@id> and <@!id>)
foreach (var mentionedUser in message.MentionedUsers)
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>", $"@{mentionedUser}");
// Role mentions (<@&id>)
foreach (var mentionedRole in message.MentionedRoles)
content = content.Replace($"<@&{mentionedRole.Id}>", $"@{mentionedRole.Name}");
// Channel mentions (<#id>)
foreach (var mentionedChannel in message.MentionedChannels)
content = content.Replace($"<#{mentionedChannel.Id}>", $"#{mentionedChannel.Name}");
// Custom emojis (<:name:id>)
content = Regex.Replace(content, "<(:.*?:)\\d*>", "$1");
return content;
}
private async Task ExportAsPlainTextAsync(ChannelChatLog log, TextWriter output)
{
// Generation info
await output.WriteLineAsync("https://github.com/Tyrrrz/DiscordChatExporter");
await output.WriteLineAsync();
// Guild and channel info
await output.WriteLineAsync('='.Repeat(48));
await output.WriteLineAsync($"Guild: {log.Guild.Name}");
await output.WriteLineAsync($"Channel: {log.Channel.Name}");
await output.WriteLineAsync($"Topic: {log.Channel.Topic}");
await output.WriteLineAsync($"Messages: {log.TotalMessageCount:N0}");
await output.WriteLineAsync('='.Repeat(48));
await output.WriteLineAsync();
// Chat log
foreach (var group in log.MessageGroups)
{
var timeStampFormatted = group.TimeStamp.ToString(_settingsService.DateFormat);
await output.WriteLineAsync($"{group.Author.FullName} [{timeStampFormatted}]");
// Messages
foreach (var message in group.Messages)
{
// Content
if (message.Content.IsNotBlank())
{
var contentFormatted = FormatMessageContentPlainText(message);
await output.WriteLineAsync(contentFormatted);
}
// Attachments
foreach (var attachment in message.Attachments)
{
await output.WriteLineAsync(attachment.Url);
}
}
await output.WriteLineAsync();
}
}
}
}

@ -1,8 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Text; using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using DiscordChatExporter.Core.Internal; using DiscordChatExporter.Core.Internal;
using DiscordChatExporter.Core.Models; using DiscordChatExporter.Core.Models;
@ -19,190 +18,49 @@ namespace DiscordChatExporter.Core.Services
_settingsService = settingsService; _settingsService = settingsService;
} }
private async Task ExportAsTextAsync(string filePath, ChannelChatLog log) public async Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log)
{ {
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128 * 1024)) using (var output = File.CreateText(filePath))
{ {
// Generation info if (format == ExportFormat.PlainText)
await writer.WriteLineAsync("https://github.com/Tyrrrz/DiscordChatExporter");
await writer.WriteLineAsync();
// Guild and channel info
await writer.WriteLineAsync('='.Repeat(48));
await writer.WriteLineAsync($"Guild: {log.Guild.Name}");
await writer.WriteLineAsync($"Channel: {log.Channel.Name}");
await writer.WriteLineAsync($"Topic: {log.Channel.Topic}");
await writer.WriteLineAsync($"Messages: {log.TotalMessageCount:N0}");
await writer.WriteLineAsync('='.Repeat(48));
await writer.WriteLineAsync();
// Chat log
foreach (var group in log.MessageGroups)
{ {
var timeStampFormatted = group.TimeStamp.ToString(_settingsService.DateFormat); await ExportAsPlainTextAsync(log, output);
await writer.WriteLineAsync($"{group.Author.FullyQualifiedName} [{timeStampFormatted}]");
// Messages
foreach (var message in group.Messages)
{
// Content
if (message.Content.IsNotBlank())
{
var contentFormatted = FormatMessageContentText(message);
await writer.WriteLineAsync(contentFormatted);
}
// Attachments
foreach (var attachment in message.Attachments)
{
await writer.WriteLineAsync(attachment.Url);
}
}
await writer.WriteLineAsync();
} }
}
}
private async Task ExportAsHtmlAsync(string filePath, ChannelChatLog log, string css)
{
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128 * 1024))
{
// Generation info
await writer.WriteLineAsync("<!-- https://github.com/Tyrrrz/DiscordChatExporter -->");
// Html start
await writer.WriteLineAsync("<!DOCTYPE html>");
await writer.WriteLineAsync("<html lang=\"en\">");
// HEAD
await writer.WriteLineAsync("<head>");
await writer.WriteLineAsync($"<title>{log.Guild.Name} - {log.Channel.Name}</title>");
await writer.WriteLineAsync("<meta charset=\"utf-8\" />");
await writer.WriteLineAsync("<meta name=\"viewport\" content=\"width=device-width\" />");
await writer.WriteLineAsync($"<style>{css}</style>");
await writer.WriteLineAsync("</head>");
// Body start
await writer.WriteLineAsync("<body>");
// Guild and channel info
await writer.WriteLineAsync("<div id=\"info\">");
await writer.WriteLineAsync("<div class=\"info-left\">");
await writer.WriteLineAsync($"<img class=\"guild-icon\" src=\"{log.Guild.IconUrl}\" />");
await writer.WriteLineAsync("</div>"); // info-left
await writer.WriteLineAsync("<div class=\"info-right\">");
await writer.WriteLineAsync($"<div class=\"guild-name\">{log.Guild.Name}</div>");
await writer.WriteLineAsync($"<div class=\"channel-name\">{log.Channel.Name}</div>");
await writer.WriteLineAsync($"<div class=\"channel-topic\">{log.Channel.Topic}</div>");
await writer.WriteLineAsync(
$"<div class=\"channel-messagecount\">{log.TotalMessageCount:N0} messages</div>");
await writer.WriteLineAsync("</div>"); // info-right
await writer.WriteLineAsync("</div>"); // info
// Chat log else if (format == ExportFormat.HtmlDark)
await writer.WriteLineAsync("<div id=\"log\">");
foreach (var group in log.MessageGroups)
{ {
await writer.WriteLineAsync("<div class=\"msg\">"); var css = Assembly.GetExecutingAssembly()
await writer.WriteLineAsync("<div class=\"msg-left\">"); .GetManifestResourceString("DiscordChatExporter.Core.Resources.ExportService.DarkTheme.css");
await writer.WriteLineAsync($"<img class=\"msg-avatar\" src=\"{group.Author.AvatarUrl}\" />"); await ExportAsHtmlAsync(log, output, css);
await writer.WriteLineAsync("</div>");
await writer.WriteLineAsync("<div class=\"msg-right\">");
await writer.WriteAsync(
$"<span class=\"msg-user\" title=\"{HtmlEncode(group.Author.FullyQualifiedName)}\">");
await writer.WriteAsync(HtmlEncode(group.Author.Name));
await writer.WriteLineAsync("</span>");
var timeStampFormatted = HtmlEncode(group.TimeStamp.ToString(_settingsService.DateFormat));
await writer.WriteLineAsync($"<span class=\"msg-date\">{timeStampFormatted}</span>");
// Messages
foreach (var message in group.Messages)
{
// Content
if (message.Content.IsNotBlank())
{
await writer.WriteLineAsync("<div class=\"msg-content\">");
var contentFormatted = FormatMessageContentHtml(message);
await writer.WriteAsync(contentFormatted);
// Edited timestamp
if (message.EditedTimeStamp != null)
{
var editedTimeStampFormatted =
HtmlEncode(message.EditedTimeStamp.Value.ToString(_settingsService.DateFormat));
await writer.WriteAsync(
$"<span class=\"msg-edited\" title=\"{editedTimeStampFormatted}\">(edited)</span>");
}
await writer.WriteLineAsync("</div>"); // msg-content
}
// Attachments
foreach (var attachment in message.Attachments)
{
if (attachment.Type == AttachmentType.Image)
{
await writer.WriteLineAsync("<div class=\"msg-attachment\">");
await writer.WriteLineAsync($"<a href=\"{attachment.Url}\">");
await writer.WriteLineAsync(
$"<img class=\"msg-attachment\" src=\"{attachment.Url}\" />");
await writer.WriteLineAsync("</a>");
await writer.WriteLineAsync("</div>");
}
else
{
await writer.WriteLineAsync("<div class=\"msg-attachment\">");
await writer.WriteLineAsync($"<a href=\"{attachment.Url}\">");
var fileSizeFormatted = FormatFileSize(attachment.FileSize);
await writer.WriteLineAsync($"Attachment: {attachment.FileName} ({fileSizeFormatted})");
await writer.WriteLineAsync("</a>");
await writer.WriteLineAsync("</div>");
}
}
}
await writer.WriteLineAsync("</div>"); // msg-right
await writer.WriteLineAsync("</div>"); // msg
} }
await writer.WriteLineAsync("</div>"); // log
await writer.WriteLineAsync("</body>"); else if (format == ExportFormat.HtmlLight)
await writer.WriteLineAsync("</html>"); {
} var css = Assembly.GetExecutingAssembly()
} .GetManifestResourceString("DiscordChatExporter.Core.Resources.ExportService.LightTheme.css");
await ExportAsHtmlAsync(log, output, css);
}
public Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log) else throw new ArgumentOutOfRangeException(nameof(format));
{
if (format == ExportFormat.PlainText)
{
return ExportAsTextAsync(filePath, log);
}
if (format == ExportFormat.HtmlDark)
{
var css = AssemblyHelper.GetResourceString("DiscordChatExporter.Core.Resources.ExportService.DarkTheme.css");
return ExportAsHtmlAsync(filePath, log, css);
} }
if (format == ExportFormat.HtmlLight)
{
var css = AssemblyHelper.GetResourceString("DiscordChatExporter.Core.Resources.ExportService.LightTheme.css");
return ExportAsHtmlAsync(filePath, log, css);
}
throw new ArgumentOutOfRangeException(nameof(format));
} }
} }
public partial class ExportService public partial class ExportService
{ {
private static string HtmlEncode(string str) private static string Base64Encode(string str)
{ {
return WebUtility.HtmlEncode(str); return str.GetBytes().ToBase64();
} }
private static string HtmlEncode(object obj) private static string Base64Decode(string str)
{
return str.FromBase64().GetString();
}
private static string HtmlEncode(string str)
{ {
return WebUtility.HtmlEncode(obj.ToString()); return WebUtility.HtmlEncode(str);
} }
private static string FormatFileSize(long fileSize) private static string FormatFileSize(long fileSize)
@ -219,104 +77,5 @@ namespace DiscordChatExporter.Core.Services
return $"{size:0.#} {units[unit]}"; return $"{size:0.#} {units[unit]}";
} }
public static string FormatMessageContentText(Message message)
{
var content = message.Content;
// New lines
content = content.Replace("\n", Environment.NewLine);
// User mentions (<@id> and <@!id>)
foreach (var mentionedUser in message.MentionedUsers)
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>", $"@{mentionedUser}");
// Role mentions (<@&id>)
foreach (var mentionedRole in message.MentionedRoles)
content = content.Replace($"<@&{mentionedRole.Id}>", $"@{mentionedRole.Name}");
// Channel mentions (<#id>)
foreach (var mentionedChannel in message.MentionedChannels)
content = content.Replace($"<#{mentionedChannel.Id}>", $"#{mentionedChannel.Name}");
// Custom emojis (<:name:id>)
content = Regex.Replace(content, "<(:.*?:)\\d*>", "$1");
return content;
}
private static string FormatMessageContentHtml(Message message)
{
var content = message.Content;
// Encode HTML
content = HtmlEncode(content);
// Pre multiline (```text```)
content = Regex.Replace(content, "```+(?:[^`]*?\\n)?([^`]+)\\n?```+", "<div class=\"pre\">$1</div>");
// Pre inline (`text`)
content = Regex.Replace(content, "`([^`]+)`", "<span class=\"pre\">$1</span>");
// Bold (**text**)
content = Regex.Replace(content, "\\*\\*([^\\*]*?)\\*\\*", "<b>$1</b>");
// Italic (*text*)
content = Regex.Replace(content, "\\*([^\\*]*?)\\*", "<i>$1</i>");
// Underline (__text__)
content = Regex.Replace(content, "__([^_]*?)__", "<u>$1</u>");
// Italic (_text_)
content = Regex.Replace(content, "_([^_]*?)_", "<i>$1</i>");
// Strike through (~~text~~)
content = Regex.Replace(content, "~~([^~]*?)~~", "<s>$1</s>");
// New lines
content = content.Replace("\n", "<br />");
// URL links
content = Regex.Replace(content, "((https?|ftp)://[^\\s/$.?#].[^\\s<>]*)", "<a href=\"$1\">$1</a>");
// Meta mentions (@everyone)
content = content.Replace("@everyone", "<span class=\"mention\">@everyone</span>");
// Meta mentions (@here)
content = content.Replace("@here", "<span class=\"mention\">@here</span>");
// User mentions (<@id> and <@!id>)
foreach (var mentionedUser in message.MentionedUsers)
{
content = Regex.Replace(content, $"&lt;@!?{mentionedUser.Id}&gt;",
$"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser)}\">" +
$"@{HtmlEncode(mentionedUser.Name)}" +
"</span>");
}
// Role mentions (<@&id>)
foreach (var mentionedRole in message.MentionedRoles)
{
content = content.Replace($"&lt;@&amp;{mentionedRole.Id}&gt;",
"<span class=\"mention\">" +
$"@{HtmlEncode(mentionedRole.Name)}" +
"</span>");
}
// Channel mentions (<#id>)
foreach (var mentionedChannel in message.MentionedChannels)
{
content = content.Replace($"&lt;#{mentionedChannel.Id}&gt;",
"<span class=\"mention\">" +
$"#{HtmlEncode(mentionedChannel.Name)}" +
"</span>");
}
// Custom emojis (<:name:id>)
content = Regex.Replace(content, "&lt;(:.*?:)(\\d*)&gt;",
"<img class=\"emoji\" title=\"$1\" src=\"https://cdn.discordapp.com/emojis/$2.png\" />");
return content;
}
} }
} }

@ -235,12 +235,12 @@ namespace DiscordChatExporter.Gui.ViewModels
await _exportService.ExportAsync(format, filePath, log); await _exportService.ExportAsync(format, filePath, log);
// Notify completion // Notify completion
MessengerInstance.Send(new ShowNotificationMessage($"Export completed for channel [{channel.Name}]", MessengerInstance.Send(new ShowNotificationMessage("Export complete",
"OPEN", () => Process.Start(filePath))); "OPEN", () => Process.Start(filePath)));
} }
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
{ {
MessengerInstance.Send(new ShowNotificationMessage("You don't have access to that channel")); MessengerInstance.Send(new ShowNotificationMessage("You don't have access to this channel"));
} }
IsBusy = false; IsBusy = false;

Loading…
Cancel
Save