mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-07 22:54:38 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b518fb2f59 | |||
| 94165dcd8d | |||
| d6507598fa | |||
| a0359b1e43 | |||
| aa53cecd4e | |||
| 4144911208 | |||
| d28e81f8dc | |||
| 761cb032d7 | |||
| 47f0561c71 | |||
| 23116b776b | |||
| ba27a9f909 | |||
| 52684c264f | |||
| bcad5b4ed1 | |||
| 95a4217ab3 | |||
| 4e8fb80ac5 | |||
| 7ae560dde0 |
+11
-2
@@ -1,9 +1,18 @@
|
||||
### v2.8 (03-Nov-2018)
|
||||
|
||||
- Added support for partitioning which lets you split the output into multiple files by setting the partition message limit (`-p` parameter for CLI).
|
||||
- Exported file will no longer open automatically on completion.
|
||||
- Reduced amount of memory used during exporting.
|
||||
- Disabled message grouping in PlainText export.
|
||||
- Improved encoding of newlines in CSV export.
|
||||
- Improved error messages in the GUI app during crashes.
|
||||
|
||||
### v2.7 (14-Sep-2018)
|
||||
|
||||
- Updated instructions on how to obtain the user token.
|
||||
- Expanded CLI with new commands: `channels` (get a list of channels in a guild), `dms` (get a list of DM channels), `guilds` (get a list of guilds), on top of `export` (export chatlog).
|
||||
- Expanded CLI with new commands: `channels` (get a list of channels in a guild), `dm` (get a list of DM channels), `guilds` (get a list of guilds), on top of `export` (export chatlog).
|
||||
- Improved help text and error messages in CLI.
|
||||
- In CLI, if the provided output file path is a directory, a file name will be generated and appended to it automatically.
|
||||
- In CLI, the file name will be automatically generated if the provided output file path is a directory.
|
||||
|
||||
### v2.6 (25-Jul-2018)
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace DiscordChatExporter.Cli
|
||||
// Services
|
||||
SimpleIoc.Default.Register<IDataService, DataService>();
|
||||
SimpleIoc.Default.Register<IExportService, ExportService>();
|
||||
SimpleIoc.Default.Register<IMessageGroupService, MessageGroupService>();
|
||||
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
||||
SimpleIoc.Default.Register<IUpdateService, UpdateService>();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
<Version>2.7</Version>
|
||||
<Version>2.8</Version>
|
||||
<Company>Tyrrrz</Company>
|
||||
<Copyright>Copyright (c) 2017-2018 Alexey Golub</Copyright>
|
||||
<ApplicationIcon>..\favicon.ico</ApplicationIcon>
|
||||
|
||||
@@ -43,8 +43,7 @@ namespace DiscordChatExporter.Cli
|
||||
typeof(ExportChatOptions),
|
||||
typeof(GetChannelsOptions),
|
||||
typeof(GetDirectMessageChannelsOptions),
|
||||
typeof(GetGuildsOptions),
|
||||
//typeof(UpdateAppOptions)
|
||||
typeof(GetGuildsOptions)
|
||||
};
|
||||
|
||||
// Parse command line arguments
|
||||
@@ -55,7 +54,6 @@ namespace DiscordChatExporter.Cli
|
||||
parsedArgs.WithParsed<GetChannelsOptions>(o => new GetChannelsVerb(o).Execute());
|
||||
parsedArgs.WithParsed<GetDirectMessageChannelsOptions>(o => new GetDirectMessageChannelsVerb(o).Execute());
|
||||
parsedArgs.WithParsed<GetGuildsOptions>(o => new GetGuildsVerb(o).Execute());
|
||||
parsedArgs.WithParsed<UpdateAppOptions>(o => new UpdateAppVerb(o).Execute());
|
||||
|
||||
// Show token help if help requested or no verb specified
|
||||
parsedArgs.WithNotParsed(errs =>
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace DiscordChatExporter.Cli.Verbs
|
||||
var container = new Container();
|
||||
var settingsService = container.Resolve<ISettingsService>();
|
||||
var dataService = container.Resolve<IDataService>();
|
||||
var messageGroupService = container.Resolve<IMessageGroupService>();
|
||||
var exportService = container.Resolve<IExportService>();
|
||||
|
||||
// Configure settings
|
||||
@@ -30,37 +29,20 @@ namespace DiscordChatExporter.Cli.Verbs
|
||||
if (Options.MessageGroupLimit > 0)
|
||||
settingsService.MessageGroupLimit = Options.MessageGroupLimit;
|
||||
|
||||
// Get channel and guild
|
||||
var channel = await dataService.GetChannelAsync(Options.GetToken(), Options.ChannelId);
|
||||
var guild = channel.GuildId == Guild.DirectMessages.Id
|
||||
? Guild.DirectMessages
|
||||
: await dataService.GetGuildAsync(Options.GetToken(), channel.GuildId);
|
||||
// Get chat log
|
||||
var chatLog = await dataService.GetChatLogAsync(Options.GetToken(), Options.ChannelId,
|
||||
Options.After, Options.Before);
|
||||
|
||||
// Generate file path if not set
|
||||
var filePath = Options.FilePath;
|
||||
if (filePath == null || filePath.EndsWith("/") || filePath.EndsWith("\\"))
|
||||
{
|
||||
filePath += $"{guild.Name} - {channel.Name}.{Options.ExportFormat.GetFileExtension()}"
|
||||
filePath += $"{chatLog.Guild.Name} - {chatLog.Channel.Name}.{Options.ExportFormat.GetFileExtension()}"
|
||||
.Replace(Path.GetInvalidFileNameChars(), '_');
|
||||
}
|
||||
|
||||
// TODO: extract this to make it reusable across implementations
|
||||
// Get messages
|
||||
var messages =
|
||||
await dataService.GetChannelMessagesAsync(Options.GetToken(), channel.Id,
|
||||
Options.After, Options.Before);
|
||||
|
||||
// Group messages
|
||||
var messageGroups = messageGroupService.GroupMessages(messages);
|
||||
|
||||
// Get mentionables
|
||||
var mentionables = await dataService.GetMentionablesAsync(Options.GetToken(), guild.Id, messages);
|
||||
|
||||
// Create log
|
||||
var log = new ChatLog(guild, channel, Options.After, Options.Before, messageGroups, mentionables);
|
||||
|
||||
// Export
|
||||
exportService.Export(Options.ExportFormat, filePath, log);
|
||||
exportService.ExportChatLog(chatLog, filePath, Options.ExportFormat, Options.PartitionLimit);
|
||||
|
||||
// Print result
|
||||
Console.WriteLine($"Exported chat to [{filePath}]");
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace DiscordChatExporter.Cli.Verbs.Options
|
||||
[Option("before", Default = null, HelpText = "Limit to messages sent before this date.")]
|
||||
public DateTime? Before { get; set; }
|
||||
|
||||
[Option('p', "partition", Default = null, HelpText = "Split output into partitions limited to this number of messages.")]
|
||||
public int? PartitionLimit { get; set; }
|
||||
|
||||
[Option("dateformat", Default = null, HelpText = "Date format used in output.")]
|
||||
public string DateFormat { get; set; }
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace DiscordChatExporter.Cli.Verbs.Options
|
||||
{
|
||||
[Verb("update", HelpText = "Updates this application to the latest version.")]
|
||||
public class UpdateAppOptions
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Cli.Verbs.Options;
|
||||
using DiscordChatExporter.Core.Services;
|
||||
|
||||
namespace DiscordChatExporter.Cli.Verbs
|
||||
{
|
||||
public class UpdateAppVerb : Verb<UpdateAppOptions>
|
||||
{
|
||||
public UpdateAppVerb(UpdateAppOptions options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync()
|
||||
{
|
||||
// Get update service
|
||||
var container = new Container();
|
||||
var updateService = container.Resolve<IUpdateService>();
|
||||
|
||||
// TODO: this is configured only for GUI
|
||||
// Get update version
|
||||
var updateVersion = await updateService.CheckPrepareUpdateAsync();
|
||||
|
||||
if (updateVersion != null)
|
||||
{
|
||||
Console.WriteLine($"Updating to version {updateVersion}");
|
||||
|
||||
updateService.NeedRestart = false;
|
||||
updateService.FinalizeUpdate();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("There are no application updates available.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
<Version>2.7</Version>
|
||||
<Version>2.8</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
@@ -14,20 +13,18 @@ namespace DiscordChatExporter.Core.Models
|
||||
|
||||
public DateTime? To { get; }
|
||||
|
||||
public IReadOnlyList<MessageGroup> MessageGroups { get; }
|
||||
|
||||
public int TotalMessageCount => MessageGroups.Sum(g => g.Messages.Count);
|
||||
public IReadOnlyList<Message> Messages { get; }
|
||||
|
||||
public Mentionables Mentionables { get; }
|
||||
|
||||
public ChatLog(Guild guild, Channel channel, DateTime? from, DateTime? to,
|
||||
IReadOnlyList<MessageGroup> messageGroups, Mentionables mentionables)
|
||||
IReadOnlyList<Message> messages, Mentionables mentionables)
|
||||
{
|
||||
Guild = guild;
|
||||
Channel = channel;
|
||||
From = from;
|
||||
To = to;
|
||||
MessageGroups = messageGroups;
|
||||
Messages = messages;
|
||||
Mentionables = mentionables;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
@@ -31,5 +33,45 @@ namespace DiscordChatExporter.Core.Models
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(format));
|
||||
}
|
||||
|
||||
public static IReadOnlyList<ChatLog> SplitIntoPartitions(this ChatLog chatLog, int partitionLimit)
|
||||
{
|
||||
// If chat log has fewer messages than the limit - just return chat log in a list
|
||||
if (chatLog.Messages.Count <= partitionLimit)
|
||||
return new[] {chatLog};
|
||||
|
||||
var result = new List<ChatLog>();
|
||||
|
||||
// Loop through messages
|
||||
var buffer = new List<Message>();
|
||||
foreach (var message in chatLog.Messages)
|
||||
{
|
||||
// Add message to buffer
|
||||
buffer.Add(message);
|
||||
|
||||
// If reached the limit - split and reset buffer
|
||||
if (buffer.Count >= partitionLimit)
|
||||
{
|
||||
// Add to result
|
||||
var chatLogPartition = new ChatLog(chatLog.Guild, chatLog.Channel, chatLog.From, chatLog.To, buffer,
|
||||
chatLog.Mentionables);
|
||||
result.Add(chatLogPartition);
|
||||
|
||||
// Reset the buffer instead of clearing to avoid mutations on existing references
|
||||
buffer = new List<Message>();
|
||||
}
|
||||
}
|
||||
|
||||
// Add what's remaining in buffer
|
||||
if (buffer.Any())
|
||||
{
|
||||
// Add to result
|
||||
var chatLogPartition = new ChatLog(chatLog.Guild, chatLog.Channel, chatLog.From, chatLog.To, buffer,
|
||||
chatLog.Mentionables);
|
||||
result.Add(chatLogPartition);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class MessageGroup
|
||||
{
|
||||
public User Author { get; }
|
||||
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public IReadOnlyList<Message> Messages { get; }
|
||||
|
||||
public MessageGroup(User author, DateTime timestamp, IReadOnlyList<Message> messages)
|
||||
{
|
||||
Author = author;
|
||||
Timestamp = timestamp;
|
||||
Messages = messages;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Author.FullName} | {Timestamp} | {Messages.Count} messages";
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
Author;Date;Content;Attachments;
|
||||
{{~ for group in Model.MessageGroups -}}
|
||||
{{- for message in group.Messages -}}
|
||||
{{- message.Author.FullName }};
|
||||
{{~ for message in Model.Messages -}}
|
||||
{{- }}"{{ message.Author.FullName }}";
|
||||
|
||||
{{- message.Timestamp | FormatDate }};
|
||||
{{- }}"{{ message.Timestamp | FormatDate }}";
|
||||
|
||||
{{- message.Content | FormatContent }};
|
||||
{{- }}"{{ message.Content | FormatContent }}";
|
||||
|
||||
{{- message.Attachments | array.map "Url" | array.join "," }};
|
||||
{{~ end -}}
|
||||
{{- end -}}
|
||||
{{- }}"{{ message.Attachments | array.map "Url" | array.join "," }}";
|
||||
{{~ end -}}
|
||||
|
Can't render this file because it contains an unexpected character in line 10 and column 41.
|
@@ -24,7 +24,7 @@
|
||||
<div class="info__channel-topic">{{ Model.Channel.Topic | html.escape }}</div>
|
||||
{{~ end ~}}
|
||||
|
||||
<div class="info__channel-message-count">{{ Model.TotalMessageCount | Format "N0" }} messages</div>
|
||||
<div class="info__channel-message-count">{{ Model.Messages | array.size | Format "N0" }} messages</div>
|
||||
|
||||
{{~ if Model.From || Model.To ~}}
|
||||
<div class="info__channel-date-range">
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
{{~ # Log ~}}
|
||||
<div class="chatlog">
|
||||
{{~ for group in Model.MessageGroups ~}}
|
||||
{{~ for group in Model.Messages | GroupMessages ~}}
|
||||
<div class="chatlog__message-group">
|
||||
{{~ # Avatar ~}}
|
||||
<div class="chatlog__author-avatar-container">
|
||||
|
||||
@@ -3,23 +3,19 @@
|
||||
Guild: {{ Model.Guild.Name }}
|
||||
Channel: {{ Model.Channel.Name }}
|
||||
Topic: {{ Model.Channel.Topic }}
|
||||
Messages: {{ Model.TotalMessageCount | Format "N0" }}
|
||||
Messages: {{ Model.Messages | array.size | Format "N0" }}
|
||||
Range: {{ if Model.From }}{{ Model.From | FormatDate }} {{ end }}{{ if Model.From || Model.To }}->{{ end }}{{ if Model.To }} {{ Model.To | FormatDate }}{{ end }}
|
||||
==============================================================
|
||||
|
||||
{{~ # Log ~}}
|
||||
{{~ for group in Model.MessageGroups ~}}
|
||||
{{~ for message in Model.Messages ~}}
|
||||
{{~ # Author name and timestamp ~}}
|
||||
{{~ }}[{{ group.Timestamp | FormatDate }}] {{ group.Author.FullName }}
|
||||
{{~ # Messages ~}}
|
||||
{{~ for message in group.Messages ~}}
|
||||
{{~ # Content ~}}
|
||||
{{~ message.Content | FormatContent }}
|
||||
{{~ # Attachments ~}}
|
||||
{{~ for attachment in message.Attachments ~}}
|
||||
{{~ attachment.Url }}
|
||||
{{~ end ~}}
|
||||
{{~ }}[{{ message.Timestamp | FormatDate }}] {{ message.Author.FullName }}
|
||||
{{~ # Content ~}}
|
||||
{{~ message.Content | FormatContent }}
|
||||
{{~ # Attachments ~}}
|
||||
{{~ for attachment in message.Attachments ~}}
|
||||
{{~ attachment.Url }}
|
||||
{{~ end ~}}
|
||||
|
||||
|
||||
{{~ end ~}}
|
||||
@@ -65,6 +65,10 @@ namespace DiscordChatExporter.Core.Services
|
||||
|
||||
public async Task<Guild> GetGuildAsync(AuthToken token, string guildId)
|
||||
{
|
||||
// Special case for direct messages pseudo-guild
|
||||
if (guildId == Guild.DirectMessages.Id)
|
||||
return Guild.DirectMessages;
|
||||
|
||||
var response = await GetApiResponseAsync(token, "guilds", guildId);
|
||||
var guild = ParseGuild(response);
|
||||
|
||||
@@ -210,6 +214,33 @@ namespace DiscordChatExporter.Core.Services
|
||||
return new Mentionables(users, channels, roles);
|
||||
}
|
||||
|
||||
public async Task<ChatLog> GetChatLogAsync(AuthToken token, Guild guild, Channel channel,
|
||||
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null)
|
||||
{
|
||||
// Get messages
|
||||
var messages = await GetChannelMessagesAsync(token, channel.Id, from, to, progress);
|
||||
|
||||
// Get mentionables
|
||||
var mentionables = await GetMentionablesAsync(token, guild.Id, messages);
|
||||
|
||||
return new ChatLog(guild, channel, from, to, messages, mentionables);
|
||||
}
|
||||
|
||||
public async Task<ChatLog> GetChatLogAsync(AuthToken token, string channelId,
|
||||
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null)
|
||||
{
|
||||
// Get channel
|
||||
var channel = await GetChannelAsync(token, channelId);
|
||||
|
||||
// Get guild
|
||||
var guild = channel.GuildId == Guild.DirectMessages.Id
|
||||
? Guild.DirectMessages
|
||||
: await GetGuildAsync(token, channel.GuildId);
|
||||
|
||||
// Get the chat log
|
||||
return await GetChatLogAsync(token, guild, channel, from, to, progress);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class ExportService
|
||||
{
|
||||
private class MessageGroup
|
||||
{
|
||||
public User Author { get; }
|
||||
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public IReadOnlyList<Message> Messages { get; }
|
||||
|
||||
public MessageGroup(User author, DateTime timestamp, IReadOnlyList<Message> messages)
|
||||
{
|
||||
Author = author;
|
||||
Timestamp = timestamp;
|
||||
Messages = messages;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -18,12 +19,57 @@ 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)
|
||||
public TemplateModel(ExportFormat format, ChatLog log, string dateFormat, int messageGroupLimit)
|
||||
{
|
||||
_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)
|
||||
{
|
||||
var groupFirst = buffer.FirstOrDefault();
|
||||
|
||||
// Group break condition
|
||||
var breakCondition =
|
||||
groupFirst != null &&
|
||||
(
|
||||
message.Author.Id != groupFirst.Author.Id ||
|
||||
(message.Timestamp - groupFirst.Timestamp).TotalHours > 1 ||
|
||||
message.Timestamp.Hour != groupFirst.Timestamp.Hour ||
|
||||
buffer.Count >= _messageGroupLimit
|
||||
);
|
||||
|
||||
// If condition is true - flush buffer
|
||||
if (breakCondition)
|
||||
{
|
||||
var group = new MessageGroup(groupFirst.Author, groupFirst.Timestamp, buffer);
|
||||
|
||||
// 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 groupFirst = buffer.First();
|
||||
var group = new MessageGroup(groupFirst.Author, groupFirst.Timestamp, buffer);
|
||||
|
||||
yield return group;
|
||||
}
|
||||
}
|
||||
|
||||
private string HtmlEncode(string str) => WebUtility.HtmlEncode(str);
|
||||
@@ -229,9 +275,6 @@ namespace DiscordChatExporter.Core.Services
|
||||
|
||||
private string FormatContentCsv(string content)
|
||||
{
|
||||
// New lines
|
||||
content = content.Replace("\n", ", ");
|
||||
|
||||
// Escape quotes
|
||||
content = content.Replace("\"", "\"\"");
|
||||
|
||||
@@ -310,6 +353,7 @@ namespace DiscordChatExporter.Core.Services
|
||||
scriptObject.SetValue("Model", _log, true);
|
||||
|
||||
// Import functions
|
||||
scriptObject.Import(nameof(GroupMessages), new Func<IEnumerable<Message>, IEnumerable<MessageGroup>>(GroupMessages));
|
||||
scriptObject.Import(nameof(Format), new Func<IFormattable, string, string>(Format));
|
||||
scriptObject.Import(nameof(FormatDate), new Func<DateTime, string>(FormatDate));
|
||||
scriptObject.Import(nameof(FormatFileSize), new Func<long, string>(FormatFileSize));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Scriban;
|
||||
using Scriban.Runtime;
|
||||
@@ -15,7 +16,7 @@ namespace DiscordChatExporter.Core.Services
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public void Export(ExportFormat format, string filePath, ChatLog log)
|
||||
private void ExportChatLogSingle(ChatLog chatLog, string filePath, ExportFormat format)
|
||||
{
|
||||
// Create template loader
|
||||
var loader = new TemplateLoader();
|
||||
@@ -35,7 +36,9 @@ namespace DiscordChatExporter.Core.Services
|
||||
};
|
||||
|
||||
// Create template model
|
||||
var templateModel = new TemplateModel(format, log, _settingsService.DateFormat);
|
||||
var templateModel = new TemplateModel(format, chatLog,
|
||||
_settingsService.DateFormat, _settingsService.MessageGroupLimit);
|
||||
|
||||
context.PushGlobal(templateModel.GetScriptObject());
|
||||
|
||||
// Create directory
|
||||
@@ -49,8 +52,50 @@ namespace DiscordChatExporter.Core.Services
|
||||
// Configure output
|
||||
context.PushOutput(new TextWriterOutput(output));
|
||||
|
||||
// Render template
|
||||
template.Render(context);
|
||||
// Render output
|
||||
context.Evaluate(template.Page);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExportChatLogPartitions(IReadOnlyList<ChatLog> partitions, string filePath, ExportFormat format)
|
||||
{
|
||||
// Split file path into components
|
||||
var dirPath = Path.GetDirectoryName(filePath);
|
||||
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
|
||||
var fileExt = Path.GetExtension(filePath);
|
||||
|
||||
// Export each partition separately
|
||||
var partitionNumber = 1;
|
||||
foreach (var partition in partitions)
|
||||
{
|
||||
// Compose new file name
|
||||
var partitionFilePath = $"{fileNameWithoutExt}-{partitionNumber}{fileExt}";
|
||||
|
||||
// Compose full file path
|
||||
if (dirPath.IsNotBlank())
|
||||
partitionFilePath = Path.Combine(dirPath, partitionFilePath);
|
||||
|
||||
// Export
|
||||
ExportChatLogSingle(partition, partitionFilePath, format);
|
||||
|
||||
// Increment partition number
|
||||
partitionNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
public void ExportChatLog(ChatLog chatLog, string filePath, ExportFormat format,
|
||||
int? partitionLimit = null)
|
||||
{
|
||||
// If partitioning is disabled or there are fewer messages in chat log than the limit - process it without partitioning
|
||||
if (partitionLimit == null || chatLog.Messages.Count <= partitionLimit)
|
||||
{
|
||||
ExportChatLogSingle(chatLog, filePath, format);
|
||||
}
|
||||
// Otherwise split into partitions and export separately
|
||||
else
|
||||
{
|
||||
var partitions = chatLog.SplitIntoPartitions(partitionLimit.Value);
|
||||
ExportChatLogPartitions(partitions, filePath, format);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,5 +24,11 @@ namespace DiscordChatExporter.Core.Services
|
||||
|
||||
Task<Mentionables> GetMentionablesAsync(AuthToken token, string guildId,
|
||||
IEnumerable<Message> messages);
|
||||
|
||||
Task<ChatLog> GetChatLogAsync(AuthToken token, Guild guild, Channel channel,
|
||||
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null);
|
||||
|
||||
Task<ChatLog> GetChatLogAsync(AuthToken token, string channelId,
|
||||
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public interface IExportService
|
||||
{
|
||||
void Export(ExportFormat format, string filePath, ChatLog log);
|
||||
void ExportChatLog(ChatLog chatLog, string filePath, ExportFormat format,
|
||||
int? partitionLimit = null);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public interface IMessageGroupService
|
||||
{
|
||||
IReadOnlyList<MessageGroup> GroupMessages(IEnumerable<Message> messages);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace DiscordChatExporter.Core.Services
|
||||
|
||||
AuthToken LastToken { get; set; }
|
||||
ExportFormat LastExportFormat { get; set; }
|
||||
int? LastPartitionLimit { get; set; }
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public class MessageGroupService : IMessageGroupService
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public MessageGroupService(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public IReadOnlyList<MessageGroup> GroupMessages(IEnumerable<Message> messages)
|
||||
{
|
||||
var result = new List<MessageGroup>();
|
||||
|
||||
// Group adjacent messages by timestamp and author
|
||||
var groupBuffer = new List<Message>();
|
||||
foreach (var message in messages)
|
||||
{
|
||||
var groupFirst = groupBuffer.FirstOrDefault();
|
||||
|
||||
// Group break condition
|
||||
var breakCondition =
|
||||
groupFirst != null &&
|
||||
(
|
||||
message.Author.Id != groupFirst.Author.Id ||
|
||||
(message.Timestamp - groupFirst.Timestamp).TotalHours > 1 ||
|
||||
message.Timestamp.Hour != groupFirst.Timestamp.Hour ||
|
||||
groupBuffer.Count >= _settingsService.MessageGroupLimit
|
||||
);
|
||||
|
||||
// If condition is true - flush buffer
|
||||
if (breakCondition)
|
||||
{
|
||||
var group = new MessageGroup(groupFirst.Author, groupFirst.Timestamp, groupBuffer.ToArray());
|
||||
result.Add(group);
|
||||
groupBuffer.Clear();
|
||||
}
|
||||
|
||||
// Add message to buffer
|
||||
groupBuffer.Add(message);
|
||||
}
|
||||
|
||||
// Add what's remaining in buffer
|
||||
if (groupBuffer.Any())
|
||||
{
|
||||
var groupFirst = groupBuffer.First();
|
||||
var group = new MessageGroup(groupFirst.Author, groupFirst.Timestamp, groupBuffer.ToArray());
|
||||
result.Add(group);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ namespace DiscordChatExporter.Core.Services
|
||||
|
||||
public AuthToken LastToken { get; set; }
|
||||
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
|
||||
public int? LastPartitionLimit { get; set; }
|
||||
|
||||
public SettingsService()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||
xmlns:local="clr-namespace:DiscordChatExporter.Gui"
|
||||
DispatcherUnhandledException="App_OnDispatcherUnhandledException"
|
||||
StartupUri="Views/MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
namespace DiscordChatExporter.Gui
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace DiscordChatExporter.Gui
|
||||
{
|
||||
public partial class App
|
||||
{
|
||||
private void App_OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs args)
|
||||
{
|
||||
MessageBox.Show(args.Exception.ToString(), "Error occured", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ namespace DiscordChatExporter.Gui
|
||||
// Services
|
||||
SimpleIoc.Default.Register<IDataService, DataService>();
|
||||
SimpleIoc.Default.Register<IExportService, ExportService>();
|
||||
SimpleIoc.Default.Register<IMessageGroupService, MessageGroupService>();
|
||||
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
||||
SimpleIoc.Default.Register<IUpdateService, UpdateService>();
|
||||
|
||||
|
||||
@@ -42,39 +42,12 @@
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="CommonServiceLocator, Version=2.0.3.0, Culture=neutral, PublicKeyToken=489b6accfaf20ef0, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\CommonServiceLocator.2.0.3\lib\net45\CommonServiceLocator.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="GalaSoft.MvvmLight, Version=5.4.1.0, Culture=neutral, PublicKeyToken=e7570ab207bcb616, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\GalaSoft.MvvmLight.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="GalaSoft.MvvmLight.Extras, Version=5.4.1.0, Culture=neutral, PublicKeyToken=669f0b5e8f868abf, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\GalaSoft.MvvmLight.Extras.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="GalaSoft.MvvmLight.Platform, Version=5.4.1.0, Culture=neutral, PublicKeyToken=5f873c45e98af8a1, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\GalaSoft.MvvmLight.Platform.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MaterialDesignColors, Version=1.1.3.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MaterialDesignColors.1.1.3\lib\net45\MaterialDesignColors.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MaterialDesignThemes.Wpf, Version=2.4.0.1044, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MaterialDesignThemes.2.4.0.1044\lib\net45\MaterialDesignThemes.Wpf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\System.Windows.Interactivity.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Xaml">
|
||||
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="Tyrrrz.Extensions, Version=1.5.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Tyrrrz.Extensions.1.5.1\lib\net45\Tyrrrz.Extensions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Tyrrrz.Settings, Version=1.3.2.0, Culture=neutral, PublicKeyToken=null" />
|
||||
<Reference Include="Tyrrrz.WpfExtensions, Version=1.0.6269.37170, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Tyrrrz.WpfExtensions.1.0.5\lib\net45\Tyrrrz.WpfExtensions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="WindowsBase" />
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
@@ -119,9 +92,6 @@
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config">
|
||||
<SubType>Designer</SubType>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="..\favicon.ico" />
|
||||
@@ -150,6 +120,25 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommonServiceLocator">
|
||||
<Version>2.0.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MaterialDesignColors">
|
||||
<Version>1.1.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MaterialDesignThemes">
|
||||
<Version>2.4.0.1044</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MvvmLightLibs">
|
||||
<Version>5.4.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Tyrrrz.Extensions">
|
||||
<Version>1.5.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Tyrrrz.WpfExtensions">
|
||||
<Version>1.0.5</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -15,14 +15,17 @@ namespace DiscordChatExporter.Gui.Messages
|
||||
|
||||
public DateTime? To { get; }
|
||||
|
||||
public int? PartitionLimit { get; }
|
||||
|
||||
public StartExportMessage(Channel channel, string filePath, ExportFormat format,
|
||||
DateTime? from, DateTime? to)
|
||||
DateTime? from, DateTime? to, int? partitionLimit)
|
||||
{
|
||||
Channel = channel;
|
||||
FilePath = filePath;
|
||||
Format = format;
|
||||
From = from;
|
||||
To = to;
|
||||
PartitionLimit = partitionLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,5 @@
|
||||
[assembly: AssemblyTitle("DiscordChatExporter")]
|
||||
[assembly: AssemblyCompany("Tyrrrz")]
|
||||
[assembly: AssemblyCopyright("Copyright (c) 2017-2018 Alexey Golub")]
|
||||
[assembly: AssemblyVersion("2.7")]
|
||||
[assembly: AssemblyFileVersion("2.7")]
|
||||
[assembly: AssemblyVersion("2.8")]
|
||||
[assembly: AssemblyFileVersion("2.8")]
|
||||
@@ -19,6 +19,7 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||
private ExportFormat _format;
|
||||
private DateTime? _from;
|
||||
private DateTime? _to;
|
||||
private int? _partitionLimit;
|
||||
|
||||
public Guild Guild { get; private set; }
|
||||
|
||||
@@ -63,6 +64,12 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||
set => Set(ref _to, value);
|
||||
}
|
||||
|
||||
public int? PartitionLimit
|
||||
{
|
||||
get => _partitionLimit;
|
||||
set => Set(ref _partitionLimit, value);
|
||||
}
|
||||
|
||||
// Commands
|
||||
public RelayCommand ExportCommand { get; }
|
||||
|
||||
@@ -83,13 +90,15 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||
.Replace(Path.GetInvalidFileNameChars(), '_');
|
||||
From = null;
|
||||
To = null;
|
||||
PartitionLimit = _settingsService.LastPartitionLimit;
|
||||
});
|
||||
}
|
||||
|
||||
private void Export()
|
||||
{
|
||||
// Save format
|
||||
// Persist preferences
|
||||
_settingsService.LastExportFormat = SelectedFormat;
|
||||
_settingsService.LastPartitionLimit = PartitionLimit;
|
||||
|
||||
// Clamp 'from' and 'to' values
|
||||
if (From > To)
|
||||
@@ -98,7 +107,7 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||
To = From;
|
||||
|
||||
// Start export
|
||||
MessengerInstance.Send(new StartExportMessage(Channel, FilePath, SelectedFormat, From, To));
|
||||
MessengerInstance.Send(new StartExportMessage(Channel, FilePath, SelectedFormat, From, To, PartitionLimit));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||
ExportFormat SelectedFormat { get; set; }
|
||||
DateTime? From { get; set; }
|
||||
DateTime? To { get; set; }
|
||||
int? PartitionLimit { get; set; }
|
||||
|
||||
RelayCommand ExportCommand { get; }
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IUpdateService _updateService;
|
||||
private readonly IDataService _dataService;
|
||||
private readonly IMessageGroupService _messageGroupService;
|
||||
private readonly IExportService _exportService;
|
||||
|
||||
private readonly Dictionary<Guild, IReadOnlyList<Channel>> _guildChannelsMap;
|
||||
@@ -111,12 +110,11 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||
public RelayCommand<Channel> ShowExportSetupCommand { get; }
|
||||
|
||||
public MainViewModel(ISettingsService settingsService, IUpdateService updateService, IDataService dataService,
|
||||
IMessageGroupService messageGroupService, IExportService exportService)
|
||||
IExportService exportService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_updateService = updateService;
|
||||
_dataService = dataService;
|
||||
_messageGroupService = messageGroupService;
|
||||
_exportService = exportService;
|
||||
|
||||
_guildChannelsMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
||||
@@ -131,7 +129,7 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||
|
||||
// Messages
|
||||
MessengerInstance.Register<StartExportMessage>(this,
|
||||
m => Export(m.Channel, m.FilePath, m.Format, m.From, m.To));
|
||||
m => Export(m.Channel, m.FilePath, m.Format, m.From, m.To, m.PartitionLimit));
|
||||
}
|
||||
|
||||
private async void ViewLoaded()
|
||||
@@ -241,7 +239,8 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||
MessengerInstance.Send(new ShowExportSetupMessage(SelectedGuild, channel));
|
||||
}
|
||||
|
||||
private async void Export(Channel channel, string filePath, ExportFormat format, DateTime? from, DateTime? to)
|
||||
private async void Export(Channel channel, string filePath, ExportFormat format,
|
||||
DateTime? from, DateTime? to, int? partitionLimit)
|
||||
{
|
||||
IsBusy = true;
|
||||
|
||||
@@ -256,24 +255,11 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: extract this to make it reusable across implementations
|
||||
// Get messages
|
||||
var messages = await _dataService.GetChannelMessagesAsync(token, channel.Id, from, to, progressHandler);
|
||||
|
||||
// Group messages
|
||||
var messageGroups = _messageGroupService.GroupMessages(messages);
|
||||
|
||||
// Get mentionables
|
||||
var mentionables = await _dataService.GetMentionablesAsync(token, guild.Id, messages);
|
||||
|
||||
// Create log
|
||||
var log = new ChatLog(guild, channel, from, to, messageGroups, mentionables);
|
||||
// Get chat log
|
||||
var chatLog = await _dataService.GetChatLogAsync(token, guild, channel, from, to, progressHandler);
|
||||
|
||||
// Export
|
||||
_exportService.Export(format, filePath, log);
|
||||
|
||||
// Open
|
||||
Process.Start(filePath);
|
||||
_exportService.ExportChatLog(chatLog, filePath, format, partitionLimit);
|
||||
|
||||
// Notify completion
|
||||
MessengerInstance.Send(new ShowNotificationMessage("Export complete"));
|
||||
|
||||
@@ -55,6 +55,13 @@
|
||||
SelectedDate="{Binding To}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Partitioning -->
|
||||
<TextBox
|
||||
Margin="16,8,16,8"
|
||||
materialDesign:HintAssist.Hint="Messages per partition (optional)"
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
Text="{Binding PartitionLimit, TargetNullValue=''}" />
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<Button
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="CommonServiceLocator" version="2.0.3" targetFramework="net461" />
|
||||
<package id="MaterialDesignColors" version="1.1.3" targetFramework="net461" />
|
||||
<package id="MaterialDesignThemes" version="2.4.0.1044" targetFramework="net461" />
|
||||
<package id="MvvmLightLibs" version="5.4.1" targetFramework="net461" />
|
||||
<package id="Tyrrrz.Extensions" version="1.5.1" targetFramework="net461" />
|
||||
<package id="Tyrrrz.WpfExtensions" version="1.0.5" targetFramework="net461" />
|
||||
</packages>
|
||||
Reference in New Issue
Block a user