mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-06-02 23:59:50 -04:00
Add support for embeds (#46)
This commit is contained in:
parent
3b7da21c24
commit
d958f613a3
17 changed files with 1117 additions and 228 deletions
|
@ -5,9 +5,14 @@
|
|||
<Version>2.4.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\ExportService\Shared.css" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\ExportService\DarkTheme.css" />
|
||||
<EmbeddedResource Include="Resources\ExportService\LightTheme.css" />
|
||||
<EmbeddedResource Include="Resources\ExportService\Shared.css" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
73
DiscordChatExporter.Core/Models/Embed.cs
Normal file
73
DiscordChatExporter.Core/Models/Embed.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class Embed : IMentionable
|
||||
{
|
||||
public string Title { get; }
|
||||
|
||||
public string Type { get; }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
public DateTime? TimeStamp { get; }
|
||||
|
||||
public Color? Color { get; }
|
||||
|
||||
public EmbedFooter Footer { get; }
|
||||
|
||||
public EmbedImage Image { get; }
|
||||
|
||||
public EmbedImage Thumbnail { get; }
|
||||
|
||||
public EmbedVideo Video { get; }
|
||||
|
||||
public EmbedProvider Provider { get; }
|
||||
|
||||
public EmbedAuthor Author { get; }
|
||||
|
||||
public IReadOnlyList<EmbedField> Fields { get; }
|
||||
|
||||
public List<User> MentionedUsers { get; }
|
||||
|
||||
public List<Role> MentionedRoles { get; }
|
||||
|
||||
public List<Channel> MentionedChannels { get; }
|
||||
|
||||
public Embed(string title, string type, string description,
|
||||
string url, DateTime? timeStamp, Color? color,
|
||||
EmbedFooter footer, EmbedImage image, EmbedImage thumbnail,
|
||||
EmbedVideo video, EmbedProvider provider, EmbedAuthor author,
|
||||
List<EmbedField> fields, List<User> mentionedUsers,
|
||||
List<Role> mentionedRoles, List<Channel> mentionedChannels)
|
||||
{
|
||||
Title = title;
|
||||
Type = type;
|
||||
Description = description;
|
||||
Url = url;
|
||||
TimeStamp = timeStamp;
|
||||
Color = color;
|
||||
Footer = footer;
|
||||
Image = image;
|
||||
Thumbnail = thumbnail;
|
||||
Video = video;
|
||||
Provider = provider;
|
||||
Author = author;
|
||||
Fields = fields;
|
||||
MentionedUsers = mentionedUsers;
|
||||
MentionedRoles = mentionedRoles;
|
||||
MentionedChannels = mentionedChannels;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Description;
|
||||
}
|
||||
}
|
||||
}
|
31
DiscordChatExporter.Core/Models/EmbedAuthor.cs
Normal file
31
DiscordChatExporter.Core/Models/EmbedAuthor.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-author-structure
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class EmbedAuthor
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
public string IconUrl { get; }
|
||||
|
||||
public string ProxyIconUrl { get; }
|
||||
|
||||
public EmbedAuthor(string name, string url, string iconUrl, string proxyIconUrl)
|
||||
{
|
||||
Name = name;
|
||||
Url = url;
|
||||
IconUrl = iconUrl;
|
||||
ProxyIconUrl = proxyIconUrl;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
23
DiscordChatExporter.Core/Models/EmbedField.cs
Normal file
23
DiscordChatExporter.Core/Models/EmbedField.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-field-structure
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class EmbedField
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public string Value { get; }
|
||||
|
||||
public bool? Inline { get; }
|
||||
|
||||
public EmbedField(string name, string value, bool? inline)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
Inline = inline;
|
||||
}
|
||||
}
|
||||
}
|
28
DiscordChatExporter.Core/Models/EmbedFooter.cs
Normal file
28
DiscordChatExporter.Core/Models/EmbedFooter.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-footer-structure
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class EmbedFooter
|
||||
{
|
||||
public string Text { get; }
|
||||
|
||||
public string IconUrl { get; }
|
||||
|
||||
public string ProxyIconUrl { get; }
|
||||
|
||||
public EmbedFooter(string text, string iconUrl, string proxyIconUrl)
|
||||
{
|
||||
Text = text;
|
||||
IconUrl = iconUrl;
|
||||
ProxyIconUrl = proxyIconUrl;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Text;
|
||||
}
|
||||
}
|
||||
}
|
26
DiscordChatExporter.Core/Models/EmbedImage.cs
Normal file
26
DiscordChatExporter.Core/Models/EmbedImage.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-image-structure
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class EmbedImage
|
||||
{
|
||||
public string Url { get; }
|
||||
|
||||
public string ProxyUrl { get; }
|
||||
|
||||
public int? Height { get; }
|
||||
|
||||
public int? Width { get; }
|
||||
|
||||
public EmbedImage(string url, string proxyUrl, int? height, int? width)
|
||||
{
|
||||
Url = url;
|
||||
ProxyUrl = proxyUrl;
|
||||
Height = height;
|
||||
Width = width;
|
||||
}
|
||||
}
|
||||
}
|
20
DiscordChatExporter.Core/Models/EmbedProvider.cs
Normal file
20
DiscordChatExporter.Core/Models/EmbedProvider.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-provider-structure
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class EmbedProvider
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
public EmbedProvider(string name, string url)
|
||||
{
|
||||
Name = name;
|
||||
Url = url;
|
||||
}
|
||||
}
|
||||
}
|
23
DiscordChatExporter.Core/Models/EmbedVideo.cs
Normal file
23
DiscordChatExporter.Core/Models/EmbedVideo.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-video-structure
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class EmbedVideo
|
||||
{
|
||||
public string Url { get; }
|
||||
|
||||
public int? Height { get; }
|
||||
|
||||
public int? Width { get; }
|
||||
|
||||
public EmbedVideo(string url, int? height, int? width)
|
||||
{
|
||||
Url = url;
|
||||
Height = height;
|
||||
Width = width;
|
||||
}
|
||||
}
|
||||
}
|
17
DiscordChatExporter.Core/Models/IMentionable.cs
Normal file
17
DiscordChatExporter.Core/Models/IMentionable.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
interface IMentionable
|
||||
{
|
||||
List<User> MentionedUsers { get; }
|
||||
|
||||
List<Role> MentionedRoles { get; }
|
||||
|
||||
List<Channel> MentionedChannels { get; }
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class Message
|
||||
public class Message : IMentionable
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
|
@ -21,17 +21,20 @@ namespace DiscordChatExporter.Core.Models
|
|||
|
||||
public IReadOnlyList<Attachment> Attachments { get; }
|
||||
|
||||
public IReadOnlyList<User> MentionedUsers { get; }
|
||||
public IReadOnlyList<Embed> Embeds { get; }
|
||||
|
||||
public IReadOnlyList<Role> MentionedRoles { get; }
|
||||
public List<User> MentionedUsers { get; }
|
||||
|
||||
public IReadOnlyList<Channel> MentionedChannels { get; }
|
||||
public List<Role> MentionedRoles { get; }
|
||||
|
||||
public List<Channel> MentionedChannels { get; }
|
||||
|
||||
public Message(string id, string channelId, MessageType type,
|
||||
User author, DateTime timeStamp,
|
||||
DateTime? editedTimeStamp, string content,
|
||||
IReadOnlyList<Attachment> attachments, IReadOnlyList<User> mentionedUsers,
|
||||
IReadOnlyList<Role> mentionedRoles, IReadOnlyList<Channel> mentionedChannels)
|
||||
IReadOnlyList<Attachment> attachments, IReadOnlyList<Embed> embeds,
|
||||
List<User> mentionedUsers, List<Role> mentionedRoles,
|
||||
List<Channel> mentionedChannels)
|
||||
{
|
||||
Id = id;
|
||||
ChannelId = channelId;
|
||||
|
@ -41,6 +44,7 @@ namespace DiscordChatExporter.Core.Models
|
|||
EditedTimeStamp = editedTimeStamp;
|
||||
Content = content;
|
||||
Attachments = attachments;
|
||||
Embeds = embeds;
|
||||
MentionedUsers = mentionedUsers;
|
||||
MentionedRoles = mentionedRoles;
|
||||
MentionedChannels = mentionedChannels;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class User
|
||||
public partial class User
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
|
@ -33,4 +33,12 @@ namespace DiscordChatExporter.Core.Models
|
|||
return FullName;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class User
|
||||
{
|
||||
public static User CreateUnknownUser(string id)
|
||||
{
|
||||
return new User(id, 0, "Unknown", null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,142 +1,76 @@
|
|||
body {
|
||||
background-color: #36393E;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-family: Whitney, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0096CF;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.pre {
|
||||
background-color: #2F3136;
|
||||
color: rgb(131, 148, 150);
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
span.pre {
|
||||
background-color: #2F3136;
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
div#info {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
div#log {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img.guild-icon {
|
||||
max-height: 64px;
|
||||
max-width: 64px;
|
||||
}
|
||||
|
||||
div.info-right {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
div.guild-name {
|
||||
color: #FFFFFF;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
div.channel-name {
|
||||
color: #FFFFFF;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
div.channel-topic {
|
||||
margin-top: 2px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
div.channel-messagecount {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
div.msg {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||
display: flex;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
padding-bottom: 15px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
div.msg-left {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
img.msg-avatar {
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
div.msg-right {
|
||||
flex: 1;
|
||||
margin-left: 20px;
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
span.msg-user {
|
||||
color: #FFFFFF;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
span.msg-date {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
font-size: .75em;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
span.msg-edited {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
font-size: .8em;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
div.msg-content {
|
||||
font-size: .9375em;
|
||||
padding-top: 5px;
|
||||
word-wrap: break-word;
|
||||
.embed-wrapper .embed-color-pill {
|
||||
background-color: #4f545c
|
||||
}
|
||||
|
||||
div.msg-attachment {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
.embed {
|
||||
background-color: rgba(46, 48, 54, .3);
|
||||
border-color: rgba(46, 48, 54, .6)
|
||||
}
|
||||
|
||||
img.msg-attachment {
|
||||
max-height: 500px;
|
||||
max-width: 50%;
|
||||
.embed .embed-footer,
|
||||
.embed .embed-provider {
|
||||
color: hsla(0, 0%, 100%, .6)
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
vertical-align: -.4em;
|
||||
.embed .embed-author-name {
|
||||
color: #fff!important
|
||||
}
|
||||
|
||||
span.mention {
|
||||
font-weight: 600;
|
||||
.embed div.embed-title {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.embed .embed-description,
|
||||
.embed .embed-fields {
|
||||
color: hsla(0, 0%, 100%, .6)
|
||||
}
|
||||
|
||||
.embed .embed-fields .embed-field-name {
|
||||
color: #fff
|
||||
}
|
|
@ -1,142 +1,45 @@
|
|||
body {
|
||||
background-color: #FFFFFF;
|
||||
color: #737F8D;
|
||||
font-family: Whitney, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B0F4;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.pre {
|
||||
background-color: #F9F9F9;
|
||||
color: rgb(101, 123, 131);
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
span.pre {
|
||||
background-color: #F9F9F9;
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
div#info {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
div#log {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img.guild-icon {
|
||||
max-height: 64px;
|
||||
max-width: 64px;
|
||||
}
|
||||
|
||||
div.info-right {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
div.guild-name {
|
||||
color: #2F3136;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
div.channel-name {
|
||||
color: #2F3136;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
div.channel-topic {
|
||||
margin-top: 2px;
|
||||
color: #2F3136;
|
||||
}
|
||||
|
||||
div.channel-messagecount {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
div.msg {
|
||||
border-top: 1px solid #ECEEEF;
|
||||
display: flex;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
padding-bottom: 15px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
div.msg-left {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
img.msg-avatar {
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
div.msg-right {
|
||||
flex: 1;
|
||||
margin-left: 20px;
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
span.msg-user {
|
||||
color: #2F3136;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
span.msg-date {
|
||||
color: #99AAB5;
|
||||
font-size: .75em;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
span.msg-edited {
|
||||
color: #99AAB5;
|
||||
font-size: .8em;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
div.msg-content {
|
||||
font-size: .9375em;
|
||||
padding-top: 5px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
div.msg-attachment {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
img.msg-attachment {
|
||||
max-height: 500px;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
vertical-align: -.4em;
|
||||
}
|
||||
|
||||
span.mention {
|
||||
font-weight: 600;
|
||||
}
|
396
DiscordChatExporter.Core/Resources/ExportService/Shared.css
Normal file
396
DiscordChatExporter.Core/Resources/ExportService/Shared.css
Normal file
|
@ -0,0 +1,396 @@
|
|||
body {
|
||||
font-family: Whitney, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.pre {
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
span.pre {
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
div#info {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
div#log {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img.guild-icon {
|
||||
max-height: 64px;
|
||||
max-width: 64px;
|
||||
}
|
||||
|
||||
div.info-right {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
div.guild-name {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
div.channel-name {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
div.channel-topic {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
div.channel-messagecount {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
div.msg {
|
||||
display: flex;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
padding-bottom: 15px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
div.msg-left {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
img.msg-avatar {
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
div.msg-right {
|
||||
flex: 1;
|
||||
margin-left: 20px;
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
span.msg-user {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
span.msg-date {
|
||||
font-size: .75em;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
span.msg-edited {
|
||||
font-size: .8em;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
div.msg-content {
|
||||
font-size: .9375em;
|
||||
padding-top: 5px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
div.msg-attachment {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
img.msg-attachment {
|
||||
max-height: 500px;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
span.mention {
|
||||
font-weight: 600;
|
||||
color: #7289da;
|
||||
background-color: rgba(115, 139, 215, 0.1);
|
||||
}
|
||||
|
||||
.emoji {
|
||||
-o-object-fit: contain;
|
||||
object-fit: contain;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 .05em 0 .1em!important;
|
||||
vertical-align: -.4em
|
||||
}
|
||||
|
||||
.emoji.jumboable {
|
||||
width: 32px;
|
||||
height: 32px
|
||||
}
|
||||
|
||||
.image {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text
|
||||
}
|
||||
|
||||
.embed,
|
||||
.embed-wrapper {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox
|
||||
}
|
||||
|
||||
.embed-wrapper {
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
max-width: 520px;
|
||||
display: flex
|
||||
}
|
||||
|
||||
.embed-wrapper .embed-color-pill {
|
||||
width: 4px;
|
||||
background: #cacbce;
|
||||
border-radius: 3px 0 0 3px;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.embed {
|
||||
padding: 8px 10px;
|
||||
box-sizing: border-box;
|
||||
background: hsla(0, 0%, 98%, .3);
|
||||
border: 1px solid hsla(0, 0%, 80%, .3);
|
||||
border-radius: 0 3px 3px 0;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.embed .embed-content,
|
||||
.embed.embed-rich {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox
|
||||
}
|
||||
|
||||
.embed .embed-fields,
|
||||
.embed.embed-link {
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal
|
||||
}
|
||||
|
||||
.embed div.embed-title {
|
||||
color: #4f545c
|
||||
}
|
||||
|
||||
.embed .embed-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.embed .embed-content .embed-content-inner {
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.embed.embed-rich {
|
||||
position: relative;
|
||||
display: flex;
|
||||
border-radius: 0 3px 3px 0
|
||||
}
|
||||
|
||||
.embed.embed-rich .embed-rich-thumb {
|
||||
max-height: 80px;
|
||||
max-width: 80px;
|
||||
border-radius: 3px;
|
||||
width: auto;
|
||||
-o-object-fit: contain;
|
||||
object-fit: contain;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
margin-left: 20px
|
||||
}
|
||||
|
||||
.embed.embed-inline {
|
||||
padding: 0;
|
||||
margin: 4px 0;
|
||||
border-radius: 3px
|
||||
}
|
||||
|
||||
.embed .image,
|
||||
.embed video {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border-radius: 2px
|
||||
}
|
||||
|
||||
.embed .embed-content-inner>:last-child,
|
||||
.embed .embed-content:last-child,
|
||||
.embed .embed-inner>:last-child,
|
||||
.embed>:last-child {
|
||||
margin-bottom: 0!important
|
||||
}
|
||||
|
||||
.embed .embed-provider {
|
||||
display: inline-block;
|
||||
color: #87909c;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px
|
||||
}
|
||||
|
||||
.embed .embed-author {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
margin-bottom: 5px
|
||||
}
|
||||
|
||||
.embed .embed-author-name,
|
||||
.embed .embed-footer,
|
||||
.embed .embed-title {
|
||||
display: inline-block;
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.embed .embed-author-name {
|
||||
font-size: 14px;
|
||||
color: #4f545c!important
|
||||
}
|
||||
|
||||
.embed .embed-author-icon {
|
||||
margin-right: 9px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
-o-object-fit: contain;
|
||||
object-fit: contain;
|
||||
border-radius: 50%
|
||||
}
|
||||
|
||||
.embed .embed-footer {
|
||||
font-size: 12px;
|
||||
color: rgba(79, 83, 91, .6);
|
||||
letter-spacing: 0
|
||||
}
|
||||
|
||||
.embed .embed-footer-icon {
|
||||
margin-right: 10px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
-o-object-fit: contain;
|
||||
object-fit: contain;
|
||||
float: left;
|
||||
border-radius: 2.45px
|
||||
}
|
||||
|
||||
.embed .embed-title {
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
.embed .embed-title+.embed-description {
|
||||
margin-top: -3px!important
|
||||
}
|
||||
|
||||
.embed .embed-description {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
color: rgba(79, 83, 91, .9);
|
||||
letter-spacing: 0
|
||||
}
|
||||
|
||||
.embed .embed-description.markup {
|
||||
white-space: pre-line;
|
||||
margin-top: 0!important;
|
||||
font-size: 14px!important;
|
||||
line-height: 16px!important
|
||||
}
|
||||
|
||||
.embed .embed-description.markup pre {
|
||||
max-width: 100%!important
|
||||
}
|
||||
|
||||
.embed .embed-fields {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
color: #36393e;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.embed .embed-fields .embed-field {
|
||||
-webkit-box-flex: 0;
|
||||
-ms-flex: 0;
|
||||
flex: 0;
|
||||
padding-top: 10px;
|
||||
min-width: 100%;
|
||||
max-width: 506px
|
||||
}
|
||||
|
||||
.embed .embed-fields .embed-field.embed-field-inline {
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
-ms-flex-preferred-size: auto;
|
||||
flex-basis: auto
|
||||
}
|
||||
|
||||
.embed .embed-fields .embed-field .embed-field-name {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.embed .embed-fields .embed-field .embed-field-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500
|
||||
}
|
||||
|
||||
.embed .embed-thumbnail,
|
||||
.embed .embed-thumbnail-gifv {
|
||||
position: relative;
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.embed .embed-thumbnail {
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.embed .embed-thumbnail img {
|
||||
margin: 0;
|
||||
max-width: 500px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.comment>:last-child .embed {
|
||||
margin-bottom: auto
|
||||
}
|
|
@ -8,6 +8,8 @@ using DiscordChatExporter.Core.Exceptions;
|
|||
using DiscordChatExporter.Core.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Tyrrrz.Extensions;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
|
@ -16,6 +18,7 @@ namespace DiscordChatExporter.Core.Services
|
|||
private const string ApiRoot = "https://discordapp.com/api/v6";
|
||||
|
||||
private readonly HttpClient _httpClient = new HttpClient();
|
||||
private readonly Dictionary<string, User> _userCache = new Dictionary<string, User>();
|
||||
private readonly Dictionary<string, Role> _roleCache = new Dictionary<string, Role>();
|
||||
private readonly Dictionary<string, Channel> _channelCache = new Dictionary<string, Channel>();
|
||||
|
||||
|
@ -76,6 +79,113 @@ namespace DiscordChatExporter.Core.Services
|
|||
return new Channel(id, guildId, name, topic, type);
|
||||
}
|
||||
|
||||
private Embed ParseEmbed(JToken token)
|
||||
{
|
||||
|
||||
// var embedFileSize = embedJson["size"].Value<long>();
|
||||
var title = token["title"]?.Value<string>();
|
||||
var type = token["type"]?.Value<string>();
|
||||
var description = token["description"]?.Value<string>();
|
||||
var url = token["url"]?.Value<string>();
|
||||
var timestamp = token["timestamp"]?.Value<DateTime>();
|
||||
var color = token["color"] != null
|
||||
? Color.FromArgb(token["color"].Value<int>())
|
||||
: (Color?)null;
|
||||
|
||||
var footerNode = token["footer"];
|
||||
var footer = footerNode != null
|
||||
? new EmbedFooter(
|
||||
footerNode["text"]?.Value<string>(),
|
||||
footerNode["icon_url"]?.Value<string>(),
|
||||
footerNode["proxy_icon_url"]?.Value<string>())
|
||||
: null;
|
||||
|
||||
var imageNode = token["image"];
|
||||
var image = imageNode != null
|
||||
? new EmbedImage(
|
||||
imageNode["url"]?.Value<string>(),
|
||||
imageNode["proxy_url"]?.Value<string>(),
|
||||
imageNode["height"]?.Value<int>(),
|
||||
imageNode["width"]?.Value<int>())
|
||||
: null;
|
||||
|
||||
var thumbnailNode = token["thumbnail"];
|
||||
var thumbnail = thumbnailNode != null
|
||||
? new EmbedImage(
|
||||
thumbnailNode["url"]?.Value<string>(),
|
||||
thumbnailNode["proxy_url"]?.Value<string>(),
|
||||
thumbnailNode["height"]?.Value<int>(),
|
||||
thumbnailNode["width"]?.Value<int>())
|
||||
: null;
|
||||
|
||||
var videoNode = token["video"];
|
||||
var video = videoNode != null
|
||||
? new EmbedVideo(
|
||||
videoNode["url"]?.Value<string>(),
|
||||
videoNode["height"]?.Value<int>(),
|
||||
videoNode["width"]?.Value<int>())
|
||||
: null;
|
||||
|
||||
var providerNode = token["provider"];
|
||||
var provider = providerNode != null
|
||||
? new EmbedProvider(
|
||||
providerNode["name"]?.Value<string>(),
|
||||
providerNode["url"]?.Value<string>())
|
||||
: null;
|
||||
|
||||
var authorNode = token["author"];
|
||||
var author = authorNode != null
|
||||
? new EmbedAuthor(
|
||||
authorNode["name"]?.Value<string>(),
|
||||
authorNode["url"]?.Value<string>(),
|
||||
authorNode["icon_url"]?.Value<string>(),
|
||||
authorNode["proxy_icon_url"]?.Value<string>())
|
||||
: null;
|
||||
|
||||
var fields = new List<EmbedField>();
|
||||
foreach (var fieldNode in token["fields"].EmptyIfNull())
|
||||
{
|
||||
fields.Add(new EmbedField(
|
||||
fieldNode["name"]?.Value<string>(),
|
||||
fieldNode["value"]?.Value<string>(),
|
||||
fieldNode["inline"]?.Value<bool>()));
|
||||
}
|
||||
|
||||
var mentionableContent = description ?? "";
|
||||
fields.ForEach(f => mentionableContent += f.Value);
|
||||
|
||||
// Get user mentions
|
||||
var mentionedUsers = Regex.Matches(mentionableContent, "<@!?(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.Select(i => _userCache.GetOrDefault(i) ?? User.CreateUnknownUser(i))
|
||||
.ToList();
|
||||
|
||||
// Get role mentions
|
||||
var mentionedRoles = Regex.Matches(mentionableContent, "<@&(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.Select(i => _roleCache.GetOrDefault(i) ?? Role.CreateDeletedRole(i))
|
||||
.ToList();
|
||||
|
||||
// Get channel mentions
|
||||
var mentionedChannels = Regex.Matches(mentionableContent, "<#(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.Select(i => _channelCache.GetOrDefault(i) ?? Channel.CreateDeletedChannel(i))
|
||||
.ToList();
|
||||
|
||||
return new Embed(
|
||||
title, type, description,
|
||||
url, timestamp, color,
|
||||
footer, image, thumbnail,
|
||||
video, provider, author,
|
||||
fields, mentionedUsers, mentionedRoles, mentionedChannels);
|
||||
}
|
||||
|
||||
private Message ParseMessage(JToken token)
|
||||
{
|
||||
// Get basic data
|
||||
|
@ -123,27 +233,64 @@ namespace DiscordChatExporter.Core.Services
|
|||
attachments.Add(attachment);
|
||||
}
|
||||
|
||||
// Get embeds
|
||||
var embeds = token["embeds"].EmptyIfNull().Select(ParseEmbed).ToArray();
|
||||
|
||||
// Get user mentions
|
||||
var mentionedUsers = token["mentions"].Select(ParseUser).ToArray();
|
||||
var mentionedUsers = token["mentions"].Select(ParseUser).ToList();
|
||||
|
||||
// Get role mentions
|
||||
var mentionedRoles = token["mention_roles"]
|
||||
.Values<string>()
|
||||
.Select(i => _roleCache.GetOrDefault(i) ?? Role.CreateDeletedRole(id))
|
||||
.ToArray();
|
||||
.Select(i => _roleCache.GetOrDefault(i) ?? Role.CreateDeletedRole(i))
|
||||
.ToList();
|
||||
|
||||
// Get channel mentions
|
||||
var mentionedChannels = Regex.Matches(content, "<#(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.Select(i => _channelCache.GetOrDefault(i) ?? Channel.CreateDeletedChannel(id))
|
||||
.ToArray();
|
||||
.Select(i => _channelCache.GetOrDefault(i) ?? Channel.CreateDeletedChannel(i))
|
||||
.ToList();
|
||||
|
||||
return new Message(id, channelId, type, author, timeStamp, editedTimeStamp, content, attachments,
|
||||
return new Message(id, channelId, type, author, timeStamp, editedTimeStamp, content, attachments, embeds,
|
||||
mentionedUsers, mentionedRoles, mentionedChannels);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to query for users, channels, and roles if they havent been found yet, and set them in the mentionable
|
||||
/// </summary>
|
||||
private async Task FillMentionable(string token, string guildId, IMentionable mentionable)
|
||||
{
|
||||
for (int i = 0; i < mentionable.MentionedUsers.Count; i++)
|
||||
{
|
||||
var user = mentionable.MentionedUsers[i];
|
||||
if (user.Name == "Unknown" && user.Discriminator == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
mentionable.MentionedUsers[i] = _userCache.GetOrDefault(user.Id) ?? (await GetMemberAsync(token, guildId, user.Id));
|
||||
}
|
||||
catch (HttpErrorStatusCodeException e) { } // This likely means the user doesnt exist any more, so ignore
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < mentionable.MentionedChannels.Count; i++)
|
||||
{
|
||||
var channel = mentionable.MentionedChannels[i];
|
||||
if (channel.Name == "deleted-channel" && channel.GuildId == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
mentionable.MentionedChannels[i] = _channelCache.GetOrDefault(channel.Id) ?? (await GetChannelAsync(token, channel.Id));
|
||||
}
|
||||
catch (HttpErrorStatusCodeException e) { } // This likely means the user doesnt exist any more, so ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Roles are already gotten via GetGuildRolesAsync at the start
|
||||
}
|
||||
|
||||
private async Task<string> GetStringAsync(string url)
|
||||
{
|
||||
using (var response = await _httpClient.GetAsync(url))
|
||||
|
@ -193,6 +340,23 @@ namespace DiscordChatExporter.Core.Services
|
|||
return channel;
|
||||
}
|
||||
|
||||
public async Task<User> GetMemberAsync(string token, string guildId, string memberId)
|
||||
{
|
||||
// Form request url
|
||||
var url = $"{ApiRoot}/guilds/{guildId}/members/{memberId}?token={token}";
|
||||
|
||||
// Get response
|
||||
var content = await GetStringAsync(url);
|
||||
|
||||
// Parse
|
||||
var user = ParseUser(JToken.Parse(content)["user"]);
|
||||
|
||||
// Add user to cache
|
||||
_userCache[user.Id] = user;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string token, string guildId)
|
||||
{
|
||||
// Form request url
|
||||
|
@ -211,6 +375,25 @@ namespace DiscordChatExporter.Core.Services
|
|||
return channels;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IReadOnlyList<Role>> GetGuildRolesAsync(string token, string guildId)
|
||||
{
|
||||
// Form request url
|
||||
var url = $"{ApiRoot}/guilds/{guildId}/roles?token={token}";
|
||||
|
||||
// Get response
|
||||
var content = await GetStringAsync(url);
|
||||
|
||||
// Parse
|
||||
var roles = JArray.Parse(content).Select(ParseRole).ToArray();
|
||||
|
||||
// Add roles to cache
|
||||
foreach (var role in roles)
|
||||
_roleCache[role.Id] = role;
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Guild>> GetUserGuildsAsync(string token)
|
||||
{
|
||||
// Form request url
|
||||
|
@ -247,9 +430,60 @@ namespace DiscordChatExporter.Core.Services
|
|||
return channels;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<User>> GetGuildMembersAsync(string token, string guildId)
|
||||
{
|
||||
var result = new List<User>();
|
||||
|
||||
var afterId = "";
|
||||
while (true)
|
||||
{
|
||||
// Form request url
|
||||
var url = $"{ApiRoot}/guilds/{guildId}/members?token={token}&limit=1000";
|
||||
if (afterId.IsNotBlank())
|
||||
url += $"&after={afterId}";
|
||||
|
||||
// Get response
|
||||
var content = await GetStringAsync(url);
|
||||
|
||||
// Parse
|
||||
var users = JArray.Parse(content).Select(m => ParseUser(m["user"]));
|
||||
|
||||
// Add user to cache
|
||||
foreach (var user in users)
|
||||
_userCache[user.Id] = user;
|
||||
|
||||
// Add users to list
|
||||
string currentUserId = null;
|
||||
foreach (var user in users)
|
||||
{
|
||||
// Add user
|
||||
result.Add(user);
|
||||
if (currentUserId == null || BigInteger.Parse(user.Id) > BigInteger.Parse(currentUserId))
|
||||
currentUserId = user.Id;
|
||||
}
|
||||
|
||||
// If no users - break
|
||||
if (currentUserId == null)
|
||||
break;
|
||||
|
||||
// Otherwise offset the next request
|
||||
afterId = currentUserId;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
|
||||
DateTime? from, DateTime? to)
|
||||
{
|
||||
Channel channel = await GetChannelAsync(token, channelId);
|
||||
|
||||
try
|
||||
{
|
||||
await GetGuildRolesAsync(token, channel.GuildId);
|
||||
}
|
||||
catch (HttpErrorStatusCodeException e) { } // This will be thrown if the user doesnt have the MANAGE_ROLES permission for the guild
|
||||
|
||||
var result = new List<Message>();
|
||||
|
||||
// We are going backwards from last message to first
|
||||
|
@ -295,6 +529,13 @@ namespace DiscordChatExporter.Core.Services
|
|||
// Messages appear newest first, we need to reverse
|
||||
result.Reverse();
|
||||
|
||||
foreach (var message in result)
|
||||
{
|
||||
await FillMentionable(token, channel.GuildId, message);
|
||||
foreach (var embed in message.Embeds)
|
||||
await FillMentionable(token, channel.GuildId, embed);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,17 +3,19 @@ using System.Text.RegularExpressions;
|
|||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Tyrrrz.Extensions;
|
||||
using System.Drawing;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class ExportService
|
||||
{
|
||||
private string FormatMessageContentHtml(Message message)
|
||||
private string MarkdownToHtml(string content, IMentionable mentionable = null, bool allowLinks = false)
|
||||
{
|
||||
// A lot of these regexes were inspired by or taken from MarkdownSharp
|
||||
|
||||
var content = message.Content;
|
||||
|
||||
// HTML-encode content
|
||||
content = HtmlEncode(content);
|
||||
|
||||
|
@ -29,7 +31,7 @@ namespace DiscordChatExporter.Core.Services
|
|||
|
||||
// Encode URLs
|
||||
content = Regex.Replace(content,
|
||||
@"((https?|ftp)://[-a-zA-Z0-9+&@#/%?=~_|!:,\.\[\]\(\);]*[-a-zA-Z0-9+&@#/%=~_|\[\])])(?=$|\W)",
|
||||
@"(\b(?:(?:https?|ftp|file)://|www\.|ftp\.)(?:\([-a-zA-Z0-9+&@#/%?=~_|!:,\.\[\];]*\)|[-a-zA-Z0-9+&@#/%?=~_|!:,\.\[\];])*(?:\([-a-zA-Z0-9+&@#/%?=~_|!:,\.\[\];]*\)|[-a-zA-Z0-9+&@#/%=~_|$]))",
|
||||
m => $"\x1AL{Base64Encode(m.Groups[1].Value)}\x1AL");
|
||||
|
||||
// Process bold (**text**)
|
||||
|
@ -52,6 +54,12 @@ namespace DiscordChatExporter.Core.Services
|
|||
content = Regex.Replace(content, "\x1AI(.*?)\x1AI",
|
||||
m => $"<span class=\"pre\">{Base64Decode(m.Groups[1].Value)}</span>");
|
||||
|
||||
if (allowLinks)
|
||||
{
|
||||
content = Regex.Replace(content, "\\[([^\\]]+)\\]\\(\x1AL(.*?)\x1AL\\)",
|
||||
m => $"<a href=\"{Base64Decode(m.Groups[2].Value)}\">{m.Groups[1].Value}</a>");
|
||||
}
|
||||
|
||||
// Decode and process URLs
|
||||
content = Regex.Replace(content, "\x1AL(.*?)\x1AL",
|
||||
m => $"<a href=\"{Base64Decode(m.Groups[1].Value)}\">{Base64Decode(m.Groups[1].Value)}</a>");
|
||||
|
@ -65,31 +73,34 @@ namespace DiscordChatExporter.Core.Services
|
|||
// Meta mentions (@here)
|
||||
content = content.Replace("@here", "<span class=\"mention\">@here</span>");
|
||||
|
||||
// User mentions (<@id> and <@!id>)
|
||||
foreach (var mentionedUser in message.MentionedUsers)
|
||||
if (mentionable != null)
|
||||
{
|
||||
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>",
|
||||
$"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser.FullName)}\">" +
|
||||
$"@{HtmlEncode(mentionedUser.Name)}" +
|
||||
"</span>");
|
||||
}
|
||||
// User mentions (<@id> and <@!id>)
|
||||
foreach (var mentionedUser in mentionable.MentionedUsers)
|
||||
{
|
||||
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>",
|
||||
$"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser.FullName)}\">" +
|
||||
$"@{HtmlEncode(mentionedUser.Name)}" +
|
||||
"</span>");
|
||||
}
|
||||
|
||||
// Role mentions (<@&id>)
|
||||
foreach (var mentionedRole in message.MentionedRoles)
|
||||
{
|
||||
content = content.Replace($"<@&{mentionedRole.Id}>",
|
||||
"<span class=\"mention\">" +
|
||||
$"@{HtmlEncode(mentionedRole.Name)}" +
|
||||
"</span>");
|
||||
}
|
||||
// Role mentions (<@&id>)
|
||||
foreach (var mentionedRole in mentionable.MentionedRoles)
|
||||
{
|
||||
content = content.Replace($"<@&{mentionedRole.Id}>",
|
||||
"<span class=\"mention\">" +
|
||||
$"@{HtmlEncode(mentionedRole.Name)}" +
|
||||
"</span>");
|
||||
}
|
||||
|
||||
// Channel mentions (<#id>)
|
||||
foreach (var mentionedChannel in message.MentionedChannels)
|
||||
{
|
||||
content = content.Replace($"<#{mentionedChannel.Id}>",
|
||||
"<span class=\"mention\">" +
|
||||
$"#{HtmlEncode(mentionedChannel.Name)}" +
|
||||
"</span>");
|
||||
// Channel mentions (<#id>)
|
||||
foreach (var mentionedChannel in mentionable.MentionedChannels)
|
||||
{
|
||||
content = content.Replace($"<#{mentionedChannel.Id}>",
|
||||
"<span class=\"mention\">" +
|
||||
$"#{HtmlEncode(mentionedChannel.Name)}" +
|
||||
"</span>");
|
||||
}
|
||||
}
|
||||
|
||||
// Custom emojis (<:name:id>)
|
||||
|
@ -99,6 +110,145 @@ namespace DiscordChatExporter.Core.Services
|
|||
return content;
|
||||
}
|
||||
|
||||
private string FormatMessageContentHtml(Message message)
|
||||
{
|
||||
return MarkdownToHtml(message.Content, message);
|
||||
}
|
||||
|
||||
// The code used to convert embeds to html was based heavily off of the Embed Visualizer project, from this file:
|
||||
// https://github.com/leovoel/embed-visualizer/blob/master/src/components/embed.jsx
|
||||
|
||||
private string EmbedColorPillToHtml(Color? color)
|
||||
{
|
||||
string backgroundColor = "";
|
||||
|
||||
if (color != null)
|
||||
backgroundColor = $"rgba({color?.R},{color?.G},{color?.B},1)";
|
||||
|
||||
return $"<div class='embed-color-pill' style='background-color: {backgroundColor}'></div>";
|
||||
}
|
||||
|
||||
private string EmbedTitleToHtml(string title, string url)
|
||||
{
|
||||
if (title == null)
|
||||
return null;
|
||||
|
||||
string computed = $"<div class='embed-title'>{MarkdownToHtml(title)}</div>";
|
||||
if (url != null)
|
||||
computed = $"<a target='_blank' rel='noreferrer' href='{url}' class='embed-title'>{MarkdownToHtml(title)}</a>";
|
||||
|
||||
return computed;
|
||||
}
|
||||
|
||||
private string EmbedDescriptionToHtml(string content, IMentionable mentionable)
|
||||
{
|
||||
if (content == null)
|
||||
return null;
|
||||
|
||||
return $"<div class='embed-description markup'>{MarkdownToHtml(content, mentionable, true)}</div>";
|
||||
}
|
||||
|
||||
private string EmbedAuthorToHtml(string name, string url, string icon_url)
|
||||
{
|
||||
if (name == null)
|
||||
return null;
|
||||
|
||||
string authorName = null;
|
||||
if (name != null)
|
||||
{
|
||||
authorName = $"<span class='embed-author-name'>{name}</span>";
|
||||
if (url != null)
|
||||
authorName = $"<a target='_blank' rel='noreferrer' href='{url}' class='embed-author-name'>{name}</a>";
|
||||
}
|
||||
|
||||
string authorIcon = icon_url != null ? $"<img src='{icon_url}' role='presentation' class='embed-author-icon' />" : null;
|
||||
|
||||
return $"<div class='embed-author'>{authorIcon}{authorName}</div>";
|
||||
}
|
||||
|
||||
private string EmbedFieldToHtml(string name, string value, bool? inline, IMentionable mentionable)
|
||||
{
|
||||
if (name == null && value == null)
|
||||
return null;
|
||||
|
||||
string cls = "embed-field" + (inline == true ? " embed-field-inline" : "");
|
||||
|
||||
string fieldName = name != null ? $"<div class='embed-field-name'>{MarkdownToHtml(name)}</div>" : null;
|
||||
string fieldValue = value != null ? $"<div class='embed-field-value markup'>{MarkdownToHtml(value, mentionable, true)}</div>" : null;
|
||||
|
||||
return $"<div class='{cls}'>{fieldName}{fieldValue}</div>";
|
||||
}
|
||||
|
||||
private string EmbedThumbnailToHtml(string url)
|
||||
{
|
||||
if (url == null)
|
||||
return null;
|
||||
|
||||
return $@"
|
||||
<img
|
||||
src = '{url}'
|
||||
role = 'presentation'
|
||||
class='embed-rich-thumb'
|
||||
style='max-width: 80px; max-height: 80px'
|
||||
/>";
|
||||
}
|
||||
|
||||
private string EmbedImageToHtml(string url)
|
||||
{
|
||||
if (url == null)
|
||||
return null;
|
||||
|
||||
return $"<a class='embed-thumbnail embed-thumbnail-rich'><img class='image' role='presentation' src='{url}' /></a>";
|
||||
}
|
||||
|
||||
private string EmbedFooterToHtml(DateTime? timestamp, string text, string icon_url)
|
||||
{
|
||||
if (text == null && timestamp == null)
|
||||
return null;
|
||||
|
||||
// format: ddd MMM Do, YYYY [at] h:mm A
|
||||
|
||||
string time = timestamp != null ? HtmlEncode(timestamp?.ToString(_settingsService.DateFormat)) : null;
|
||||
|
||||
string footerText = string.Join(" | ", new List<string> { text, time }.Where(s => s != null));
|
||||
string footerIcon = text != null && icon_url != null
|
||||
? $"<img src='{icon_url}' class='embed-footer-icon' role='presentation' width='20' height='20' />"
|
||||
: null;
|
||||
|
||||
return $"<div>{footerIcon}<span class='embed-footer'>{footerText}</span></div>";
|
||||
}
|
||||
|
||||
private string EmbedFieldsToHtml(IReadOnlyList<EmbedField> fields, IMentionable mentionable)
|
||||
{
|
||||
if (fields.Count == 0)
|
||||
return null;
|
||||
|
||||
return $"<div class='embed-fields'>{string.Join("", fields.Select(f => EmbedFieldToHtml(f.Name, f.Value, f.Inline, mentionable)))}</div>";
|
||||
}
|
||||
|
||||
private string FormatEmbedHtml(Embed embed)
|
||||
{
|
||||
return $@"
|
||||
<div class='accessory'>
|
||||
<div class='embed-wrapper'>
|
||||
{EmbedColorPillToHtml(embed.Color)}
|
||||
<div class='embed embed-rich'>
|
||||
<div class='embed-content'>
|
||||
<div class='embed-content-inner'>
|
||||
{EmbedAuthorToHtml(embed.Author?.Name, embed.Author?.Url, embed.Author?.IconUrl)}
|
||||
{EmbedTitleToHtml(embed.Title, embed.Url)}
|
||||
{EmbedDescriptionToHtml(embed.Description, embed)}
|
||||
{EmbedFieldsToHtml(embed.Fields, embed)}
|
||||
</div>
|
||||
{EmbedThumbnailToHtml(embed.Thumbnail?.Url)}
|
||||
</div>
|
||||
{EmbedImageToHtml(embed.Image?.Url)}
|
||||
{EmbedFooterToHtml(embed.TimeStamp, embed.Footer?.Text, embed.Footer?.IconUrl)}
|
||||
</div>
|
||||
</div>
|
||||
</div>";
|
||||
}
|
||||
|
||||
private async Task ExportAsHtmlAsync(ChannelChatLog log, TextWriter output, string css)
|
||||
{
|
||||
// Generation info
|
||||
|
@ -193,6 +343,13 @@ namespace DiscordChatExporter.Core.Services
|
|||
await output.WriteLineAsync("</div>");
|
||||
}
|
||||
}
|
||||
|
||||
// Embeds
|
||||
foreach (var embed in message.Embeds)
|
||||
{
|
||||
var contentFormatted = FormatEmbedHtml(embed);
|
||||
await output.WriteAsync(contentFormatted);
|
||||
}
|
||||
}
|
||||
|
||||
await output.WriteLineAsync("</div>"); // msg-right
|
||||
|
|
|
@ -22,25 +22,25 @@ namespace DiscordChatExporter.Core.Services
|
|||
{
|
||||
using (var output = File.CreateText(filePath))
|
||||
{
|
||||
var sharedCss = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceString("DiscordChatExporter.Core.Resources.ExportService.Shared.css");
|
||||
|
||||
if (format == ExportFormat.PlainText)
|
||||
{
|
||||
await ExportAsPlainTextAsync(log, output);
|
||||
}
|
||||
|
||||
else if (format == ExportFormat.HtmlDark)
|
||||
{
|
||||
var css = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceString("DiscordChatExporter.Core.Resources.ExportService.DarkTheme.css");
|
||||
await ExportAsHtmlAsync(log, output, css);
|
||||
await ExportAsHtmlAsync(log, output, $"{sharedCss}\n{css}");
|
||||
}
|
||||
|
||||
else if (format == ExportFormat.HtmlLight)
|
||||
{
|
||||
var css = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceString("DiscordChatExporter.Core.Resources.ExportService.LightTheme.css");
|
||||
await ExportAsHtmlAsync(log, output, css);
|
||||
await ExportAsHtmlAsync(log, output, $"{sharedCss}\n{css}");
|
||||
}
|
||||
|
||||
else if (format == ExportFormat.Csv)
|
||||
{
|
||||
await ExportAsCsvAsync(log, output);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue