From a556926fe6b780d5b9c10f2e835dabed35d516c5 Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Sat, 30 Sep 2017 17:18:41 +0300 Subject: [PATCH] Rework HTML export Fixes #8 --- .../DiscordChatExporter.csproj | 4 - DiscordChatExporter/Models/AttachmentType.cs | 2 +- DiscordChatExporter/Models/ChannelChatLog.cs | 9 +- DiscordChatExporter/Models/Message.cs | 5 +- DiscordChatExporter/Models/MessageGroup.cs | 5 +- .../Resources/ExportService/Template.html | 18 -- DiscordChatExporter/Services/DataService.cs | 18 +- DiscordChatExporter/Services/ExportService.cs | 184 ++++++++---------- DiscordChatExporter/Services/IDataService.cs | 8 +- .../Services/IMessageGroupService.cs | 2 +- .../Services/MessageGroupService.cs | 6 +- .../ViewModels/MainViewModel.cs | 5 +- DiscordChatExporter/packages.config | 1 - 13 files changed, 116 insertions(+), 151 deletions(-) delete mode 100644 DiscordChatExporter/Resources/ExportService/Template.html diff --git a/DiscordChatExporter/DiscordChatExporter.csproj b/DiscordChatExporter/DiscordChatExporter.csproj index 3260cc2..5439cf5 100644 --- a/DiscordChatExporter/DiscordChatExporter.csproj +++ b/DiscordChatExporter/DiscordChatExporter.csproj @@ -48,9 +48,6 @@ ..\packages\MvvmLightLibs.5.3.0.0\lib\net45\GalaSoft.MvvmLight.Platform.dll - - ..\packages\HtmlAgilityPack.1.5.5\lib\Net45\HtmlAgilityPack.dll - ..\packages\MaterialDesignColors.1.1.3\lib\net45\MaterialDesignColors.dll @@ -186,7 +183,6 @@ - diff --git a/DiscordChatExporter/Models/AttachmentType.cs b/DiscordChatExporter/Models/AttachmentType.cs index 13fcccc..2444ff5 100644 --- a/DiscordChatExporter/Models/AttachmentType.cs +++ b/DiscordChatExporter/Models/AttachmentType.cs @@ -2,7 +2,7 @@ { public enum AttachmentType { - Unrecognized, + Other, Image } } \ No newline at end of file diff --git a/DiscordChatExporter/Models/ChannelChatLog.cs b/DiscordChatExporter/Models/ChannelChatLog.cs index 19c3cb7..81c6d2d 100644 --- a/DiscordChatExporter/Models/ChannelChatLog.cs +++ b/DiscordChatExporter/Models/ChannelChatLog.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; namespace DiscordChatExporter.Models { @@ -11,11 +10,15 @@ namespace DiscordChatExporter.Models public IReadOnlyList MessageGroups { get; } - public ChannelChatLog(Guild guild, Channel channel, IEnumerable messageGroups) + public int TotalMessageCount { get; } + + public ChannelChatLog(Guild guild, Channel channel, IReadOnlyList messageGroups, + int totalMessageCount) { Guild = guild; Channel = channel; - MessageGroups = messageGroups.ToArray(); + MessageGroups = messageGroups; + TotalMessageCount = totalMessageCount; } } } \ No newline at end of file diff --git a/DiscordChatExporter/Models/Message.cs b/DiscordChatExporter/Models/Message.cs index 31adb5f..a672333 100644 --- a/DiscordChatExporter/Models/Message.cs +++ b/DiscordChatExporter/Models/Message.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; namespace DiscordChatExporter.Models { @@ -20,14 +19,14 @@ namespace DiscordChatExporter.Models public Message(string id, User author, DateTime timeStamp, DateTime? editedTimeStamp, - string content, IEnumerable attachments) + string content, IReadOnlyList attachments) { Id = id; Author = author; TimeStamp = timeStamp; EditedTimeStamp = editedTimeStamp; Content = content; - Attachments = attachments.ToArray(); + Attachments = attachments; } public override string ToString() diff --git a/DiscordChatExporter/Models/MessageGroup.cs b/DiscordChatExporter/Models/MessageGroup.cs index 5d394cd..a55a51e 100644 --- a/DiscordChatExporter/Models/MessageGroup.cs +++ b/DiscordChatExporter/Models/MessageGroup.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; namespace DiscordChatExporter.Models { @@ -12,11 +11,11 @@ namespace DiscordChatExporter.Models public IReadOnlyList Messages { get; } - public MessageGroup(User author, DateTime timeStamp, IEnumerable messages) + public MessageGroup(User author, DateTime timeStamp, IReadOnlyList messages) { Author = author; TimeStamp = timeStamp; - Messages = messages.ToArray(); + Messages = messages; } } } \ No newline at end of file diff --git a/DiscordChatExporter/Resources/ExportService/Template.html b/DiscordChatExporter/Resources/ExportService/Template.html deleted file mode 100644 index 738d777..0000000 --- a/DiscordChatExporter/Resources/ExportService/Template.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Discord Chat Log - - - - - - - - -
-
- - \ No newline at end of file diff --git a/DiscordChatExporter/Services/DataService.cs b/DiscordChatExporter/Services/DataService.cs index c1aa2ad..979feea 100644 --- a/DiscordChatExporter/Services/DataService.cs +++ b/DiscordChatExporter/Services/DataService.cs @@ -29,7 +29,7 @@ namespace DiscordChatExporter.Services } } - public async Task> GetGuildsAsync(string token) + public async Task> GetGuildsAsync(string token) { // Form request url var url = $"{ApiRoot}/users/@me/guilds?token={token}&limit=100"; @@ -38,12 +38,12 @@ namespace DiscordChatExporter.Services var content = await GetStringAsync(url); // Parse - var guilds = JArray.Parse(content).Select(ParseGuild); + var guilds = JArray.Parse(content).Select(ParseGuild).ToArray(); return guilds; } - public async Task> GetDirectMessageChannelsAsync(string token) + public async Task> GetDirectMessageChannelsAsync(string token) { // Form request url var url = $"{ApiRoot}/users/@me/channels?token={token}"; @@ -52,12 +52,12 @@ namespace DiscordChatExporter.Services var content = await GetStringAsync(url); // Parse - var channels = JArray.Parse(content).Select(ParseChannel); + var channels = JArray.Parse(content).Select(ParseChannel).ToArray(); return channels; } - public async Task> GetGuildChannelsAsync(string token, string guildId) + public async Task> GetGuildChannelsAsync(string token, string guildId) { // Form request url var url = $"{ApiRoot}/guilds/{guildId}/channels?token={token}"; @@ -66,12 +66,12 @@ namespace DiscordChatExporter.Services var content = await GetStringAsync(url); // Parse - var channels = JArray.Parse(content).Select(ParseChannel); + var channels = JArray.Parse(content).Select(ParseChannel).ToArray(); return channels; } - public async Task> GetChannelMessagesAsync(string token, string channelId) + public async Task> GetChannelMessagesAsync(string token, string channelId) { var result = new List(); @@ -92,7 +92,7 @@ namespace DiscordChatExporter.Services var messages = JArray.Parse(content).Select(ParseMessage); // Add messages to list - string currentMessageId = null; + var currentMessageId = default(string); foreach (var message in messages) { result.Add(message); @@ -192,7 +192,7 @@ namespace DiscordChatExporter.Services var attachmentUrl = attachmentJson.Value("url"); var attachmentType = attachmentJson["width"] != null ? AttachmentType.Image - : AttachmentType.Unrecognized; + : AttachmentType.Other; var attachmentFileName = attachmentJson.Value("filename"); var attachmentFileSize = attachmentJson.Value("size"); diff --git a/DiscordChatExporter/Services/ExportService.cs b/DiscordChatExporter/Services/ExportService.cs index 331b7fe..633a023 100644 --- a/DiscordChatExporter/Services/ExportService.cs +++ b/DiscordChatExporter/Services/ExportService.cs @@ -1,11 +1,11 @@ using System.IO; -using System.Linq; +using System.Net; using System.Reflection; using System.Resources; +using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using DiscordChatExporter.Models; -using HtmlAgilityPack; using Tyrrrz.Extensions; namespace DiscordChatExporter.Services @@ -19,80 +19,77 @@ namespace DiscordChatExporter.Services _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(); - var style = GetStyle(theme); - var dateFormat = _settingsService.DateFormat; - - // Set theme - var themeHtml = doc.GetElementbyId("theme"); - themeHtml.InnerHtml = style; - - // Title - var titleHtml = doc.DocumentNode.Element("html").Element("head").Element("title"); - titleHtml.InnerHtml = $"{log.Guild.Name} - {log.Channel.Name}"; - - // Info - var infoHtml = doc.GetElementbyId("info"); - var infoLeftHtml = infoHtml.AppendChild(HtmlNode.CreateNode("
")); - infoLeftHtml.AppendChild(HtmlNode.CreateNode( - $"")); - var infoRightHtml = infoHtml.AppendChild(HtmlNode.CreateNode("
")); - infoRightHtml.AppendChild(HtmlNode.CreateNode( - $"
{log.Guild.Name}
")); - infoRightHtml.AppendChild(HtmlNode.CreateNode( - $"
{log.Channel.Name}
")); - infoRightHtml.AppendChild(HtmlNode.CreateNode( - $"
{log.MessageGroups.SelectMany(g => g.Messages).Count():N0} messages
")); - - // Log - var logHtml = doc.GetElementbyId("log"); - foreach (var messageGroup in log.MessageGroups) + // Generation info + await writer.WriteLineAsync(""); + + // Html start + await writer.WriteLineAsync(""); + await writer.WriteLineAsync(""); + + // HEAD + await writer.WriteLineAsync(""); + await writer.WriteLineAsync($"{log.Guild.Name} - {log.Channel.Name}"); + await writer.WriteLineAsync(""); + await writer.WriteLineAsync(""); + await writer.WriteLineAsync($""); + await writer.WriteLineAsync(""); + + // Body start + await writer.WriteLineAsync(""); + + // Guild and channel info + await writer.WriteLineAsync("
"); + await writer.WriteLineAsync("
"); + await writer.WriteLineAsync($""); + await writer.WriteLineAsync("
"); // info-left + await writer.WriteLineAsync("
"); + await writer.WriteLineAsync($"
{log.Guild.Name}
"); + await writer.WriteLineAsync($"
{log.Channel.Name}
"); + await writer.WriteLineAsync($"
{log.TotalMessageCount:N0} messages
"); + await writer.WriteLineAsync("
"); // info-right + await writer.WriteLineAsync("
"); // info + + // Chat log + await writer.WriteLineAsync("
"); + foreach (var group in log.MessageGroups) { - // Container - var messageHtml = logHtml.AppendChild(HtmlNode.CreateNode("
")); - - // Left - var messageLeftHtml = - messageHtml.AppendChild(HtmlNode.CreateNode("
")); - - // Avatar - messageLeftHtml.AppendChild( - HtmlNode.CreateNode($"")); - - // Right - var messageRightHtml = - messageHtml.AppendChild(HtmlNode.CreateNode("
")); - - // Author - var authorName = HtmlDocument.HtmlEncode(messageGroup.Author.Name); - messageRightHtml.AppendChild(HtmlNode.CreateNode($"{authorName}")); - - // Date - var timeStamp = HtmlDocument.HtmlEncode(messageGroup.TimeStamp.ToString(dateFormat)); - messageRightHtml.AppendChild(HtmlNode.CreateNode($"{timeStamp}")); - - // Individual messages - foreach (var message in messageGroup.Messages) + await writer.WriteLineAsync("
"); + await writer.WriteLineAsync("
"); + await writer.WriteLineAsync($""); + await writer.WriteLineAsync("
"); + + await writer.WriteLineAsync("
"); + await writer.WriteLineAsync($"{HtmlEncode(group.Author.Name)}"); + var timeStampFormatted = HtmlEncode(group.TimeStamp.ToString(dateFormat)); + await writer.WriteLineAsync($"{timeStampFormatted}"); + + // Message + foreach (var message in group.Messages) { // Content if (message.Content.IsNotBlank()) { - var content = FormatMessageContent(message.Content); - var contentHtml = - messageRightHtml.AppendChild( - HtmlNode.CreateNode($"
{content}
")); + await writer.WriteLineAsync("
"); + var contentFormatted = FormatMessageContent(message.Content); + await writer.WriteAsync(contentFormatted); // Edited timestamp if (message.EditedTimeStamp != null) { - contentHtml.AppendChild( - HtmlNode.CreateNode( - $"(edited)")); + var editedTimeStampFormatted = + HtmlEncode(message.EditedTimeStamp.Value.ToString(dateFormat)); + await writer.WriteAsync( + $"(edited)"); } + + await writer.WriteLineAsync("
"); // msg-content } // Attachments @@ -100,51 +97,37 @@ namespace DiscordChatExporter.Services { if (attachment.Type == AttachmentType.Image) { - messageRightHtml.AppendChild( - HtmlNode.CreateNode("
" + - $"" + - $"" + - "" + - "
")); + await writer.WriteLineAsync("
"); + await writer.WriteLineAsync($""); + await writer.WriteLineAsync($""); + await writer.WriteLineAsync(""); + await writer.WriteLineAsync("
"); } else { - messageRightHtml.AppendChild( - HtmlNode.CreateNode("")); + await writer.WriteLineAsync(""); } } } + await writer.WriteLineAsync("
"); // msg-right + await writer.WriteLineAsync("
"); // msg } + await writer.WriteLineAsync("
"); // log - doc.Save(filePath); - }); + await writer.WriteLineAsync(""); + await writer.WriteLineAsync(""); + } } } public partial class ExportService { - private static HtmlDocument GetTemplate() - { - 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) + private static string GetThemeCss(Theme theme) { 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"}; double size = fileSize; @@ -178,7 +166,7 @@ namespace DiscordChatExporter.Services private static string FormatMessageContent(string content) { // Encode HTML - content = HtmlDocument.HtmlEncode(content); + content = HtmlEncode(content); // Links from URLs content = Regex.Replace(content, "((https?|ftp)://[^\\s/$.?#].[^\\s]*)", diff --git a/DiscordChatExporter/Services/IDataService.cs b/DiscordChatExporter/Services/IDataService.cs index 53b4421..43bd517 100644 --- a/DiscordChatExporter/Services/IDataService.cs +++ b/DiscordChatExporter/Services/IDataService.cs @@ -6,12 +6,12 @@ namespace DiscordChatExporter.Services { public interface IDataService { - Task> GetGuildsAsync(string token); + Task> GetGuildsAsync(string token); - Task> GetDirectMessageChannelsAsync(string token); + Task> GetDirectMessageChannelsAsync(string token); - Task> GetGuildChannelsAsync(string token, string guildId); + Task> GetGuildChannelsAsync(string token, string guildId); - Task> GetChannelMessagesAsync(string token, string channelId); + Task> GetChannelMessagesAsync(string token, string channelId); } } \ No newline at end of file diff --git a/DiscordChatExporter/Services/IMessageGroupService.cs b/DiscordChatExporter/Services/IMessageGroupService.cs index 256faeb..5739ea4 100644 --- a/DiscordChatExporter/Services/IMessageGroupService.cs +++ b/DiscordChatExporter/Services/IMessageGroupService.cs @@ -5,6 +5,6 @@ namespace DiscordChatExporter.Services { public interface IMessageGroupService { - IEnumerable GroupMessages(IEnumerable messages); + IReadOnlyList GroupMessages(IReadOnlyList messages); } } \ No newline at end of file diff --git a/DiscordChatExporter/Services/MessageGroupService.cs b/DiscordChatExporter/Services/MessageGroupService.cs index 7dd05c3..bb5218c 100644 --- a/DiscordChatExporter/Services/MessageGroupService.cs +++ b/DiscordChatExporter/Services/MessageGroupService.cs @@ -13,7 +13,7 @@ namespace DiscordChatExporter.Services _settingsService = settingsService; } - public IEnumerable GroupMessages(IEnumerable messages) + public IReadOnlyList GroupMessages(IReadOnlyList messages) { var groupLimit = _settingsService.MessageGroupLimit; var result = new List(); @@ -37,7 +37,7 @@ namespace DiscordChatExporter.Services // If condition is true - flush buffer if (breakCondition) { - var group = new MessageGroup(groupFirst.Author, groupFirst.TimeStamp, groupBuffer); + var group = new MessageGroup(groupFirst.Author, groupFirst.TimeStamp, groupBuffer.ToArray()); result.Add(group); groupBuffer.Clear(); } @@ -50,7 +50,7 @@ namespace DiscordChatExporter.Services if (groupBuffer.Any()) { var groupFirst = groupBuffer.First(); - var group = new MessageGroup(groupFirst.Author, groupFirst.TimeStamp, groupBuffer); + var group = new MessageGroup(groupFirst.Author, groupFirst.TimeStamp, groupBuffer.ToArray()); result.Add(group); } diff --git a/DiscordChatExporter/ViewModels/MainViewModel.cs b/DiscordChatExporter/ViewModels/MainViewModel.cs index 2778aa1..4fcbcea 100644 --- a/DiscordChatExporter/ViewModels/MainViewModel.cs +++ b/DiscordChatExporter/ViewModels/MainViewModel.cs @@ -127,8 +127,7 @@ namespace DiscordChatExporter.ViewModels foreach (var guild in guilds) { var channels = await _dataService.GetGuildChannelsAsync(_cachedToken, guild.Id); - channels = channels.Where(c => c.Type == ChannelType.GuildTextChat); - _guildChannelsMap[guild] = channels.ToArray(); + _guildChannelsMap[guild] = channels.Where(c => c.Type == ChannelType.GuildTextChat).ToArray(); } } } @@ -175,7 +174,7 @@ namespace DiscordChatExporter.ViewModels var messageGroups = _messageGroupService.GroupMessages(messages); // Create log - var chatLog = new ChannelChatLog(SelectedGuild, channel, messageGroups); + var chatLog = new ChannelChatLog(SelectedGuild, channel, messageGroups, messages.Count); // Export await _exportService.ExportAsync(sfd.FileName, chatLog, _settingsService.Theme); diff --git a/DiscordChatExporter/packages.config b/DiscordChatExporter/packages.config index f21e4b9..1ba795d 100644 --- a/DiscordChatExporter/packages.config +++ b/DiscordChatExporter/packages.config @@ -3,7 +3,6 @@ -