Add JSON message writer

Closes #103
This commit is contained in:
Alexey Golub 2020-02-03 17:47:42 +02:00
parent 9fa40dca00
commit 9f6090b3af
9 changed files with 302 additions and 26 deletions

View file

@ -5,6 +5,7 @@
PlainText, PlainText,
HtmlDark, HtmlDark,
HtmlLight, HtmlLight,
Csv Csv,
Json
} }
} }

View file

@ -18,6 +18,7 @@ namespace DiscordChatExporter.Core.Models
ExportFormat.HtmlDark => "html", ExportFormat.HtmlDark => "html",
ExportFormat.HtmlLight => "html", ExportFormat.HtmlLight => "html",
ExportFormat.Csv => "csv", ExportFormat.Csv => "csv",
ExportFormat.Json => "json",
_ => throw new ArgumentOutOfRangeException(nameof(format)) _ => throw new ArgumentOutOfRangeException(nameof(format))
}; };
@ -28,6 +29,7 @@ namespace DiscordChatExporter.Core.Models
ExportFormat.HtmlDark => "HTML (Dark)", ExportFormat.HtmlDark => "HTML (Dark)",
ExportFormat.HtmlLight => "HTML (Light)", ExportFormat.HtmlLight => "HTML (Light)",
ExportFormat.Csv => "Comma Separated Values (CSV)", ExportFormat.Csv => "Comma Separated Values (CSV)",
ExportFormat.Json => "JavaScript Object Notation (JSON)",
_ => throw new ArgumentOutOfRangeException(nameof(format)) _ => throw new ArgumentOutOfRangeException(nameof(format))
}; };
} }

View file

@ -7,19 +7,28 @@ namespace DiscordChatExporter.Core.Rendering.Formatters
{ {
public class CsvMessageWriter : MessageWriterBase public class CsvMessageWriter : MessageWriterBase
{ {
public CsvMessageWriter(TextWriter writer, RenderContext context) private readonly TextWriter _writer;
: base(writer, context)
public CsvMessageWriter(Stream stream, RenderContext context)
: base(stream, context)
{ {
_writer = new StreamWriter(stream);
} }
public override async Task WritePreambleAsync() public override async Task WritePreambleAsync()
{ {
await Writer.WriteLineAsync(CsvRenderingLogic.FormatHeader(Context)); await _writer.WriteLineAsync(CsvRenderingLogic.FormatHeader(Context));
} }
public override async Task WriteMessageAsync(Message message) public override async Task WriteMessageAsync(Message message)
{ {
await Writer.WriteLineAsync(CsvRenderingLogic.FormatMessage(Context, message)); await _writer.WriteLineAsync(CsvRenderingLogic.FormatMessage(Context, message));
}
public override async ValueTask DisposeAsync()
{
await _writer.DisposeAsync();
await base.DisposeAsync();
} }
} }
} }

View file

@ -14,6 +14,7 @@ namespace DiscordChatExporter.Core.Rendering.Formatters
{ {
public partial class HtmlMessageWriter : MessageWriterBase public partial class HtmlMessageWriter : MessageWriterBase
{ {
private readonly TextWriter _writer;
private readonly string _themeName; private readonly string _themeName;
private readonly List<Message> _messageGroupBuffer = new List<Message>(); private readonly List<Message> _messageGroupBuffer = new List<Message>();
@ -23,9 +24,10 @@ namespace DiscordChatExporter.Core.Rendering.Formatters
private long _messageCount; private long _messageCount;
public HtmlMessageWriter(TextWriter writer, RenderContext context, string themeName) public HtmlMessageWriter(Stream stream, RenderContext context, string themeName)
: base(writer, context) : base(stream, context)
{ {
_writer = new StreamWriter(stream);
_themeName = themeName; _themeName = themeName;
_preambleTemplate = Template.Parse(GetPreambleTemplateCode()); _preambleTemplate = Template.Parse(GetPreambleTemplateCode());
@ -77,7 +79,7 @@ namespace DiscordChatExporter.Core.Rendering.Formatters
templateContext.PushGlobal(scriptObject); templateContext.PushGlobal(scriptObject);
// Push output // Push output
templateContext.PushOutput(new TextWriterOutput(Writer)); templateContext.PushOutput(new TextWriterOutput(_writer));
return templateContext; return templateContext;
} }
@ -131,6 +133,12 @@ namespace DiscordChatExporter.Core.Rendering.Formatters
await templateContext.EvaluateAsync(_postambleTemplate.Page); await templateContext.EvaluateAsync(_postambleTemplate.Page);
} }
public override async ValueTask DisposeAsync()
{
await _writer.DisposeAsync();
await base.DisposeAsync();
}
} }
public partial class HtmlMessageWriter public partial class HtmlMessageWriter

View file

@ -0,0 +1,222 @@
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Models;
using DiscordChatExporter.Core.Rendering.Internal;
using DiscordChatExporter.Core.Rendering.Logic;
namespace DiscordChatExporter.Core.Rendering.Formatters
{
public class JsonMessageWriter : MessageWriterBase
{
private readonly Utf8JsonWriter _writer;
private long _messageCount;
public JsonMessageWriter(Stream stream, RenderContext context)
: base(stream, context)
{
_writer = new Utf8JsonWriter(stream, new JsonWriterOptions
{
Indented = true
});
}
public override async Task WritePreambleAsync()
{
// Root object (start)
_writer.WriteStartObject();
// Guild
_writer.WriteStartObject("guild");
_writer.WriteString("id", Context.Guild.Id);
_writer.WriteString("name", Context.Guild.Name);
_writer.WriteString("iconUrl", Context.Guild.IconUrl);
_writer.WriteEndObject();
// Channel
_writer.WriteStartObject("channel");
_writer.WriteString("id", Context.Channel.Id);
_writer.WriteString("type", Context.Channel.Type.ToString());
_writer.WriteString("name", Context.Channel.Name);
_writer.WriteString("topic", Context.Channel.Topic);
_writer.WriteEndObject();
// Date range
_writer.WriteStartObject("dateRange");
_writer.WriteString("after", Context.After);
_writer.WriteString("before", Context.Before);
_writer.WriteEndObject();
// Message array (start)
_writer.WriteStartArray("messages");
await _writer.FlushAsync();
}
public override async Task WriteMessageAsync(Message message)
{
_writer.WriteStartObject();
// Metadata
_writer.WriteString("id", message.Id);
_writer.WriteString("type", message.Type.ToString());
_writer.WriteString("timestamp", message.Timestamp);
_writer.WriteString("timestampEdited", message.EditedTimestamp);
_writer.WriteBoolean("isPinned", message.IsPinned);
// Content
var content = PlainTextRenderingLogic.FormatMessageContent(Context, message);
_writer.WriteString("content", content);
// Author
_writer.WriteStartObject("author");
_writer.WriteString("id", message.Author.Id);
_writer.WriteString("name", message.Author.Name);
_writer.WriteString("discriminator", $"{message.Author.Discriminator:0000}");
_writer.WriteBoolean("isBot", message.Author.IsBot);
_writer.WriteString("avatarUrl", message.Author.AvatarUrl);
_writer.WriteEndObject();
// Attachments
_writer.WriteStartArray("attachments");
foreach (var attachment in message.Attachments)
{
_writer.WriteStartObject();
_writer.WriteString("id", attachment.Id);
_writer.WriteString("url", attachment.Url);
_writer.WriteString("fileName", attachment.FileName);
_writer.WriteNumber("fileSizeBytes", (long) attachment.FileSize.Bytes);
_writer.WriteEndObject();
}
_writer.WriteEndArray();
// Embeds
_writer.WriteStartArray("embeds");
foreach (var embed in message.Embeds)
{
_writer.WriteStartObject();
_writer.WriteString("title", embed.Title);
_writer.WriteString("url", embed.Url);
_writer.WriteString("timestamp", embed.Timestamp);
_writer.WriteString("description", embed.Description);
// Author
if (embed.Author != null)
{
_writer.WriteStartObject("author");
_writer.WriteString("name", embed.Author.Name);
_writer.WriteString("url", embed.Author.Url);
_writer.WriteString("iconUrl", embed.Author.IconUrl);
_writer.WriteEndObject();
}
// Thumbnail
if (embed.Thumbnail != null)
{
_writer.WriteStartObject("thumbnail");
_writer.WriteString("url", embed.Thumbnail.Url);
_writer.WriteNumber("width", embed.Thumbnail.Width);
_writer.WriteNumber("height", embed.Thumbnail.Height);
_writer.WriteEndObject();
}
// Image
if (embed.Image != null)
{
_writer.WriteStartObject("image");
_writer.WriteString("url", embed.Image.Url);
_writer.WriteNumber("width", embed.Image.Width);
_writer.WriteNumber("height", embed.Image.Height);
_writer.WriteEndObject();
}
// Footer
if (embed.Footer != null)
{
_writer.WriteStartObject("footer");
_writer.WriteString("text", embed.Footer.Text);
_writer.WriteString("iconUrl", embed.Footer.IconUrl);
_writer.WriteEndObject();
}
// Fields
_writer.WriteStartArray("fields");
foreach (var field in embed.Fields)
{
_writer.WriteStartObject();
_writer.WriteString("name", field.Name);
_writer.WriteString("value", field.Value);
_writer.WriteBoolean("isInline", field.IsInline);
_writer.WriteEndObject();
}
_writer.WriteEndArray();
_writer.WriteEndObject();
}
_writer.WriteEndArray();
// Reactions
_writer.WriteStartArray("reactions");
foreach (var reaction in message.Reactions)
{
_writer.WriteStartObject();
// Emoji
_writer.WriteStartObject("emoji");
_writer.WriteString("id", reaction.Emoji.Id);
_writer.WriteString("name", reaction.Emoji.Name);
_writer.WriteBoolean("isAnimated", reaction.Emoji.IsAnimated);
_writer.WriteString("imageUrl", reaction.Emoji.ImageUrl);
_writer.WriteEndObject();
// Count
_writer.WriteNumber("count", reaction.Count);
_writer.WriteEndObject();
}
_writer.WriteEndArray();
_writer.WriteEndObject();
_messageCount++;
// Flush every 100 messages
if (_messageCount % 100 == 0)
await _writer.FlushAsync();
}
public override async Task WritePostambleAsync()
{
// Message array (end)
_writer.WriteEndArray();
// Message count
_writer.WriteNumber("messageCount", _messageCount);
// Root object (end)
_writer.WriteEndObject();
await _writer.FlushAsync();
}
public override async ValueTask DisposeAsync()
{
await _writer.DisposeAsync();
await base.DisposeAsync();
}
}
}

View file

@ -7,13 +7,13 @@ namespace DiscordChatExporter.Core.Rendering.Formatters
{ {
public abstract class MessageWriterBase : IAsyncDisposable public abstract class MessageWriterBase : IAsyncDisposable
{ {
protected TextWriter Writer { get; } protected Stream Stream { get; }
protected RenderContext Context { get; } protected RenderContext Context { get; }
protected MessageWriterBase(TextWriter writer, RenderContext context) protected MessageWriterBase(Stream stream, RenderContext context)
{ {
Writer = writer; Stream = stream;
Context = context; Context = context;
} }
@ -23,6 +23,6 @@ namespace DiscordChatExporter.Core.Rendering.Formatters
public virtual Task WritePostambleAsync() => Task.CompletedTask; public virtual Task WritePostambleAsync() => Task.CompletedTask;
public async ValueTask DisposeAsync() => await Writer.DisposeAsync(); public virtual async ValueTask DisposeAsync() => await Stream.DisposeAsync();
} }
} }

View file

@ -7,30 +7,39 @@ namespace DiscordChatExporter.Core.Rendering.Formatters
{ {
public class PlainTextMessageWriter : MessageWriterBase public class PlainTextMessageWriter : MessageWriterBase
{ {
private readonly TextWriter _writer;
private long _messageCount; private long _messageCount;
public PlainTextMessageWriter(TextWriter writer, RenderContext context) public PlainTextMessageWriter(Stream stream, RenderContext context)
: base(writer, context) : base(stream, context)
{ {
_writer = new StreamWriter(stream);
} }
public override async Task WritePreambleAsync() public override async Task WritePreambleAsync()
{ {
await Writer.WriteLineAsync(PlainTextRenderingLogic.FormatPreamble(Context)); await _writer.WriteLineAsync(PlainTextRenderingLogic.FormatPreamble(Context));
} }
public override async Task WriteMessageAsync(Message message) public override async Task WriteMessageAsync(Message message)
{ {
await Writer.WriteLineAsync(PlainTextRenderingLogic.FormatMessage(Context, message)); await _writer.WriteLineAsync(PlainTextRenderingLogic.FormatMessage(Context, message));
await Writer.WriteLineAsync(); await _writer.WriteLineAsync();
_messageCount++; _messageCount++;
} }
public override async Task WritePostambleAsync() public override async Task WritePostambleAsync()
{ {
await Writer.WriteLineAsync(); await _writer.WriteLineAsync();
await Writer.WriteLineAsync(PlainTextRenderingLogic.FormatPostamble(_messageCount)); await _writer.WriteLineAsync(PlainTextRenderingLogic.FormatPostamble(_messageCount));
}
public override async ValueTask DisposeAsync()
{
await _writer.DisposeAsync();
await base.DisposeAsync();
} }
} }
} }

View file

@ -1,4 +1,6 @@
using System.Text; using System;
using System.Text;
using System.Text.Json;
namespace DiscordChatExporter.Core.Rendering.Internal namespace DiscordChatExporter.Core.Rendering.Internal
{ {
@ -17,5 +19,25 @@ namespace DiscordChatExporter.Core.Rendering.Internal
return builder; return builder;
} }
public static void WriteString(this Utf8JsonWriter writer, string propertyName, DateTimeOffset? value)
{
writer.WritePropertyName(propertyName);
if (value != null)
writer.WriteStringValue(value.Value);
else
writer.WriteNullValue();
}
public static void WriteNumber(this Utf8JsonWriter writer, string propertyName, int? value)
{
writer.WritePropertyName(propertyName);
if (value != null)
writer.WriteNumberValue(value.Value);
else
writer.WriteNullValue();
}
} }
} }

View file

@ -99,21 +99,24 @@ namespace DiscordChatExporter.Core.Rendering
private static MessageWriterBase CreateMessageWriter(string filePath, ExportFormat format, RenderContext context) private static MessageWriterBase CreateMessageWriter(string filePath, ExportFormat format, RenderContext context)
{ {
// Create inner writer (it will get disposed by the wrapper) // Create a stream (it will get disposed by the writer)
var writer = File.CreateText(filePath); var stream = File.Create(filePath);
// Create formatter // Create formatter
if (format == ExportFormat.PlainText) if (format == ExportFormat.PlainText)
return new PlainTextMessageWriter(writer, context); return new PlainTextMessageWriter(stream, context);
if (format == ExportFormat.Csv) if (format == ExportFormat.Csv)
return new CsvMessageWriter(writer, context); return new CsvMessageWriter(stream, context);
if (format == ExportFormat.HtmlDark) if (format == ExportFormat.HtmlDark)
return new HtmlMessageWriter(writer, context, "Dark"); return new HtmlMessageWriter(stream, context, "Dark");
if (format == ExportFormat.HtmlLight) if (format == ExportFormat.HtmlLight)
return new HtmlMessageWriter(writer, context, "Light"); return new HtmlMessageWriter(stream, context, "Light");
if (format == ExportFormat.Json)
return new JsonMessageWriter(stream, context);
throw new InvalidOperationException($"Unknown export format [{format}]."); throw new InvalidOperationException($"Unknown export format [{format}].");
} }