mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-06-02 07:39:50 -04:00
parent
7ffb799136
commit
7b67cbc385
6 changed files with 323 additions and 279 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
208
DiscordChatExporter.Core/Services/ExportService.Html.cs
Normal file
208
DiscordChatExporter.Core/Services/ExportService.Html.cs
Normal file
|
@ -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, $"<@!?{mentionedUser.Id}>",
|
||||||
|
$"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser.FullName)}\">" +
|
||||||
|
$"@{HtmlEncode(mentionedUser.Name)}" +
|
||||||
|
"</span>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role mentions (<@&id>)
|
||||||
|
foreach (var mentionedRole in message.MentionedRoles)
|
||||||
|
{
|
||||||
|
content = content.Replace($"<@&{mentionedRole.Id}>",
|
||||||
|
"<span class=\"mention\">" +
|
||||||
|
$"@{HtmlEncode(mentionedRole.Name)}" +
|
||||||
|
"</span>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel mentions (<#id>)
|
||||||
|
foreach (var mentionedChannel in message.MentionedChannels)
|
||||||
|
{
|
||||||
|
content = content.Replace($"<#{mentionedChannel.Id}>",
|
||||||
|
"<span class=\"mention\">" +
|
||||||
|
$"#{HtmlEncode(mentionedChannel.Name)}" +
|
||||||
|
"</span>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom emojis (<:name:id>)
|
||||||
|
content = Regex.Replace(content, "<(:.*?:)(\\d*)>",
|
||||||
|
"<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>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
DiscordChatExporter.Core/Services/ExportService.PlainText.cs
Normal file
79
DiscordChatExporter.Core/Services/ExportService.PlainText.cs
Normal file
|
@ -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,192 +18,51 @@ 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
|
|
||||||
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 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
|
|
||||||
await writer.WriteLineAsync("<div id=\"log\">");
|
|
||||||
foreach (var group in log.MessageGroups)
|
|
||||||
{
|
|
||||||
await writer.WriteLineAsync("<div class=\"msg\">");
|
|
||||||
await writer.WriteLineAsync("<div class=\"msg-left\">");
|
|
||||||
await writer.WriteLineAsync($"<img class=\"msg-avatar\" src=\"{group.Author.AvatarUrl}\" />");
|
|
||||||
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>");
|
|
||||||
await writer.WriteLineAsync("</html>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log)
|
|
||||||
{
|
{
|
||||||
if (format == ExportFormat.PlainText)
|
if (format == ExportFormat.PlainText)
|
||||||
{
|
{
|
||||||
return ExportAsTextAsync(filePath, log);
|
await ExportAsPlainTextAsync(log, output);
|
||||||
}
|
|
||||||
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));
|
else if (format == ExportFormat.HtmlDark)
|
||||||
|
{
|
||||||
|
var css = Assembly.GetExecutingAssembly()
|
||||||
|
.GetManifestResourceString("DiscordChatExporter.Core.Resources.ExportService.DarkTheme.css");
|
||||||
|
await ExportAsHtmlAsync(log, output, css);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (format == ExportFormat.HtmlLight)
|
||||||
|
{
|
||||||
|
var css = Assembly.GetExecutingAssembly()
|
||||||
|
.GetManifestResourceString("DiscordChatExporter.Core.Resources.ExportService.LightTheme.css");
|
||||||
|
await ExportAsHtmlAsync(log, output, css);
|
||||||
|
}
|
||||||
|
|
||||||
|
else throw new ArgumentOutOfRangeException(nameof(format));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class ExportService
|
public partial class ExportService
|
||||||
{
|
{
|
||||||
|
private static string Base64Encode(string str)
|
||||||
|
{
|
||||||
|
return str.GetBytes().ToBase64();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Base64Decode(string str)
|
||||||
|
{
|
||||||
|
return str.FromBase64().GetString();
|
||||||
|
}
|
||||||
|
|
||||||
private static string HtmlEncode(string str)
|
private static string HtmlEncode(string str)
|
||||||
{
|
{
|
||||||
return WebUtility.HtmlEncode(str);
|
return WebUtility.HtmlEncode(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string HtmlEncode(object obj)
|
|
||||||
{
|
|
||||||
return WebUtility.HtmlEncode(obj.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatFileSize(long fileSize)
|
private static string FormatFileSize(long fileSize)
|
||||||
{
|
{
|
||||||
string[] units = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
|
string[] units = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
|
||||||
|
@ -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, $"<@!?{mentionedUser.Id}>",
|
|
||||||
$"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser)}\">" +
|
|
||||||
$"@{HtmlEncode(mentionedUser.Name)}" +
|
|
||||||
"</span>");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Role mentions (<@&id>)
|
|
||||||
foreach (var mentionedRole in message.MentionedRoles)
|
|
||||||
{
|
|
||||||
content = content.Replace($"<@&{mentionedRole.Id}>",
|
|
||||||
"<span class=\"mention\">" +
|
|
||||||
$"@{HtmlEncode(mentionedRole.Name)}" +
|
|
||||||
"</span>");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel mentions (<#id>)
|
|
||||||
foreach (var mentionedChannel in message.MentionedChannels)
|
|
||||||
{
|
|
||||||
content = content.Replace($"<#{mentionedChannel.Id}>",
|
|
||||||
"<span class=\"mention\">" +
|
|
||||||
$"#{HtmlEncode(mentionedChannel.Name)}" +
|
|
||||||
"</span>");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom emojis (<:name:id>)
|
|
||||||
content = Regex.Replace(content, "<(:.*?:)(\\d*)>",
|
|
||||||
"<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…
Add table
Add a link
Reference in a new issue