mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-07 22:54:38 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fa61bf75a | |||
| 1976e3ad08 | |||
| adbe544c57 | |||
| 6f4d1c3d77 | |||
| 33f71c1612 | |||
| 9b78472e05 | |||
| 1f6d1f21e6 | |||
| 918cb4820f | |||
| 32af1aa05d | |||
| dda32d8af2 | |||
| d1d82f975b |
@@ -1,3 +1,15 @@
|
||||
### 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)
|
||||
|
||||
- [HTML] Fixed an issue where multiple emojis on a single line would get rendered as one emoji.
|
||||
- [HTML] Fixed an issue where an open square bracket character would incorrectly render as an emoji.
|
||||
- [HTML] Added alt text to emoji images so that they will be rendered using default font when Twemoji images are not available.
|
||||
|
||||
### v2.10 (03-Mar-2019)
|
||||
|
||||
- [HTML] Reworked the entire markdown parsing engine which fixes numerous rendering inconsistencies (over 15 issues fixed).
|
||||
|
||||
@@ -2,10 +2,12 @@ New-Item "$PSScriptRoot\bin" -ItemType Directory -Force
|
||||
|
||||
# GUI
|
||||
$files = @()
|
||||
$files += Get-Item -Path "$PSScriptRoot\..\License.txt"
|
||||
$files += Get-ChildItem -Path "$PSScriptRoot\..\DiscordChatExporter.Gui\bin\Release\*" -Include "*.exe", "*.dll", "*.config"
|
||||
$files | Compress-Archive -DestinationPath "$PSScriptRoot\bin\DiscordChatExporter.zip" -Force
|
||||
|
||||
# CLI
|
||||
$files = @()
|
||||
$files += Get-Item -Path "$PSScriptRoot\..\License.txt"
|
||||
$files += Get-ChildItem -Path "$PSScriptRoot\..\DiscordChatExporter.Cli\bin\Release\net461\*" -Include "*.exe", "*.dll", "*.config"
|
||||
$files | Compress-Archive -DestinationPath "$PSScriptRoot\bin\DiscordChatExporter.CLI.zip" -Force
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
<Version>2.10</Version>
|
||||
<Version>2.10.2</Version>
|
||||
<Company>Tyrrrz</Company>
|
||||
<Copyright>Copyright (c) Alexey Golub</Copyright>
|
||||
<ApplicationIcon>..\favicon.ico</ApplicationIcon>
|
||||
|
||||
@@ -9,19 +9,31 @@ namespace DiscordChatExporter.Cli.Internal
|
||||
|
||||
public InlineProgress()
|
||||
{
|
||||
_posX = Console.CursorLeft;
|
||||
_posY = Console.CursorTop;
|
||||
// If output is not redirected - save initial cursor position
|
||||
if (!Console.IsOutputRedirected)
|
||||
{
|
||||
_posX = Console.CursorLeft;
|
||||
_posY = Console.CursorTop;
|
||||
}
|
||||
}
|
||||
|
||||
public void Report(double progress)
|
||||
{
|
||||
Console.SetCursorPosition(_posX, _posY);
|
||||
Console.WriteLine($"{progress:P1}");
|
||||
// If output is not redirected - reset cursor position and write progress
|
||||
if (!Console.IsOutputRedirected)
|
||||
{
|
||||
Console.SetCursorPosition(_posX, _posY);
|
||||
Console.WriteLine($"{progress:P1}");
|
||||
}
|
||||
}
|
||||
|
||||
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 ✓");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ namespace DiscordChatExporter.Cli.Verbs
|
||||
// Configure settings
|
||||
if (Options.DateFormat.IsNotBlank())
|
||||
settingsService.DateFormat = Options.DateFormat;
|
||||
if (Options.MessageGroupLimit > 0)
|
||||
settingsService.MessageGroupLimit = Options.MessageGroupLimit;
|
||||
|
||||
// Track progress
|
||||
Console.Write($"Exporting channel [{Options.ChannelId}]... ");
|
||||
|
||||
@@ -29,8 +29,6 @@ namespace DiscordChatExporter.Cli.Verbs
|
||||
// Configure settings
|
||||
if (Options.DateFormat.IsNotBlank())
|
||||
settingsService.DateFormat = Options.DateFormat;
|
||||
if (Options.MessageGroupLimit > 0)
|
||||
settingsService.MessageGroupLimit = Options.MessageGroupLimit;
|
||||
|
||||
// Get channels
|
||||
var channels = await dataService.GetDirectMessageChannelsAsync(Options.GetToken());
|
||||
|
||||
@@ -30,8 +30,6 @@ namespace DiscordChatExporter.Cli.Verbs
|
||||
// Configure settings
|
||||
if (Options.DateFormat.IsNotBlank())
|
||||
settingsService.DateFormat = Options.DateFormat;
|
||||
if (Options.MessageGroupLimit > 0)
|
||||
settingsService.MessageGroupLimit = Options.MessageGroupLimit;
|
||||
|
||||
// Get channels
|
||||
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.")]
|
||||
public string DateFormat { get; set; }
|
||||
|
||||
[Option("grouplimit", Default = 0, HelpText = "Message group limit.")]
|
||||
public int MessageGroupLimit { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ namespace DiscordChatExporter.Core.Markdown.Internal
|
||||
|
||||
// The first word is a language identifier if it's the only word followed by a newline, the rest is code
|
||||
private static readonly Parser<Node> MultilineCodeBlockNode =
|
||||
Parse.RegexMatch(new Regex("```(?:(\\w*?)?(?:\\s*?\\n))?(.+)```", RegexOptions.Singleline))
|
||||
Parse.RegexMatch(new Regex("```(?:(\\w*?)?(?:\\s*?\\n))?(.+?)```", RegexOptions.Singleline))
|
||||
.Select(m => new MultilineCodeBlockNode(m.Value, m.Groups[1].Value, m.Groups[2].Value));
|
||||
|
||||
// Combinator, order matters
|
||||
@@ -108,13 +108,13 @@ namespace DiscordChatExporter.Core.Markdown.Internal
|
||||
"[\\ud800-\\udbff][\\udc00-\\udfff]|" +
|
||||
"[\\u0023-\\u0039]\\u20e3|" +
|
||||
"\\u3299|\\u3297|\\u303d|\\u3030|\\u24c2|\\ud83c[\\udd70-\\udd71]|\\ud83c[\\udd7e-\\udd7f]|\\ud83c\\udd8e|\\ud83c[\\udd91-\\udd9a]|\\ud83c[\\udde6-\\uddff]|" +
|
||||
"[\\ud83c[\\ude01-\\ude02]|\\ud83c\\ude1a|\\ud83c\\ude2f|[\\ud83c[\\ude32-\\ude3a]|[\\ud83c[\\ude50-\\ude51]|\\u203c|\\u2049|[\\u25aa-\\u25ab]|" +
|
||||
"\\ud83c[\\ude01-\\ude02]|\\ud83c\\ude1a|\\ud83c\\ude2f|\\ud83c[\\ude32-\\ude3a]|\\ud83c[\\ude50-\\ude51]|\\u203c|\\u2049|[\\u25aa-\\u25ab]|" +
|
||||
"\\u25b6|\\u25c0|[\\u25fb-\\u25fe]|\\u00a9|\\u00ae|\\u2122|\\u2139|\\ud83c\\udc04|[\\u2600-\\u26FF]|\\u2b05|\\u2b06|\\u2b07|\\u2b1b|\\u2b1c|\\u2b50|" +
|
||||
"\\u2b55|\\u231a|\\u231b|\\u2328|\\u23cf|[\\u23e9-\\u23f3]|[\\u23f8-\\u23fa]|\\ud83c\\udccf|\\u2934|\\u2935|[\\u2190-\\u21ff])")
|
||||
.Select(m => new EmojiNode(m.Value, m.Groups[1].Value));
|
||||
|
||||
// <:lul:123456> or <a:lul:123456>
|
||||
private static readonly Parser<Node> CustomEmojiNode = Parse.RegexMatch("<(a)?:(.+):(\\d+)>")
|
||||
private static readonly Parser<Node> CustomEmojiNode = Parse.RegexMatch("<(a)?:(.+?):(\\d+)>")
|
||||
.Select(m => new EmojiNode(m.Value, m.Groups[3].Value, m.Groups[2].Value, m.Groups[1].Value.IsNotBlank()));
|
||||
|
||||
// Combinator, order matters
|
||||
@@ -123,7 +123,7 @@ namespace DiscordChatExporter.Core.Markdown.Internal
|
||||
/* Links */
|
||||
|
||||
// [title](link)
|
||||
private static readonly Parser<Node> TitledLinkNode = Parse.RegexMatch("\\[(.+)\\]\\((.+)\\)")
|
||||
private static readonly Parser<Node> TitledLinkNode = Parse.RegexMatch("\\[(.+?)\\]\\((.+?)\\)")
|
||||
.Select(m => new LinkNode(m.Value, m.Groups[2].Value, m.Groups[1].Value));
|
||||
|
||||
// Starts with http:// or https://, stops at the last non-whitespace character followed by whitespace or punctuation character
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
||||
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 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));
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,7 @@ img {
|
||||
|
||||
.chatlog {
|
||||
max-width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chatlog__message-group {
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
<div class="chatlog__reactions">
|
||||
{{~ for reaction in message.Reactions ~}}
|
||||
<div class="chatlog__reaction">
|
||||
<img class="emoji emoji--small" title="{{ reaction.Emoji.Name }}" src="{{ reaction.Emoji.ImageUrl }}" />
|
||||
<img class="emoji emoji--small" alt="{{ reaction.Emoji.Name }}" title="{{ reaction.Emoji.Name }}" src="{{ reaction.Emoji.ImageUrl }}" />
|
||||
<span class="chatlog__reaction-count">{{ reaction.Count }}</span>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
@@ -18,62 +18,30 @@ namespace DiscordChatExporter.Core.Services
|
||||
private readonly ExportFormat _format;
|
||||
private readonly ChatLog _log;
|
||||
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;
|
||||
_log = log;
|
||||
_dateFormat = dateFormat;
|
||||
_messageGroupLimit = messageGroupLimit;
|
||||
}
|
||||
|
||||
private IEnumerable<MessageGroup> GroupMessages(IEnumerable<Message> messages)
|
||||
{
|
||||
// Group adjacent messages by timestamp and author
|
||||
var buffer = new List<Message>();
|
||||
foreach (var message in messages)
|
||||
=> messages.GroupAdjacentWhile((buffer, message) =>
|
||||
{
|
||||
// Get first message of the group
|
||||
var groupFirstMessage = buffer.FirstOrDefault();
|
||||
// Break group if the author changed
|
||||
if (buffer.Last().Author.Id != message.Author.Id)
|
||||
return false;
|
||||
|
||||
// Group break condition
|
||||
var breakCondition =
|
||||
groupFirstMessage != null &&
|
||||
(
|
||||
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
|
||||
);
|
||||
// Break group if last message was more than 7 minutes ago
|
||||
if ((message.Timestamp - buffer.Last().Timestamp).TotalMinutes > 7)
|
||||
return false;
|
||||
|
||||
// If condition is true - flush buffer
|
||||
if (breakCondition)
|
||||
{
|
||||
var group = new MessageGroup(groupFirstMessage.Author, groupFirstMessage.Timestamp, buffer);
|
||||
return true;
|
||||
}).Select(g => new MessageGroup(g.First().Author, g.First().Timestamp, g));
|
||||
|
||||
// Reset the buffer instead of clearing to avoid mutations on existing references
|
||||
buffer = new List<Message>();
|
||||
|
||||
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 Format(IFormattable obj, string format)
|
||||
=> obj.ToString(format, CultureInfo.InvariantCulture);
|
||||
|
||||
private string FormatDate(DateTime dateTime) => Format(dateTime, _dateFormat);
|
||||
|
||||
@@ -210,7 +178,7 @@ namespace DiscordChatExporter.Core.Services
|
||||
? "emoji--large"
|
||||
: null;
|
||||
|
||||
buffer.Append($"<img class=\"emoji {jumboableCssClass}\" title=\"{emojiNode.Name}\" src=\"{emojiImageUrl}\" />");
|
||||
buffer.Append($"<img class=\"emoji {jumboableCssClass}\" alt=\"{emojiNode.Name}\" title=\"{emojiNode.Name}\" src=\"{emojiImageUrl}\" />");
|
||||
}
|
||||
|
||||
else if (node is LinkNode linkNode)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DiscordChatExporter.Core.Internal;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Scriban;
|
||||
using Scriban.Runtime;
|
||||
@@ -36,8 +38,7 @@ namespace DiscordChatExporter.Core.Services
|
||||
};
|
||||
|
||||
// Create template model
|
||||
var templateModel = new TemplateModel(format, chatLog,
|
||||
_settingsService.DateFormat, _settingsService.MessageGroupLimit);
|
||||
var templateModel = new TemplateModel(format, chatLog, _settingsService.DateFormat);
|
||||
|
||||
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,
|
||||
int? partitionLimit = null)
|
||||
{
|
||||
@@ -122,7 +95,11 @@ namespace DiscordChatExporter.Core.Services
|
||||
// Otherwise split into partitions and export separately
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace DiscordChatExporter.Core.Services
|
||||
public bool IsAutoUpdateEnabled { get; set; } = true;
|
||||
|
||||
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
|
||||
public int MessageGroupLimit { get; set; } = 20;
|
||||
|
||||
public AuthToken LastToken { get; set; }
|
||||
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
[assembly: AssemblyTitle("DiscordChatExporter")]
|
||||
[assembly: AssemblyCompany("Tyrrrz")]
|
||||
[assembly: AssemblyCopyright("Copyright (c) Alexey Golub")]
|
||||
[assembly: AssemblyVersion("2.10")]
|
||||
[assembly: AssemblyFileVersion("2.10")]
|
||||
[assembly: AssemblyVersion("2.10.2")]
|
||||
[assembly: AssemblyFileVersion("2.10.2")]
|
||||
@@ -1,6 +1,5 @@
|
||||
using DiscordChatExporter.Core.Services;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||
{
|
||||
@@ -20,12 +19,6 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||
set => _settingsService.DateFormat = value;
|
||||
}
|
||||
|
||||
public int MessageGroupLimit
|
||||
{
|
||||
get => _settingsService.MessageGroupLimit;
|
||||
set => _settingsService.MessageGroupLimit = value.ClampMin(0);
|
||||
}
|
||||
|
||||
public SettingsViewModel(SettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
|
||||
@@ -25,13 +25,6 @@
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
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 -->
|
||||
<DockPanel LastChildFill="False">
|
||||
<TextBlock
|
||||
|
||||
Reference in New Issue
Block a user