|
|
@ -1,11 +1,11 @@
|
|
|
|
using System.IO;
|
|
|
|
using System.IO;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Net;
|
|
|
|
using System.Reflection;
|
|
|
|
using System.Reflection;
|
|
|
|
using System.Resources;
|
|
|
|
using System.Resources;
|
|
|
|
|
|
|
|
using System.Text;
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
using DiscordChatExporter.Models;
|
|
|
|
using DiscordChatExporter.Models;
|
|
|
|
using HtmlAgilityPack;
|
|
|
|
|
|
|
|
using Tyrrrz.Extensions;
|
|
|
|
using Tyrrrz.Extensions;
|
|
|
|
|
|
|
|
|
|
|
|
namespace DiscordChatExporter.Services
|
|
|
|
namespace DiscordChatExporter.Services
|
|
|
@ -19,80 +19,77 @@ namespace DiscordChatExporter.Services
|
|
|
|
_settingsService = settingsService;
|
|
|
|
_settingsService = settingsService;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Task ExportAsync(string filePath, ChannelChatLog log, Theme theme)
|
|
|
|
public async Task ExportAsync(string filePath, ChannelChatLog log, Theme theme)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
return Task.Run(() =>
|
|
|
|
var themeCss = GetThemeCss(theme);
|
|
|
|
|
|
|
|
var dateFormat = _settingsService.DateFormat;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128*1024))
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var doc = GetTemplate();
|
|
|
|
// Generation info
|
|
|
|
var style = GetStyle(theme);
|
|
|
|
await writer.WriteLineAsync("<!-- https://github.com/Tyrrrz/DiscordChatExporter -->");
|
|
|
|
var dateFormat = _settingsService.DateFormat;
|
|
|
|
|
|
|
|
|
|
|
|
// Html start
|
|
|
|
// Set theme
|
|
|
|
await writer.WriteLineAsync("<!DOCTYPE html>");
|
|
|
|
var themeHtml = doc.GetElementbyId("theme");
|
|
|
|
await writer.WriteLineAsync("<html lang=\"en\">");
|
|
|
|
themeHtml.InnerHtml = style;
|
|
|
|
|
|
|
|
|
|
|
|
// HEAD
|
|
|
|
// Title
|
|
|
|
await writer.WriteLineAsync("<head>");
|
|
|
|
var titleHtml = doc.DocumentNode.Element("html").Element("head").Element("title");
|
|
|
|
await writer.WriteLineAsync($"<title>{log.Guild.Name} - {log.Channel.Name}</title>");
|
|
|
|
titleHtml.InnerHtml = $"{log.Guild.Name} - {log.Channel.Name}";
|
|
|
|
await writer.WriteLineAsync("<meta charset=\"utf-8\" />");
|
|
|
|
|
|
|
|
await writer.WriteLineAsync("<meta name=\"viewport\" content=\"width=device-width\" />");
|
|
|
|
// Info
|
|
|
|
await writer.WriteLineAsync($"<style>{themeCss}</style>");
|
|
|
|
var infoHtml = doc.GetElementbyId("info");
|
|
|
|
await writer.WriteLineAsync("</head>");
|
|
|
|
var infoLeftHtml = infoHtml.AppendChild(HtmlNode.CreateNode("<div class=\"info-left\"></div>"));
|
|
|
|
|
|
|
|
infoLeftHtml.AppendChild(HtmlNode.CreateNode(
|
|
|
|
// Body start
|
|
|
|
$"<img class=\"guild-icon\" src=\"{log.Guild.IconUrl}\" />"));
|
|
|
|
await writer.WriteLineAsync("<body>");
|
|
|
|
var infoRightHtml = infoHtml.AppendChild(HtmlNode.CreateNode("<div class=\"info-right\"></div>"));
|
|
|
|
|
|
|
|
infoRightHtml.AppendChild(HtmlNode.CreateNode(
|
|
|
|
// Guild and channel info
|
|
|
|
$"<div class=\"guild-name\">{log.Guild.Name}</div>"));
|
|
|
|
await writer.WriteLineAsync("<div id=\"info\">");
|
|
|
|
infoRightHtml.AppendChild(HtmlNode.CreateNode(
|
|
|
|
await writer.WriteLineAsync("<div class=\"info-left\">");
|
|
|
|
$"<div class=\"channel-name\">{log.Channel.Name}</div>"));
|
|
|
|
await writer.WriteLineAsync($"<img class=\"guild-icon\" src=\"{log.Guild.IconUrl}\" />");
|
|
|
|
infoRightHtml.AppendChild(HtmlNode.CreateNode(
|
|
|
|
await writer.WriteLineAsync("</div>"); // info-left
|
|
|
|
$"<div class=\"misc\">{log.MessageGroups.SelectMany(g => g.Messages).Count():N0} messages</div>"));
|
|
|
|
await writer.WriteLineAsync("<div class=\"info-right\">");
|
|
|
|
|
|
|
|
await writer.WriteLineAsync($"<div class=\"guild-name\">{log.Guild.Name}</div>");
|
|
|
|
// Log
|
|
|
|
await writer.WriteLineAsync($"<div class=\"channel-name\">{log.Channel.Name}</div>");
|
|
|
|
var logHtml = doc.GetElementbyId("log");
|
|
|
|
await writer.WriteLineAsync($"<div class=\"misc\">{log.TotalMessageCount:N0} messages</div>");
|
|
|
|
foreach (var messageGroup in log.MessageGroups)
|
|
|
|
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)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
// Container
|
|
|
|
await writer.WriteLineAsync("<div class=\"msg\">");
|
|
|
|
var messageHtml = logHtml.AppendChild(HtmlNode.CreateNode("<div class=\"msg\"></div>"));
|
|
|
|
await writer.WriteLineAsync("<div class=\"msg-left\">");
|
|
|
|
|
|
|
|
await writer.WriteLineAsync($"<img class=\"msg-avatar\" src=\"{group.Author.AvatarUrl}\" />");
|
|
|
|
// Left
|
|
|
|
await writer.WriteLineAsync("</div>");
|
|
|
|
var messageLeftHtml =
|
|
|
|
|
|
|
|
messageHtml.AppendChild(HtmlNode.CreateNode("<div class=\"msg-left\"></div>"));
|
|
|
|
await writer.WriteLineAsync("<div class=\"msg-right\">");
|
|
|
|
|
|
|
|
await writer.WriteLineAsync($"<span class=\"msg-user\">{HtmlEncode(group.Author.Name)}</span>");
|
|
|
|
// Avatar
|
|
|
|
var timeStampFormatted = HtmlEncode(group.TimeStamp.ToString(dateFormat));
|
|
|
|
messageLeftHtml.AppendChild(
|
|
|
|
await writer.WriteLineAsync($"<span class=\"msg-date\">{timeStampFormatted}</span>");
|
|
|
|
HtmlNode.CreateNode($"<img class=\"msg-avatar\" src=\"{messageGroup.Author.AvatarUrl}\" />"));
|
|
|
|
|
|
|
|
|
|
|
|
// Message
|
|
|
|
// Right
|
|
|
|
foreach (var message in group.Messages)
|
|
|
|
var messageRightHtml =
|
|
|
|
|
|
|
|
messageHtml.AppendChild(HtmlNode.CreateNode("<div class=\"msg-right\"></div>"));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Author
|
|
|
|
|
|
|
|
var authorName = HtmlDocument.HtmlEncode(messageGroup.Author.Name);
|
|
|
|
|
|
|
|
messageRightHtml.AppendChild(HtmlNode.CreateNode($"<span class=\"msg-user\">{authorName}</span>"));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Date
|
|
|
|
|
|
|
|
var timeStamp = HtmlDocument.HtmlEncode(messageGroup.TimeStamp.ToString(dateFormat));
|
|
|
|
|
|
|
|
messageRightHtml.AppendChild(HtmlNode.CreateNode($"<span class=\"msg-date\">{timeStamp}</span>"));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Individual messages
|
|
|
|
|
|
|
|
foreach (var message in messageGroup.Messages)
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
// Content
|
|
|
|
// Content
|
|
|
|
if (message.Content.IsNotBlank())
|
|
|
|
if (message.Content.IsNotBlank())
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var content = FormatMessageContent(message.Content);
|
|
|
|
await writer.WriteLineAsync("<div class=\"msg-content\">");
|
|
|
|
var contentHtml =
|
|
|
|
var contentFormatted = FormatMessageContent(message.Content);
|
|
|
|
messageRightHtml.AppendChild(
|
|
|
|
await writer.WriteAsync(contentFormatted);
|
|
|
|
HtmlNode.CreateNode($"<div class=\"msg-content\">{content}</div>"));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Edited timestamp
|
|
|
|
// Edited timestamp
|
|
|
|
if (message.EditedTimeStamp != null)
|
|
|
|
if (message.EditedTimeStamp != null)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
contentHtml.AppendChild(
|
|
|
|
var editedTimeStampFormatted =
|
|
|
|
HtmlNode.CreateNode(
|
|
|
|
HtmlEncode(message.EditedTimeStamp.Value.ToString(dateFormat));
|
|
|
|
$"<span class=\"msg-edited\" title=\"{message.EditedTimeStamp.Value.ToString(dateFormat)}\">(edited)</span>"));
|
|
|
|
await writer.WriteAsync(
|
|
|
|
|
|
|
|
$"<span class=\"msg-edited\" title=\"{editedTimeStampFormatted}\">(edited)</span>");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await writer.WriteLineAsync("</div>"); // msg-content
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Attachments
|
|
|
|
// Attachments
|
|
|
@ -100,51 +97,37 @@ namespace DiscordChatExporter.Services
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (attachment.Type == AttachmentType.Image)
|
|
|
|
if (attachment.Type == AttachmentType.Image)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
messageRightHtml.AppendChild(
|
|
|
|
await writer.WriteLineAsync("<div class=\"msg-attachment\">");
|
|
|
|
HtmlNode.CreateNode("<div class=\"msg-attachment\">" +
|
|
|
|
await writer.WriteLineAsync($"<a href=\"{attachment.Url}\">");
|
|
|
|
$"<a href=\"{attachment.Url}\">" +
|
|
|
|
await writer.WriteLineAsync($"<img class=\"msg-attachment\" src=\"{attachment.Url}\" />");
|
|
|
|
$"<img class=\"msg-attachment\" src=\"{attachment.Url}\" />" +
|
|
|
|
await writer.WriteLineAsync("</a>");
|
|
|
|
"</a>" +
|
|
|
|
await writer.WriteLineAsync("</div>");
|
|
|
|
"</div>"));
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
else
|
|
|
|
{
|
|
|
|
{
|
|
|
|
messageRightHtml.AppendChild(
|
|
|
|
await writer.WriteLineAsync("<div class=\"msg-attachment\">");
|
|
|
|
HtmlNode.CreateNode("<div class=\"msg-attachment\">" +
|
|
|
|
await writer.WriteLineAsync($"<a href=\"{attachment.Url}\">");
|
|
|
|
$"<a href=\"{attachment.Url}\">" +
|
|
|
|
var fileSizeFormatted = FormatFileSize(attachment.FileSize);
|
|
|
|
$"Attachment: {attachment.FileName} ({NormalizeFileSize(attachment.FileSize)})" +
|
|
|
|
await writer.WriteLineAsync($"Attachment: {attachment.FileName} ({fileSizeFormatted})");
|
|
|
|
"</a>" +
|
|
|
|
await writer.WriteLineAsync("</a>");
|
|
|
|
"</div>"));
|
|
|
|
await writer.WriteLineAsync("</div>");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await writer.WriteLineAsync("</div>"); // msg-right
|
|
|
|
|
|
|
|
await writer.WriteLineAsync("</div>"); // msg
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await writer.WriteLineAsync("</div>"); // log
|
|
|
|
|
|
|
|
|
|
|
|
doc.Save(filePath);
|
|
|
|
await writer.WriteLineAsync("</body>");
|
|
|
|
});
|
|
|
|
await writer.WriteLineAsync("</html>");
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public partial class ExportService
|
|
|
|
public partial class ExportService
|
|
|
|
{
|
|
|
|
{
|
|
|
|
private static HtmlDocument GetTemplate()
|
|
|
|
private static string GetThemeCss(Theme theme)
|
|
|
|
{
|
|
|
|
|
|
|
|
var resourcePath = "DiscordChatExporter.Resources.ExportService.Template.html";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var assembly = Assembly.GetExecutingAssembly();
|
|
|
|
|
|
|
|
var stream = assembly.GetManifestResourceStream(resourcePath);
|
|
|
|
|
|
|
|
if (stream == null)
|
|
|
|
|
|
|
|
throw new MissingManifestResourceException("Could not find template resource");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
using (stream)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var doc = new HtmlDocument();
|
|
|
|
|
|
|
|
doc.Load(stream);
|
|
|
|
|
|
|
|
return doc;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static string GetStyle(Theme theme)
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var resourcePath = $"DiscordChatExporter.Resources.ExportService.{theme}Theme.css";
|
|
|
|
var resourcePath = $"DiscordChatExporter.Resources.ExportService.{theme}Theme.css";
|
|
|
|
|
|
|
|
|
|
|
@ -160,7 +143,12 @@ namespace DiscordChatExporter.Services
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string NormalizeFileSize(long fileSize)
|
|
|
|
private static string HtmlEncode(string str)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
return WebUtility.HtmlEncode(str);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"};
|
|
|
|
double size = fileSize;
|
|
|
|
double size = fileSize;
|
|
|
@ -178,7 +166,7 @@ namespace DiscordChatExporter.Services
|
|
|
|
private static string FormatMessageContent(string content)
|
|
|
|
private static string FormatMessageContent(string content)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
// Encode HTML
|
|
|
|
// Encode HTML
|
|
|
|
content = HtmlDocument.HtmlEncode(content);
|
|
|
|
content = HtmlEncode(content);
|
|
|
|
|
|
|
|
|
|
|
|
// Links from URLs
|
|
|
|
// Links from URLs
|
|
|
|
content = Regex.Replace(content, "((https?|ftp)://[^\\s/$.?#].[^\\s]*)",
|
|
|
|
content = Regex.Replace(content, "((https?|ftp)://[^\\s/$.?#].[^\\s]*)",
|
|
|
|