mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-06-07 01:51:18 -04:00
Make it work
This commit is contained in:
parent
01ad2fd711
commit
eb59cbde28
11 changed files with 581 additions and 0 deletions
22
DiscordChatExporter.sln
Normal file
22
DiscordChatExporter.sln
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 15
|
||||||
|
VisualStudioVersion = 15.0.26430.13
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordChatExporter", "DiscordChatExporter\DiscordChatExporter.csproj", "{4BE915D1-129C-49E2-860E-62045ACA5EAD}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{4BE915D1-129C-49E2-860E-62045ACA5EAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4BE915D1-129C-49E2-860E-62045ACA5EAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4BE915D1-129C-49E2-860E-62045ACA5EAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4BE915D1-129C-49E2-860E-62045ACA5EAD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
27
DiscordChatExporter/DiscordChatExporter.csproj
Normal file
27
DiscordChatExporter/DiscordChatExporter.csproj
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net45</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Services\ExportTemplate.html" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Services\ExportTemplate.html" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommandLineParser" Version="1.9.71" />
|
||||||
|
<PackageReference Include="HtmlAgilityPack" Version="1.5.1" />
|
||||||
|
<PackageReference Include="Newtonsoft.json" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Tyrrrz.Extensions" Version="1.4.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
21
DiscordChatExporter/Models/Attachment.cs
Normal file
21
DiscordChatExporter/Models/Attachment.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
namespace DiscordChatExporter.Models
|
||||||
|
{
|
||||||
|
public class Attachment
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
public string Url { get; }
|
||||||
|
|
||||||
|
public string FileName { get; }
|
||||||
|
|
||||||
|
public long ContentLength { get; }
|
||||||
|
|
||||||
|
public Attachment(string id, string url, string fileName, long contentLength)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Url = url;
|
||||||
|
FileName = fileName;
|
||||||
|
ContentLength = contentLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
DiscordChatExporter/Models/ChatLog.cs
Normal file
22
DiscordChatExporter/Models/ChatLog.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Models
|
||||||
|
{
|
||||||
|
public class ChatLog
|
||||||
|
{
|
||||||
|
public string ChannelId { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<User> Participants { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Message> Messages { get; }
|
||||||
|
|
||||||
|
public ChatLog(string channelId, IEnumerable<Message> messages)
|
||||||
|
{
|
||||||
|
ChannelId = channelId;
|
||||||
|
Messages = messages.ToArray();
|
||||||
|
Participants = Messages.Select(m => m.Author).Distinct(a => a.Name).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
DiscordChatExporter/Models/Message.cs
Normal file
33
DiscordChatExporter/Models/Message.cs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Models
|
||||||
|
{
|
||||||
|
public class Message
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
public DateTime TimeStamp { get; }
|
||||||
|
|
||||||
|
public User Author { get; }
|
||||||
|
|
||||||
|
public string Content { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Attachment> Attachments { get; }
|
||||||
|
|
||||||
|
public Message(string id, DateTime timeStamp, User author, string content, IEnumerable<Attachment> attachments)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
TimeStamp = timeStamp;
|
||||||
|
Author = author;
|
||||||
|
Content = content;
|
||||||
|
Attachments = attachments.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
DiscordChatExporter/Models/User.cs
Normal file
29
DiscordChatExporter/Models/User.cs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Models
|
||||||
|
{
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public string AvatarHash { get; }
|
||||||
|
|
||||||
|
public string AvatarUrl => AvatarHash.IsNotBlank()
|
||||||
|
? $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.png?size=256"
|
||||||
|
: "https://discordapp.com/assets/6debd47ed13483642cf09e832ed0bc1b.png";
|
||||||
|
|
||||||
|
public User(string id, string name, string avatarHash)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Name = name;
|
||||||
|
AvatarHash = avatarHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
DiscordChatExporter/Options.cs
Normal file
32
DiscordChatExporter/Options.cs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
using CommandLine;
|
||||||
|
using CommandLine.Text;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter
|
||||||
|
{
|
||||||
|
public class Options
|
||||||
|
{
|
||||||
|
[Option('t', "token", Required = true, HelpText = "Discord access token")]
|
||||||
|
public string Token { get; set; }
|
||||||
|
|
||||||
|
[Option('c', "channel", Required = true, HelpText = "ID of the text channel to export")]
|
||||||
|
public string ChannelId { get; set; }
|
||||||
|
|
||||||
|
[HelpOption]
|
||||||
|
public string GetUsage()
|
||||||
|
{
|
||||||
|
var help = new HelpText
|
||||||
|
{
|
||||||
|
Heading = new HeadingInfo("DiscordChatExporter"),
|
||||||
|
Copyright = new CopyrightInfo("Alexey 'Tyrrrz' Golub", 2017),
|
||||||
|
AdditionalNewLineAfterOption = true,
|
||||||
|
AddDashesToOption = true
|
||||||
|
};
|
||||||
|
help.AddPreOptionsLine("Usage: DiscordChatExporter.exe " +
|
||||||
|
"-t REkOTVqm9RWOTNOLCdiuMpWd.QiglBz.Lub0E0TZ1xX4ZxCtnwtpBhWt3v1 " +
|
||||||
|
"-c 459360869055190534");
|
||||||
|
help.AddOptions(this);
|
||||||
|
|
||||||
|
return help;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
DiscordChatExporter/Program.cs
Normal file
37
DiscordChatExporter/Program.cs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Models;
|
||||||
|
using DiscordChatExporter.Services;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter
|
||||||
|
{
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
private static readonly Options Options = new Options();
|
||||||
|
|
||||||
|
private static readonly DiscordApiService DiscordApiService = new DiscordApiService();
|
||||||
|
private static readonly ExportService ExportService = new ExportService();
|
||||||
|
|
||||||
|
private static async Task MainAsync(string[] args)
|
||||||
|
{
|
||||||
|
// Parse cmd args
|
||||||
|
CommandLine.Parser.Default.ParseArgumentsStrict(args, Options);
|
||||||
|
|
||||||
|
// Get messages
|
||||||
|
Console.WriteLine("Getting messages...");
|
||||||
|
var messages = await DiscordApiService.GetMessagesAsync(Options.Token, Options.ChannelId);
|
||||||
|
var chatLog = new ChatLog(Options.ChannelId, messages);
|
||||||
|
|
||||||
|
// Export
|
||||||
|
Console.WriteLine("Exporting messages...");
|
||||||
|
ExportService.Export($"{Options.ChannelId}.html", chatLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.Title = "Discord Chat Exporter";
|
||||||
|
|
||||||
|
MainAsync(args).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
DiscordChatExporter/Services/DiscordApiService.cs
Normal file
94
DiscordChatExporter/Services/DiscordApiService.cs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Models;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Services
|
||||||
|
{
|
||||||
|
public class DiscordApiService
|
||||||
|
{
|
||||||
|
private const string ApiRoot = "https://discordapp.com/api";
|
||||||
|
private readonly HttpClient _httpClient = new HttpClient();
|
||||||
|
|
||||||
|
private IEnumerable<Message> ParseMessages(string json)
|
||||||
|
{
|
||||||
|
var messagesJson = JArray.Parse(json);
|
||||||
|
foreach (var messageJson in messagesJson)
|
||||||
|
{
|
||||||
|
// Get basic data
|
||||||
|
string id = messageJson.Value<string>("id");
|
||||||
|
var timeStamp = messageJson.Value<DateTime>("timestamp");
|
||||||
|
string content = messageJson.Value<string>("content");
|
||||||
|
|
||||||
|
// Get author
|
||||||
|
var authorJson = messageJson["author"];
|
||||||
|
string authorId = authorJson.Value<string>("id");
|
||||||
|
string authorName = authorJson.Value<string>("username");
|
||||||
|
string authorAvatarHash = authorJson.Value<string>("avatar");
|
||||||
|
|
||||||
|
// Get attachment
|
||||||
|
var attachmentsJson = messageJson["attachments"];
|
||||||
|
var attachments = new List<Attachment>();
|
||||||
|
foreach (var attachmentJson in attachmentsJson)
|
||||||
|
{
|
||||||
|
string attachmentId = attachmentJson.Value<string>("id");
|
||||||
|
string attachmentUrl = attachmentJson.Value<string>("url");
|
||||||
|
string attachmentFileName = attachmentJson.Value<string>("filename");
|
||||||
|
long attachmentContentLength = attachmentJson.Value<long>("size");
|
||||||
|
|
||||||
|
var attachment = new Attachment(attachmentId, attachmentUrl, attachmentFileName, attachmentContentLength);
|
||||||
|
attachments.Add(attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
var author = new User(authorId, authorName, authorAvatarHash);
|
||||||
|
var message = new Message(id, timeStamp, author, content, attachments);
|
||||||
|
|
||||||
|
yield return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Message>> GetMessagesAsync(string token, string channelId)
|
||||||
|
{
|
||||||
|
var result = new List<Message>();
|
||||||
|
|
||||||
|
// We are going backwards from last message to first
|
||||||
|
// ...collecting everything between them in batches
|
||||||
|
string beforeId = null;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// Form request url
|
||||||
|
string url = $"{ApiRoot}/channels/{channelId}/messages?token={token}&limit=100";
|
||||||
|
if (beforeId.IsNotBlank())
|
||||||
|
url += $"&before={beforeId}";
|
||||||
|
|
||||||
|
// Get response
|
||||||
|
string response = await _httpClient.GetStringAsync(url);
|
||||||
|
|
||||||
|
// Parse
|
||||||
|
var messages = ParseMessages(response);
|
||||||
|
|
||||||
|
// Add messages to list
|
||||||
|
string currentMessageId = null;
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
result.Add(message);
|
||||||
|
currentMessageId = message.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no messages - break
|
||||||
|
if (currentMessageId == null) break;
|
||||||
|
|
||||||
|
// Otherwise offset the next request
|
||||||
|
beforeId = currentMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages appear newest first, we need to reverse
|
||||||
|
result.Reverse();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
180
DiscordChatExporter/Services/ExportService.cs
Normal file
180
DiscordChatExporter/Services/ExportService.cs
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using DiscordChatExporter.Models;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Services
|
||||||
|
{
|
||||||
|
public class ExportService
|
||||||
|
{
|
||||||
|
private class MessageGroup
|
||||||
|
{
|
||||||
|
public User Author { get; }
|
||||||
|
|
||||||
|
public DateTime FirstTimeStamp { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Message> Messages { get; }
|
||||||
|
|
||||||
|
public MessageGroup(User author, DateTime firstTimeStamp, IEnumerable<Message> messages)
|
||||||
|
{
|
||||||
|
Author = author;
|
||||||
|
FirstTimeStamp = firstTimeStamp;
|
||||||
|
Messages = messages.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HtmlDocument GetTemplate()
|
||||||
|
{
|
||||||
|
const string templateName = "DiscordChatExporter.Services.ExportTemplate.html";
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
using (var stream = assembly.GetManifestResourceStream(templateName))
|
||||||
|
{
|
||||||
|
var doc = new HtmlDocument();
|
||||||
|
doc.Load(stream);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<MessageGroup> GroupMessages(IEnumerable<Message> messages)
|
||||||
|
{
|
||||||
|
var result = new List<MessageGroup>();
|
||||||
|
|
||||||
|
// Group adjacent messages by timestamp and author
|
||||||
|
var buffer = new List<Message>();
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
var bufferFirst = buffer.FirstOrDefault();
|
||||||
|
|
||||||
|
// Group break condition
|
||||||
|
bool breakCondition =
|
||||||
|
bufferFirst != null &&
|
||||||
|
(
|
||||||
|
message.Author.Id != bufferFirst.Author.Id ||
|
||||||
|
(message.TimeStamp - bufferFirst.TimeStamp).TotalHours > 1 ||
|
||||||
|
message.TimeStamp.Hour != bufferFirst.TimeStamp.Hour
|
||||||
|
);
|
||||||
|
|
||||||
|
// If condition is true - flush buffer
|
||||||
|
if (breakCondition)
|
||||||
|
{
|
||||||
|
var group = new MessageGroup(bufferFirst.Author, bufferFirst.TimeStamp, buffer);
|
||||||
|
result.Add(group);
|
||||||
|
buffer.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add message to buffer
|
||||||
|
buffer.Add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add what's remaining in buffer
|
||||||
|
if (buffer.Any())
|
||||||
|
{
|
||||||
|
var bufferFirst = buffer.First();
|
||||||
|
var group = new MessageGroup(bufferFirst.Author, bufferFirst.TimeStamp, buffer);
|
||||||
|
result.Add(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatMessageContent(string content)
|
||||||
|
{
|
||||||
|
// Encode HTML
|
||||||
|
content = HtmlDocument.HtmlEncode(content);
|
||||||
|
|
||||||
|
// Links from URLs
|
||||||
|
content = Regex.Replace(content, "((^|\\s)(https?|ftp)://[^\\s/$.?#].[^\\s]*($|\\s))",
|
||||||
|
"<a href=\"$1\">$1</a>");
|
||||||
|
|
||||||
|
// Preformatted multiline
|
||||||
|
content = Regex.Replace(content, "```([^`]*?)```", e => "<pre>" + e.Groups[1].Value + "</pre>");
|
||||||
|
|
||||||
|
// Preformatted
|
||||||
|
content = Regex.Replace(content, "`([^`]*?)`", e => "<pre>" + e.Groups[1].Value + "</pre>");
|
||||||
|
|
||||||
|
// Bold
|
||||||
|
content = Regex.Replace(content, "\\*\\*([^\\*]*?)\\*\\*", "<b>$1</b>");
|
||||||
|
|
||||||
|
// Italic
|
||||||
|
content = Regex.Replace(content, "\\*([^\\*]*?)\\*", "<i>$1</i>");
|
||||||
|
|
||||||
|
// Underline
|
||||||
|
content = Regex.Replace(content, "__([^_]*?)__", "<u>$1</u>");
|
||||||
|
|
||||||
|
// Strike through
|
||||||
|
content = Regex.Replace(content, "~~([^~]*?)~~", "<s>$1</s>");
|
||||||
|
|
||||||
|
// New lines
|
||||||
|
content = content.Replace("\n", "</br>");
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Export(string filePath, ChatLog chatLog)
|
||||||
|
{
|
||||||
|
var doc = GetTemplate();
|
||||||
|
|
||||||
|
// Info
|
||||||
|
var infoHtml = doc.GetElementbyId("info");
|
||||||
|
infoHtml.AppendChild(HtmlNode.CreateNode($"<div>Channel ID: <b>{chatLog.ChannelId}</b></div>"));
|
||||||
|
string participants = HtmlDocument.HtmlEncode(chatLog.Participants.Select(u => u.Name).JoinToString(", "));
|
||||||
|
infoHtml.AppendChild(HtmlNode.CreateNode($"<div>Participants: <b>{participants}</b></div>"));
|
||||||
|
infoHtml.AppendChild(HtmlNode.CreateNode($"<div>Messages: <b>{chatLog.Messages.Count:N0}</b></div>"));
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
var logHtml = doc.GetElementbyId("log");
|
||||||
|
var messageGroups = GroupMessages(chatLog.Messages);
|
||||||
|
foreach (var messageGroup in messageGroups)
|
||||||
|
{
|
||||||
|
// Container
|
||||||
|
var messageHtml = logHtml.AppendChild(HtmlNode.CreateNode("<div class=\"msg\"></div>"));
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
messageHtml.AppendChild(HtmlNode.CreateNode("<img class=\"msg-avatar\" " +
|
||||||
|
$"src=\"{messageGroup.Author.AvatarUrl}\"></img>"));
|
||||||
|
|
||||||
|
// Body
|
||||||
|
var messageBodyHtml = messageHtml.AppendChild(HtmlNode.CreateNode("<div class=\"msg-body\"></div>"));
|
||||||
|
|
||||||
|
// Author
|
||||||
|
string authorName = HtmlDocument.HtmlEncode(messageGroup.Author.Name);
|
||||||
|
messageBodyHtml.AppendChild(HtmlNode.CreateNode($"<span class=\"msg-user\">{authorName}</span>"));
|
||||||
|
|
||||||
|
// Date
|
||||||
|
string timeStamp = HtmlDocument.HtmlEncode(messageGroup.FirstTimeStamp.ToString("g"));
|
||||||
|
messageBodyHtml.AppendChild(HtmlNode.CreateNode($"<span class=\"msg-date\">{timeStamp}</span>"));
|
||||||
|
|
||||||
|
// Separate messages
|
||||||
|
foreach (var message in messageGroup.Messages)
|
||||||
|
{
|
||||||
|
// Content
|
||||||
|
if (message.Content.IsNotBlank())
|
||||||
|
{
|
||||||
|
string content = FormatMessageContent(message.Content);
|
||||||
|
messageBodyHtml.AppendChild(HtmlNode.CreateNode($"<div class=\"msg-content\">{content}</div>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachments
|
||||||
|
if (message.Attachments.Any())
|
||||||
|
{
|
||||||
|
// Attachments
|
||||||
|
foreach (var attachment in message.Attachments)
|
||||||
|
{
|
||||||
|
messageBodyHtml.AppendChild(
|
||||||
|
HtmlNode.CreateNode("<div class=\"msg-attachment\">" +
|
||||||
|
$"<a href=\"{attachment.Url}\">" +
|
||||||
|
$"<img class=\"msg-attachment\" src=\"{attachment.Url}\" />" +
|
||||||
|
"</a></div>"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.Save(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
DiscordChatExporter/Services/ExportTemplate.html
Normal file
84
DiscordChatExporter/Services/ExportTemplate.html
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Discord Chat Log</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
font-size: 15px;
|
||||||
|
font-family: Whitney, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #37bcf7;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 2px;
|
||||||
|
padding-right: 2px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
font-family: Consolas, Courier New, Courier, Monospace;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
div#info {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #939799;
|
||||||
|
}
|
||||||
|
div#log {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
div.msg {
|
||||||
|
display: flex;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-top: 1px solid #eceeef;
|
||||||
|
}
|
||||||
|
img.msg-avatar {
|
||||||
|
flex: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
div.msg-body {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
span.msg-user {
|
||||||
|
color: #2f3136;
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
span.msg-date {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #b7bcbf;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
div.msg-content {
|
||||||
|
padding-top: 5px;
|
||||||
|
color: #939799;
|
||||||
|
}
|
||||||
|
div.msg-attachment {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
img.msg-attachment {
|
||||||
|
max-width: 50%;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="info" />
|
||||||
|
<div id="log" />
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Add table
Add a link
Reference in a new issue