Compare commits

..

9 Commits

Author SHA1 Message Date
Oleksii Holub 77bf32aa5a Update version 2019-03-14 16:36:55 +02:00
Oleksii Holub ac2b450892 Retry on any 5xx HTTP response in DataService
Closes #157
2019-03-14 16:30:27 +02:00
Oleksii Holub 4046fd0ad9 [HTML] Add syntax highlighting via Highlight.js
Closes #138
2019-03-14 16:02:39 +02:00
Alexey Golub 6fa61bf75a Update version 2019-03-10 14:45:39 +02:00
Alexey Golub 1976e3ad08 [CLI] Don't write progress if output is redirected
FIxes #155
2019-03-10 14:40:08 +02:00
Alexey Golub adbe544c57 Add some margin at the end of the chatlog in HTML 2019-03-07 22:15:21 +02:00
Alexey Golub 6f4d1c3d77 Refactor message grouping 2019-03-07 22:13:00 +02:00
Alexey Golub 33f71c1612 Remove message group limit setting 2019-03-07 21:14:11 +02:00
Alexey Golub 9b78472e05 Update message grouping algorithm to match Discord's
Fixes #152
2019-03-07 21:10:22 +02:00
21 changed files with 106 additions and 115 deletions
+11
View File
@@ -1,3 +1,14 @@
### v2.11 (14-Mar-2019)
- [HTML] Added syntax highlighting for multiline code blocks via Highlight.js.
- Added retry policy for all 5xx status codes to prevent random crashes.
### v2.10.2 (10-Mar-2019)
- [HTML] Updated message grouping algorithm to make it the same as in Discord. Removed "message group limit" setting and parameter.
- [HTML] Added small margin at the end of the chatlog so it doesn't look like it was truncated.
- [CLI] Fixed an issue where the app would crash if stdout was redirected. Progress will not be reported in such cases.
### v2.10.1 (06-Mar-2019) ### v2.10.1 (06-Mar-2019)
- [HTML] Fixed an issue where multiple emojis on a single line would get rendered as one emoji. - [HTML] Fixed an issue where multiple emojis on a single line would get rendered as one emoji.
@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net461</TargetFramework> <TargetFramework>net461</TargetFramework>
<Version>2.10.1</Version> <Version>2.11</Version>
<Company>Tyrrrz</Company> <Company>Tyrrrz</Company>
<Copyright>Copyright (c) Alexey Golub</Copyright> <Copyright>Copyright (c) Alexey Golub</Copyright>
<ApplicationIcon>..\favicon.ico</ApplicationIcon> <ApplicationIcon>..\favicon.ico</ApplicationIcon>
@@ -9,19 +9,31 @@ namespace DiscordChatExporter.Cli.Internal
public InlineProgress() public InlineProgress()
{ {
_posX = Console.CursorLeft; // If output is not redirected - save initial cursor position
_posY = Console.CursorTop; if (!Console.IsOutputRedirected)
{
_posX = Console.CursorLeft;
_posY = Console.CursorTop;
}
} }
public void Report(double progress) public void Report(double progress)
{ {
Console.SetCursorPosition(_posX, _posY); // If output is not redirected - reset cursor position and write progress
Console.WriteLine($"{progress:P1}"); if (!Console.IsOutputRedirected)
{
Console.SetCursorPosition(_posX, _posY);
Console.WriteLine($"{progress:P1}");
}
} }
public void Dispose() public void Dispose()
{ {
Console.SetCursorPosition(_posX, _posY); // If output is not redirected - reset cursor position
if (!Console.IsOutputRedirected)
Console.SetCursorPosition(_posX, _posY);
// Inform about completion
Console.WriteLine("Completed ✓"); Console.WriteLine("Completed ✓");
} }
} }
@@ -26,8 +26,6 @@ namespace DiscordChatExporter.Cli.Verbs
// Configure settings // Configure settings
if (Options.DateFormat.IsNotBlank()) if (Options.DateFormat.IsNotBlank())
settingsService.DateFormat = Options.DateFormat; settingsService.DateFormat = Options.DateFormat;
if (Options.MessageGroupLimit > 0)
settingsService.MessageGroupLimit = Options.MessageGroupLimit;
// Track progress // Track progress
Console.Write($"Exporting channel [{Options.ChannelId}]... "); Console.Write($"Exporting channel [{Options.ChannelId}]... ");
@@ -29,8 +29,6 @@ namespace DiscordChatExporter.Cli.Verbs
// Configure settings // Configure settings
if (Options.DateFormat.IsNotBlank()) if (Options.DateFormat.IsNotBlank())
settingsService.DateFormat = Options.DateFormat; settingsService.DateFormat = Options.DateFormat;
if (Options.MessageGroupLimit > 0)
settingsService.MessageGroupLimit = Options.MessageGroupLimit;
// Get channels // Get channels
var channels = await dataService.GetDirectMessageChannelsAsync(Options.GetToken()); var channels = await dataService.GetDirectMessageChannelsAsync(Options.GetToken());
@@ -30,8 +30,6 @@ namespace DiscordChatExporter.Cli.Verbs
// Configure settings // Configure settings
if (Options.DateFormat.IsNotBlank()) if (Options.DateFormat.IsNotBlank())
settingsService.DateFormat = Options.DateFormat; settingsService.DateFormat = Options.DateFormat;
if (Options.MessageGroupLimit > 0)
settingsService.MessageGroupLimit = Options.MessageGroupLimit;
// Get channels // Get channels
var channels = await dataService.GetGuildChannelsAsync(Options.GetToken(), Options.GuildId); var channels = await dataService.GetGuildChannelsAsync(Options.GetToken(), Options.GuildId);
@@ -23,8 +23,5 @@ namespace DiscordChatExporter.Cli.Verbs.Options
[Option("dateformat", Default = null, HelpText = "Date format used in output.")] [Option("dateformat", Default = null, HelpText = "Date format used in output.")]
public string DateFormat { get; set; } public string DateFormat { get; set; }
[Option("grouplimit", Default = 0, HelpText = "Message group limit.")]
public int MessageGroupLimit { get; set; }
} }
} }
@@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq;
using System.Net; using System.Net;
namespace DiscordChatExporter.Core.Internal namespace DiscordChatExporter.Core.Internal
@@ -17,5 +19,34 @@ namespace DiscordChatExporter.Core.Internal
public static Color ResetAlpha(this Color color) => Color.FromArgb(1, color); public static Color ResetAlpha(this Color color) => Color.FromArgb(1, color);
public static string HtmlEncode(this string value) => WebUtility.HtmlEncode(value); public static string HtmlEncode(this string value) => WebUtility.HtmlEncode(value);
public static IEnumerable<IReadOnlyList<T>> GroupAdjacentWhile<T>(this IEnumerable<T> source,
Func<IReadOnlyList<T>, T, bool> groupPredicate)
{
// Create buffer
var buffer = new List<T>();
// Enumerate source
foreach (var element in source)
{
// If buffer is not empty and group predicate failed - yield and flush buffer
if (buffer.Any() && !groupPredicate(buffer, element))
{
yield return buffer;
buffer = new List<T>(); // new instance to reset reference
}
// Add element to buffer
buffer.Add(element);
}
// If buffer still has something after the source has been enumerated - yield
if (buffer.Any())
yield return buffer;
}
public static IEnumerable<IReadOnlyList<T>> GroupAdjacentWhile<T>(this IEnumerable<T> source,
Func<IReadOnlyList<T>, bool> groupPredicate)
=> source.GroupAdjacentWhile((buffer, _) => groupPredicate(buffer));
} }
} }
@@ -1,2 +1,3 @@
{{~ ThemeStyleSheet = include "HtmlDark.Theme.css" ~}} {{~ ThemeStyleSheet = include "HtmlDark.Theme.css" ~}}
{{~ HighlightJsStyleName = "solarized-dark" ~}}
{{~ include "HtmlShared.Main.html" ~}} {{~ include "HtmlShared.Main.html" ~}}
@@ -14,12 +14,12 @@ a {
} }
.pre { .pre {
background-color: #2f3136; background-color: #2f3136 !important;
} }
.pre--multiline { .pre--multiline {
border-color: #282b30; border-color: #282b30 !important;
color: #839496; color: #839496 !important;
} }
.mention { .mention {
@@ -1,2 +1,3 @@
{{~ ThemeStyleSheet = include "HtmlLight.Theme.css" ~}} {{~ ThemeStyleSheet = include "HtmlLight.Theme.css" ~}}
{{~ HighlightJsStyleName = "solarized-light" ~}}
{{~ include "HtmlShared.Main.html" ~}} {{~ include "HtmlShared.Main.html" ~}}
@@ -14,12 +14,12 @@ a {
} }
.pre { .pre {
background-color: #f9f9f9; background-color: #f9f9f9 !important;
} }
.pre--multiline { .pre--multiline {
border-color: #f3f3f3; border-color: #f3f3f3 !important;
color: #657b83; color: #657b83 !important;
} }
.mention { .mention {
@@ -110,6 +110,7 @@ img {
.chatlog { .chatlog {
max-width: 100%; max-width: 100%;
margin-bottom: 24px;
} }
.chatlog__message-group { .chatlog__message-group {
@@ -2,15 +2,29 @@
<html lang="en"> <html lang="en">
<head> <head>
{{~ # Metadata ~}}
<title>{{ Model.Guild.Name | html.escape }} - {{ Model.Channel.Name | html.escape }}</title> <title>{{ Model.Guild.Name | html.escape }} - {{ Model.Channel.Name | html.escape }}</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
{{~ # Styles ~}}
<style> <style>
{{ include "HtmlShared.Main.css" }} {{ include "HtmlShared.Main.css" }}
</style> </style>
<style> <style>
{{ ThemeStyleSheet }} {{ ThemeStyleSheet }}
</style> </style>
{{~ # Syntax highlighting ~}}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/{{HighlightJsStyleName}}.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.pre--multiline').forEach((block) => {
hljs.highlightBlock(block);
});
});
</script>
</head> </head>
<body> <body>
@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -23,7 +22,7 @@ namespace DiscordChatExporter.Core.Services
{ {
// Create retry policy // Create retry policy
var retry = Retry.Create() var retry = Retry.Create()
.Catch<HttpErrorStatusCodeException>(false, e => e.StatusCode == HttpStatusCode.ServiceUnavailable) .Catch<HttpErrorStatusCodeException>(false, e => (int) e.StatusCode >= 500)
.Catch<HttpErrorStatusCodeException>(false, e => (int) e.StatusCode == 429) .Catch<HttpErrorStatusCodeException>(false, e => (int) e.StatusCode == 429)
.WithMaxTryCount(10) .WithMaxTryCount(10)
.WithDelay(TimeSpan.FromSeconds(0.4)); .WithDelay(TimeSpan.FromSeconds(0.4));
@@ -18,62 +18,30 @@ namespace DiscordChatExporter.Core.Services
private readonly ExportFormat _format; private readonly ExportFormat _format;
private readonly ChatLog _log; private readonly ChatLog _log;
private readonly string _dateFormat; private readonly string _dateFormat;
private readonly int _messageGroupLimit;
public TemplateModel(ExportFormat format, ChatLog log, string dateFormat, int messageGroupLimit) public TemplateModel(ExportFormat format, ChatLog log, string dateFormat)
{ {
_format = format; _format = format;
_log = log; _log = log;
_dateFormat = dateFormat; _dateFormat = dateFormat;
_messageGroupLimit = messageGroupLimit;
} }
private IEnumerable<MessageGroup> GroupMessages(IEnumerable<Message> messages) private IEnumerable<MessageGroup> GroupMessages(IEnumerable<Message> messages)
{ => messages.GroupAdjacentWhile((buffer, message) =>
// Group adjacent messages by timestamp and author
var buffer = new List<Message>();
foreach (var message in messages)
{ {
// Get first message of the group // Break group if the author changed
var groupFirstMessage = buffer.FirstOrDefault(); if (buffer.Last().Author.Id != message.Author.Id)
return false;
// Group break condition // Break group if last message was more than 7 minutes ago
var breakCondition = if ((message.Timestamp - buffer.Last().Timestamp).TotalMinutes > 7)
groupFirstMessage != null && return false;
(
message.Author.Id != groupFirstMessage.Author.Id || // when author changes
(message.Timestamp - groupFirstMessage.Timestamp).TotalHours > 1 || // when difference in timestamps is an hour or more
message.Timestamp.Hour != groupFirstMessage.Timestamp.Hour || // when the timestamp's hour changes
buffer.Count >= _messageGroupLimit // when group is full
);
// If condition is true - flush buffer return true;
if (breakCondition) }).Select(g => new MessageGroup(g.First().Author, g.First().Timestamp, g));
{
var group = new MessageGroup(groupFirstMessage.Author, groupFirstMessage.Timestamp, buffer);
// Reset the buffer instead of clearing to avoid mutations on existing references private string Format(IFormattable obj, string format)
buffer = new List<Message>(); => obj.ToString(format, CultureInfo.InvariantCulture);
yield return group;
}
// Add message to buffer
buffer.Add(message);
}
// Add what's remaining in buffer
if (buffer.Any())
{
var groupFirstMessage = buffer.First();
var group = new MessageGroup(groupFirstMessage.Author, groupFirstMessage.Timestamp, buffer);
yield return group;
}
}
private string Format(IFormattable obj, string format) =>
obj.ToString(format, CultureInfo.InvariantCulture);
private string FormatDate(DateTime dateTime) => Format(dateTime, _dateFormat); private string FormatDate(DateTime dateTime) => Format(dateTime, _dateFormat);
@@ -1,5 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using DiscordChatExporter.Core.Internal;
using DiscordChatExporter.Core.Models; using DiscordChatExporter.Core.Models;
using Scriban; using Scriban;
using Scriban.Runtime; using Scriban.Runtime;
@@ -36,8 +38,7 @@ namespace DiscordChatExporter.Core.Services
}; };
// Create template model // Create template model
var templateModel = new TemplateModel(format, chatLog, var templateModel = new TemplateModel(format, chatLog, _settingsService.DateFormat);
_settingsService.DateFormat, _settingsService.MessageGroupLimit);
context.PushGlobal(templateModel.GetScriptObject()); context.PushGlobal(templateModel.GetScriptObject());
@@ -83,34 +84,6 @@ namespace DiscordChatExporter.Core.Services
} }
} }
private IReadOnlyList<ChatLog> SplitIntoPartitions(ChatLog chatLog, int partitionLimit)
{
var result = new List<ChatLog>();
// Loop through all messages with an increment of partition limit
for (var i = 0; i < chatLog.Messages.Count; i += partitionLimit)
{
// Calculate how many messages left in total
var remainingMessageCount = chatLog.Messages.Count - i;
// Decide how many messages are going into this partition
// Each partition will have the same number of messages except the last one that might have fewer (all remaining messages)
var partitionMessageCount = partitionLimit.ClampMax(remainingMessageCount);
// Get messages that belong to this partition
var partitionMessages = new List<Message>();
for (var j = i; j < i + partitionMessageCount; j++)
partitionMessages.Add(chatLog.Messages[j]);
// Create a partition and add to list
var partition = new ChatLog(chatLog.Guild, chatLog.Channel, chatLog.From, chatLog.To, partitionMessages,
chatLog.Mentionables);
result.Add(partition);
}
return result;
}
public void ExportChatLog(ChatLog chatLog, string filePath, ExportFormat format, public void ExportChatLog(ChatLog chatLog, string filePath, ExportFormat format,
int? partitionLimit = null) int? partitionLimit = null)
{ {
@@ -122,7 +95,11 @@ namespace DiscordChatExporter.Core.Services
// Otherwise split into partitions and export separately // Otherwise split into partitions and export separately
else else
{ {
var partitions = SplitIntoPartitions(chatLog, partitionLimit.Value); // Create partitions by grouping up to X adjacent messages into separate chat logs
var partitions = chatLog.Messages.GroupAdjacentWhile(g => g.Count < partitionLimit.Value)
.Select(g => new ChatLog(chatLog.Guild, chatLog.Channel, chatLog.From, chatLog.To, g, chatLog.Mentionables))
.ToArray();
ExportChatLogPartitioned(partitions, filePath, format); ExportChatLogPartitioned(partitions, filePath, format);
} }
} }
@@ -8,7 +8,6 @@ namespace DiscordChatExporter.Core.Services
public bool IsAutoUpdateEnabled { get; set; } = true; public bool IsAutoUpdateEnabled { get; set; } = true;
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt"; public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
public int MessageGroupLimit { get; set; } = 20;
public AuthToken LastToken { get; set; } public AuthToken LastToken { get; set; }
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark; public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
@@ -3,5 +3,5 @@
[assembly: AssemblyTitle("DiscordChatExporter")] [assembly: AssemblyTitle("DiscordChatExporter")]
[assembly: AssemblyCompany("Tyrrrz")] [assembly: AssemblyCompany("Tyrrrz")]
[assembly: AssemblyCopyright("Copyright (c) Alexey Golub")] [assembly: AssemblyCopyright("Copyright (c) Alexey Golub")]
[assembly: AssemblyVersion("2.10.1")] [assembly: AssemblyVersion("2.11")]
[assembly: AssemblyFileVersion("2.10.1")] [assembly: AssemblyFileVersion("2.11")]
@@ -1,6 +1,5 @@
using DiscordChatExporter.Core.Services; using DiscordChatExporter.Core.Services;
using DiscordChatExporter.Gui.ViewModels.Framework; using DiscordChatExporter.Gui.ViewModels.Framework;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Gui.ViewModels.Dialogs namespace DiscordChatExporter.Gui.ViewModels.Dialogs
{ {
@@ -20,12 +19,6 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
set => _settingsService.DateFormat = value; set => _settingsService.DateFormat = value;
} }
public int MessageGroupLimit
{
get => _settingsService.MessageGroupLimit;
set => _settingsService.MessageGroupLimit = value.ClampMin(0);
}
public SettingsViewModel(SettingsService settingsService) public SettingsViewModel(SettingsService settingsService)
{ {
_settingsService = settingsService; _settingsService = settingsService;
@@ -25,13 +25,6 @@
materialDesign:HintAssist.IsFloating="True" materialDesign:HintAssist.IsFloating="True"
Text="{Binding DateFormat}" /> Text="{Binding DateFormat}" />
<!-- Message group limit -->
<TextBox
Margin="16,8"
materialDesign:HintAssist.Hint="Message group limit"
materialDesign:HintAssist.IsFloating="True"
Text="{Binding MessageGroupLimit}" />
<!-- Auto-updates --> <!-- Auto-updates -->
<DockPanel LastChildFill="False"> <DockPanel LastChildFill="False">
<TextBlock <TextBlock