mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-20 09:55:08 -04:00
Architecture refactor (#63)
This commit is contained in:
parent
d958f613a3
commit
481991bd00
52 changed files with 1484 additions and 1846 deletions
|
@ -13,7 +13,7 @@
|
|||
<PackageReference Include="CommonServiceLocator" Version="2.0.3" />
|
||||
<PackageReference Include="FluentCommandLineParser" Version="1.4.3" />
|
||||
<PackageReference Include="MvvmLightLibs" Version="5.4.1" />
|
||||
<PackageReference Include="Tyrrrz.Extensions" Version="1.5.0" />
|
||||
<PackageReference Include="Tyrrrz.Extensions" Version="1.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -38,16 +38,19 @@ namespace DiscordChatExporter.Cli.ViewModels
|
|||
}
|
||||
|
||||
// Get messages
|
||||
var messages = await _dataService.GetChannelMessagesAsync(token, channelId, from, to);
|
||||
var messages = await _dataService.GetChannelMessagesAsync(token, channel.Id, from, to);
|
||||
|
||||
// Group them
|
||||
// Group messages
|
||||
var messageGroups = _messageGroupService.GroupMessages(messages);
|
||||
|
||||
// Get mentionables
|
||||
var mentionables = await _dataService.GetMentionablesAsync(token, guild.Id, messages);
|
||||
|
||||
// Create log
|
||||
var log = new ChannelChatLog(guild, channel, messageGroups, messages.Count);
|
||||
var log = new ChatLog(guild, channel, messageGroups, mentionables);
|
||||
|
||||
// Export
|
||||
await _exportService.ExportAsync(format, filePath, log);
|
||||
_exportService.Export(format, filePath, log);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,24 +6,25 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\ExportService\Shared.css" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\PlainText.txt" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\HtmlDark.html" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\HtmlLight.html" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\Html\Core.html" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\Html\Shared.css" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\Html\DarkTheme.css" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\Html\LightTheme.css" />
|
||||
<EmbeddedResource Include="Resources\ExportTemplates\Csv.csv" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\ExportService\DarkTheme.css" />
|
||||
<EmbeddedResource Include="Resources\ExportService\LightTheme.css" />
|
||||
<EmbeddedResource Include="Resources\ExportService\Shared.css" />
|
||||
<Reference Include="mscorlib" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="7.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="Onova" Version="2.1.0" />
|
||||
<PackageReference Include="Tyrrrz.Extensions" Version="1.5.0" />
|
||||
<PackageReference Include="Scriban" Version="1.2.1" />
|
||||
<PackageReference Include="Tyrrrz.Extensions" Version="1.5.1" />
|
||||
<PackageReference Include="Tyrrrz.Settings" Version="1.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Internal
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
public static string GetManifestResourceString(this Assembly assembly, string resourceName)
|
||||
public static string ToSnowflake(this DateTime dateTime)
|
||||
{
|
||||
var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null)
|
||||
throw new MissingManifestResourceException($"Could not find resource [{resourceName}].");
|
||||
const long epoch = 62135596800000;
|
||||
var unixTime = dateTime.ToUniversalTime().Ticks / TimeSpan.TicksPerMillisecond - epoch;
|
||||
var value = ((ulong) unixTime - 1420070400000UL) << 22;
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
using (stream)
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
public static string Base64Encode(this string str) => str.GetBytes().ToBase64();
|
||||
|
||||
public static string Base64Decode(this string str) => str.FromBase64().GetString();
|
||||
|
||||
public static Color ResetAlpha(this Color color) => Color.FromArgb(1, color);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#attachment-object
|
||||
|
||||
public class Attachment
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
public AttachmentType Type { get; }
|
||||
public bool IsImage { get; }
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
|
@ -12,13 +14,15 @@
|
|||
|
||||
public long FileSize { get; }
|
||||
|
||||
public Attachment(string id, AttachmentType type, string url, string fileName, long fileSize)
|
||||
public Attachment(string id, bool isImage, string url, string fileName, long fileSize)
|
||||
{
|
||||
Id = id;
|
||||
Type = type;
|
||||
IsImage = isImage;
|
||||
Url = url;
|
||||
FileName = fileName;
|
||||
FileSize = fileSize;
|
||||
}
|
||||
|
||||
public override string ToString() => FileName;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public enum AttachmentType
|
||||
{
|
||||
Other,
|
||||
Image
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#channel-object
|
||||
|
||||
public partial class Channel
|
||||
{
|
||||
public string Id { get; }
|
||||
|
@ -21,17 +23,12 @@
|
|||
Type = type;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public partial class Channel
|
||||
{
|
||||
public static Channel CreateDeletedChannel(string id)
|
||||
{
|
||||
return new Channel(id, null, "deleted-channel", null, ChannelType.GuildTextChat);
|
||||
}
|
||||
public static Channel CreateDeletedChannel(string id) =>
|
||||
new Channel(id, null, "deleted-channel", null, ChannelType.GuildTextChat);
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class ChannelChatLog
|
||||
{
|
||||
public Guild Guild { get; }
|
||||
|
||||
public Channel Channel { get; }
|
||||
|
||||
public IReadOnlyList<MessageGroup> MessageGroups { get; }
|
||||
|
||||
public int TotalMessageCount { get; }
|
||||
|
||||
public ChannelChatLog(Guild guild, Channel channel, IReadOnlyList<MessageGroup> messageGroups,
|
||||
int totalMessageCount)
|
||||
{
|
||||
Guild = guild;
|
||||
Channel = channel;
|
||||
MessageGroups = messageGroups;
|
||||
TotalMessageCount = totalMessageCount;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
|
||||
public enum ChannelType
|
||||
{
|
||||
GuildTextChat,
|
||||
|
|
29
DiscordChatExporter.Core/Models/ChatLog.cs
Normal file
29
DiscordChatExporter.Core/Models/ChatLog.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class ChatLog
|
||||
{
|
||||
public Guild Guild { get; }
|
||||
|
||||
public Channel Channel { get; }
|
||||
|
||||
public IReadOnlyList<MessageGroup> MessageGroups { get; }
|
||||
|
||||
public int TotalMessageCount => MessageGroups.Sum(g => g.Messages.Count);
|
||||
|
||||
public Mentionables Mentionables { get; }
|
||||
|
||||
public ChatLog(Guild guild, Channel channel, IReadOnlyList<MessageGroup> messageGroups,
|
||||
Mentionables mentionables)
|
||||
{
|
||||
Guild = guild;
|
||||
Channel = channel;
|
||||
MessageGroups = messageGroups;
|
||||
Mentionables = mentionables;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Guild.Name} | {Channel.Name}";
|
||||
}
|
||||
}
|
|
@ -2,72 +2,47 @@ 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
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object
|
||||
|
||||
public class Embed
|
||||
{
|
||||
public string Title { get; }
|
||||
|
||||
public string Type { get; }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
public DateTime? TimeStamp { 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 Color Color { get; }
|
||||
|
||||
public EmbedAuthor Author { get; }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public IReadOnlyList<EmbedField> Fields { get; }
|
||||
|
||||
public List<User> MentionedUsers { get; }
|
||||
public EmbedImage Thumbnail { get; }
|
||||
|
||||
public List<Role> MentionedRoles { get; }
|
||||
public EmbedImage Image { get; }
|
||||
|
||||
public List<Channel> MentionedChannels { get; }
|
||||
public EmbedFooter Footer { 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)
|
||||
public Embed(string title, string url, DateTime? timestamp, Color color, EmbedAuthor author, string description,
|
||||
IReadOnlyList<EmbedField> fields, EmbedImage thumbnail, EmbedImage image, EmbedFooter footer)
|
||||
{
|
||||
Title = title;
|
||||
Type = type;
|
||||
Description = description;
|
||||
Url = url;
|
||||
TimeStamp = timeStamp;
|
||||
Timestamp = timestamp;
|
||||
Color = color;
|
||||
Footer = footer;
|
||||
Image = image;
|
||||
Thumbnail = thumbnail;
|
||||
Video = video;
|
||||
Provider = provider;
|
||||
Author = author;
|
||||
Description = description;
|
||||
Fields = fields;
|
||||
MentionedUsers = mentionedUsers;
|
||||
MentionedRoles = mentionedRoles;
|
||||
MentionedChannels = mentionedChannels;
|
||||
Thumbnail = thumbnail;
|
||||
Image = image;
|
||||
Footer = footer;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Description;
|
||||
}
|
||||
public override string ToString() => Title;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-author-structure
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-author-structure
|
||||
|
||||
public class EmbedAuthor
|
||||
{
|
||||
public string Name { get; }
|
||||
|
@ -13,19 +10,13 @@ namespace DiscordChatExporter.Core.Models
|
|||
|
||||
public string IconUrl { get; }
|
||||
|
||||
public string ProxyIconUrl { get; }
|
||||
|
||||
public EmbedAuthor(string name, string url, string iconUrl, string proxyIconUrl)
|
||||
public EmbedAuthor(string name, string url, string iconUrl)
|
||||
{
|
||||
Name = name;
|
||||
Url = url;
|
||||
IconUrl = iconUrl;
|
||||
ProxyIconUrl = proxyIconUrl;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
|
@ -1,23 +1,22 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-field-structure
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-field-structure
|
||||
|
||||
public class EmbedField
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public string Value { get; }
|
||||
|
||||
public bool? Inline { get; }
|
||||
public bool IsInline { get; }
|
||||
|
||||
public EmbedField(string name, string value, bool? inline)
|
||||
public EmbedField(string name, string value, bool isInline)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
Inline = inline;
|
||||
}
|
||||
IsInline = isInline;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Name} | {Value}";
|
||||
}
|
||||
}
|
|
@ -1,28 +1,19 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-footer-structure
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-footer-structure
|
||||
|
||||
public class EmbedFooter
|
||||
{
|
||||
public string Text { get; }
|
||||
|
||||
public string IconUrl { get; }
|
||||
|
||||
public string ProxyIconUrl { get; }
|
||||
|
||||
public EmbedFooter(string text, string iconUrl, string proxyIconUrl)
|
||||
public EmbedFooter(string text, string iconUrl)
|
||||
{
|
||||
Text = text;
|
||||
IconUrl = iconUrl;
|
||||
ProxyIconUrl = proxyIconUrl;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Text;
|
||||
}
|
||||
public override string ToString() => Text;
|
||||
}
|
||||
}
|
|
@ -1,24 +1,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-image-structure
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-image-structure
|
||||
|
||||
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)
|
||||
public EmbedImage(string url, int? height, int? width)
|
||||
{
|
||||
Url = url;
|
||||
ProxyUrl = proxyUrl;
|
||||
Height = height;
|
||||
Width = width;
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using Tyrrrz.Extensions;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/guild#guild-object
|
||||
|
||||
public partial class Guild
|
||||
{
|
||||
public string Id { get; }
|
||||
|
@ -15,24 +16,18 @@ namespace DiscordChatExporter.Core.Models
|
|||
? $"https://cdn.discordapp.com/icons/{Id}/{IconHash}.png"
|
||||
: "https://cdn.discordapp.com/embed/avatars/0.png";
|
||||
|
||||
public IReadOnlyList<Role> Roles { get; }
|
||||
|
||||
public Guild(string id, string name, string iconHash, IReadOnlyList<Role> roles)
|
||||
public Guild(string id, string name, string iconHash)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
IconHash = iconHash;
|
||||
Roles = roles;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public partial class Guild
|
||||
{
|
||||
public static Guild DirectMessages { get; } = new Guild("@me", "Direct Messages", null, new Role[0]);
|
||||
public static Guild DirectMessages { get; } = new Guild("@me", "Direct Messages", null);
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
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; }
|
||||
}
|
||||
}
|
30
DiscordChatExporter.Core/Models/Mentionables.cs
Normal file
30
DiscordChatExporter.Core/Models/Mentionables.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class Mentionables
|
||||
{
|
||||
public IReadOnlyList<User> Users { get; }
|
||||
|
||||
public IReadOnlyList<Channel> Channels { get; }
|
||||
|
||||
public IReadOnlyList<Role> Roles { get; }
|
||||
|
||||
public Mentionables(IReadOnlyList<User> users, IReadOnlyList<Channel> channels, IReadOnlyList<Role> roles)
|
||||
{
|
||||
Users = users;
|
||||
Channels = channels;
|
||||
Roles = roles;
|
||||
}
|
||||
|
||||
public User GetUser(string id) =>
|
||||
Users.FirstOrDefault(u => u.Id == id) ?? User.CreateUnknownUser(id);
|
||||
|
||||
public Channel GetChannel(string id) =>
|
||||
Channels.FirstOrDefault(c => c.Id == id) ?? Channel.CreateDeletedChannel(id);
|
||||
|
||||
public Role GetRole(string id) =>
|
||||
Roles.FirstOrDefault(r => r.Id == id) ?? Role.CreateDeletedRole(id);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,9 @@ using System.Collections.Generic;
|
|||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
public class Message : IMentionable
|
||||
// https://discordapp.com/developers/docs/resources/channel#message-object
|
||||
|
||||
public class Message
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
|
@ -13,9 +15,9 @@ namespace DiscordChatExporter.Core.Models
|
|||
|
||||
public User Author { get; }
|
||||
|
||||
public DateTime TimeStamp { get; }
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public DateTime? EditedTimeStamp { get; }
|
||||
public DateTime? EditedTimestamp { get; }
|
||||
|
||||
public string Content { get; }
|
||||
|
||||
|
@ -23,36 +25,24 @@ namespace DiscordChatExporter.Core.Models
|
|||
|
||||
public IReadOnlyList<Embed> Embeds { get; }
|
||||
|
||||
public List<User> MentionedUsers { get; }
|
||||
public IReadOnlyList<User> MentionedUsers { 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<Embed> embeds,
|
||||
List<User> mentionedUsers, List<Role> mentionedRoles,
|
||||
List<Channel> mentionedChannels)
|
||||
public Message(string id, string channelId, MessageType type, User author, DateTime timestamp,
|
||||
DateTime? editedTimestamp, string content, IReadOnlyList<Attachment> attachments,
|
||||
IReadOnlyList<Embed> embeds, IReadOnlyList<User> mentionedUsers)
|
||||
{
|
||||
Id = id;
|
||||
ChannelId = channelId;
|
||||
Type = type;
|
||||
Author = author;
|
||||
TimeStamp = timeStamp;
|
||||
EditedTimeStamp = editedTimeStamp;
|
||||
Timestamp = timestamp;
|
||||
EditedTimestamp = editedTimestamp;
|
||||
Content = content;
|
||||
Attachments = attachments;
|
||||
Embeds = embeds;
|
||||
MentionedUsers = mentionedUsers;
|
||||
MentionedRoles = mentionedRoles;
|
||||
MentionedChannels = mentionedChannels;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Content;
|
||||
}
|
||||
public override string ToString() => Content;
|
||||
}
|
||||
}
|
|
@ -7,15 +7,17 @@ namespace DiscordChatExporter.Core.Models
|
|||
{
|
||||
public User Author { get; }
|
||||
|
||||
public DateTime TimeStamp { get; }
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public IReadOnlyList<Message> Messages { get; }
|
||||
|
||||
public MessageGroup(User author, DateTime timeStamp, IReadOnlyList<Message> messages)
|
||||
public MessageGroup(User author, DateTime timestamp, IReadOnlyList<Message> messages)
|
||||
{
|
||||
Author = author;
|
||||
TimeStamp = timeStamp;
|
||||
Timestamp = timestamp;
|
||||
Messages = messages;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Author.FullName} | {Timestamp} | {Messages.Count} messages";
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/resources/channel#message-object-message-types
|
||||
|
||||
public enum MessageType
|
||||
{
|
||||
Default,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/topics/permissions#role-object
|
||||
|
||||
public partial class Role
|
||||
{
|
||||
public string Id { get; }
|
||||
|
@ -12,17 +14,12 @@
|
|||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public partial class Role
|
||||
{
|
||||
public static Role CreateDeletedRole(string id)
|
||||
{
|
||||
return new Role(id, "deleted-role");
|
||||
}
|
||||
public static Role CreateDeletedRole(string id) =>
|
||||
new Role(id, "deleted-role");
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace DiscordChatExporter.Core.Models
|
||||
{
|
||||
// https://discordapp.com/developers/docs/topics/permissions#role-object
|
||||
|
||||
public partial class User
|
||||
{
|
||||
public string Id { get; }
|
||||
|
@ -28,17 +30,12 @@ namespace DiscordChatExporter.Core.Models
|
|||
AvatarHash = avatarHash;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return FullName;
|
||||
}
|
||||
public override string ToString() => FullName;
|
||||
}
|
||||
|
||||
public partial class User
|
||||
{
|
||||
public static User CreateUnknownUser(string id)
|
||||
{
|
||||
return new User(id, 0, "Unknown", null);
|
||||
}
|
||||
public static User CreateUnknownUser(string id) =>
|
||||
new User(id, 0, "Unknown", null);
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
body {
|
||||
background-color: #36393E;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0096CF;
|
||||
}
|
||||
|
||||
div.pre {
|
||||
background-color: #2F3136;
|
||||
color: rgb(131, 148, 150);
|
||||
}
|
||||
|
||||
span.pre {
|
||||
background-color: #2F3136;
|
||||
}
|
||||
|
||||
div.guild-name {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
div.channel-name {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
div.channel-topic {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
div.msg {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
span.msg-user {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
span.msg-date {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
span.msg-edited {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.embed-wrapper .embed-color-pill {
|
||||
background-color: #4f545c
|
||||
}
|
||||
|
||||
.embed {
|
||||
background-color: rgba(46, 48, 54, .3);
|
||||
border-color: rgba(46, 48, 54, .6)
|
||||
}
|
||||
|
||||
.embed .embed-footer,
|
||||
.embed .embed-provider {
|
||||
color: hsla(0, 0%, 100%, .6)
|
||||
}
|
||||
|
||||
.embed .embed-author-name {
|
||||
color: #fff!important
|
||||
}
|
||||
|
||||
.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,45 +0,0 @@
|
|||
body {
|
||||
background-color: #FFFFFF;
|
||||
color: #737F8D;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B0F4;
|
||||
}
|
||||
|
||||
div.pre {
|
||||
background-color: #F9F9F9;
|
||||
color: rgb(101, 123, 131);
|
||||
}
|
||||
|
||||
span.pre {
|
||||
background-color: #F9F9F9;
|
||||
}
|
||||
|
||||
div.guild-name {
|
||||
color: #2F3136;
|
||||
}
|
||||
|
||||
div.channel-name {
|
||||
color: #2F3136;
|
||||
}
|
||||
|
||||
div.channel-topic {
|
||||
color: #2F3136;
|
||||
}
|
||||
|
||||
div.msg {
|
||||
border-top: 1px solid #ECEEEF;
|
||||
}
|
||||
|
||||
span.msg-user {
|
||||
color: #2F3136;
|
||||
}
|
||||
|
||||
span.msg-date {
|
||||
color: #99AAB5;
|
||||
}
|
||||
|
||||
span.msg-edited {
|
||||
color: #99AAB5;
|
||||
}
|
|
@ -1,396 +0,0 @@
|
|||
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
|
||||
}
|
13
DiscordChatExporter.Core/Resources/ExportTemplates/Csv.csv
Normal file
13
DiscordChatExporter.Core/Resources/ExportTemplates/Csv.csv
Normal file
|
@ -0,0 +1,13 @@
|
|||
Author;Date;Content;Attachments;
|
||||
|
||||
{{- for group in MessageGroups -}}
|
||||
{{- for message in group.Messages -}}
|
||||
{{- message.Author.FullName }};
|
||||
|
||||
{{- message.TimeStamp | FormatDate }};
|
||||
|
||||
{{- message.Content | FormatContent }};
|
||||
|
||||
{{- message.Attachments | array.map "Url" | array.join "," }};
|
||||
{{~ end -}}
|
||||
{{- end -}}
|
Can't render this file because it contains an unexpected character in line 11 and column 41.
|
|
@ -0,0 +1,175 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>{{ Guild.Name | HtmlEncode }} - {{ Channel.Name | HtmlEncode }}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<style>{{ StyleSheet }}</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{~ # Info }}
|
||||
<div class="info">
|
||||
<div class="info__guild-icon-container">
|
||||
<img class="info__guild-icon" src="{{ Guild.IconUrl }}" />
|
||||
</div>
|
||||
<div class="info__metadata">
|
||||
<div class="info__guild-name">{{ Guild.Name | HtmlEncode }}</div>
|
||||
<div class="info__channel-name">{{ Channel.Name | HtmlEncode }}</div>
|
||||
<div class="info__channel-topic">{{ Channel.Topic | HtmlEncode }}</div>
|
||||
<div class="info__channel-message-count">{{ TotalMessageCount | Format "N0" }} messages</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{~ # Log }}
|
||||
<div class="chatlog">
|
||||
{{ for group in MessageGroups }}
|
||||
<div class="chatlog__message-group">
|
||||
{{~ # Avatar }}
|
||||
<div class="chatlog__author-avatar-container">
|
||||
<img class="chatlog__author-avatar" src="{{ group.Author.AvatarUrl }}" />
|
||||
</div>
|
||||
{{~ # Author name and timestamp }}
|
||||
<div class="chatlog__messages">
|
||||
<span class="chatlog__author-name" title="{{ group.Author.FullName | HtmlEncode }}">{{ group.Author.Name | HtmlEncode }}</span>
|
||||
<span class="chatlog__timestamp">{{ group.Timestamp | FormatDate | HtmlEncode }}</span>
|
||||
|
||||
{{~ # Messages }}
|
||||
{{ for message in group.Messages }}
|
||||
{{~ # Content }}
|
||||
{{ if message.Content }}
|
||||
<div class="chatlog__content">
|
||||
{{ message.Content | FormatContent }}
|
||||
|
||||
{{~ # Edited timestamp }}
|
||||
{{ if message.EditedTimestamp }}
|
||||
<span class="chatlog__edited-timestamp" title="{{ message.EditedTimestamp | FormatDate | HtmlEncode }}">(edited)</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{~ # Attachments }}
|
||||
{{ for attachment in message.Attachments }}
|
||||
<div class="chatlog__attachment">
|
||||
<a href="{{ attachment.Url }}">
|
||||
{{~ # Image }}
|
||||
{{ if attachment.IsImage }}
|
||||
<img class="chatlog__attachment-thumbnail" src="{{ attachment.Url }}" />
|
||||
{{~ # Non-image }}
|
||||
{{ else }}
|
||||
Attachment: {{ attachment.FileName }} ({{ attachment.FileSize | FormatFileSize }})
|
||||
{{ end }}
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{~ # Embeds }}
|
||||
{{ for embed in message.Embeds }}
|
||||
<div class="chatlog__embed">
|
||||
<div class="chatlog__embed-color-pill" style="background-color: rgba({{ embed.Color | FormatColor }})"></div>
|
||||
<div class="chatlog__embed-content-container">
|
||||
<div class="chatlog__embed-content">
|
||||
<div class="chatlog__embed-text">
|
||||
{{~ # Author }}
|
||||
{{ if embed.Author }}
|
||||
<div class="chatlog__embed-author">
|
||||
{{ if embed.Author.IconUrl }}
|
||||
<img class="chatlog__embed-author-icon" src="{{ embed.Author.IconUrl }}" />
|
||||
{{ end }}
|
||||
{{ if embed.Author.Name }}
|
||||
<span class="chatlog__embed-author-name">
|
||||
{{ if embed.Author.Url }}
|
||||
<a class="chatlog__embed-author-name-link" href="{{ embed.Author.Url }}">{{ embed.Author.Name | HtmlEncode }}</a>
|
||||
{{ else }}
|
||||
{{ embed.Author.Name | HtmlEncode }}
|
||||
{{ end }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{~ # Title }}
|
||||
{{ if embed.Title }}
|
||||
<div class="chatlog__embed-title">
|
||||
{{ if embed.Url }}
|
||||
<a class="chatlog__embed-title-link" href="{{ embed.Url }}">{{ embed.Title | FormatContent }}</a>
|
||||
{{ else }}
|
||||
{{ embed.Title | FormatContent }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{~ # Description }}
|
||||
{{ if embed.Description }}
|
||||
<div class="chatlog__embed-description">{{ embed.Description | FormatContent true }}</div>
|
||||
{{ end }}
|
||||
|
||||
{{~ # Fields }}
|
||||
<div class="chatlog__embed-fields">
|
||||
{{ for field in embed.Fields }}
|
||||
<div class="chatlog__embed-field {{ if field.IsInline }} chatlog__embed-field--inline {{ end }}">
|
||||
{{ if field.Name }}
|
||||
<div class="chatlog__embed-field-name">{{ field.Name | FormatContent }}</div>
|
||||
{{ end }}
|
||||
{{ if field.Value }}
|
||||
<div class="chatlog__embed-field-value">{{ field.Value | FormatContent true }}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{~ # Thumbnail }}
|
||||
{{ if embed.Thumbnail }}
|
||||
<div class="chatlog__embed-thumbnail-container">
|
||||
<a class="chatlog__embed-thumbnail-link" href="{{ embed.Thumbnail.Url }}">
|
||||
<img class="chatlog__embed-thumbnail" src="{{ embed.Thumbnail.Url }}" />
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{~ # Image }}
|
||||
{{ if embed.Image }}
|
||||
<div class="chatlog__embed-image-container">
|
||||
<a class="chatlog__embed-image-link" href="{{ embed.Image.Url }}">
|
||||
<img class="chatlog__embed-image" src="{{ embed.Image.Url }}" />
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{~ # Footer }}
|
||||
{{ if embed.Footer || embed.Timestamp }}
|
||||
<div class="chatlog__embed-footer">
|
||||
{{ if embed.Footer }}
|
||||
{{ if embed.Footer.Text && embed.Footer.IconUrl }}
|
||||
<img class="chatlog__embed-footer-icon" src="{{ embed.Footer.IconUrl }}" />
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<span class="chatlog__embed-footer-text">
|
||||
{{ if embed.Footer }}
|
||||
{{ if embed.Footer.Text }}
|
||||
{{ embed.Footer.Text | HtmlEncode }}
|
||||
{{ if embed.Timestamp }} • {{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if embed.Timestamp }}
|
||||
{{ embed.Timestamp | FormatDate | HtmlEncode }}
|
||||
{{ end }}
|
||||
</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,90 @@
|
|||
/* === GENERAL === */
|
||||
|
||||
body {
|
||||
background-color: #36393e;
|
||||
color: #ffffffb3;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0096cf;
|
||||
}
|
||||
|
||||
.pre-multiline {
|
||||
background-color: #2F3136;
|
||||
border-color: #282b30;
|
||||
color: #839496;
|
||||
}
|
||||
|
||||
.pre-inline {
|
||||
background-color: #2f3136;
|
||||
}
|
||||
|
||||
.mention {
|
||||
background-color: #738bd71a;
|
||||
color: #7289da;
|
||||
}
|
||||
|
||||
/* === INFO === */
|
||||
|
||||
.info__guild-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.info__channel-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.info__channel-topic {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* === CHATLOG === */
|
||||
|
||||
.chatlog__message-group {
|
||||
border-color: #ffffff0a;
|
||||
}
|
||||
|
||||
.chatlog__author-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__timestamp {
|
||||
color: #ffffff33;
|
||||
}
|
||||
|
||||
.chatlog__edited-timestamp {
|
||||
color: #ffffff33;
|
||||
}
|
||||
|
||||
.chatlog__embed-content-container {
|
||||
background-color: #2e30364d;
|
||||
border-color: #2e303699;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name-link {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-description {
|
||||
color: #ffffff99;
|
||||
}
|
||||
|
||||
.chatlog__embed-fields {
|
||||
color: #ffffff99;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer {
|
||||
color: #ffffff99;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/* === GENERAL === */
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
color: #737f8d;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00b0f4;
|
||||
}
|
||||
|
||||
.pre-multiline {
|
||||
background-color: #f9f9f9;
|
||||
color: #657b83;
|
||||
}
|
||||
|
||||
.pre-inline {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
/* === INFO === */
|
||||
|
||||
.info__guild-name {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
.info__channel-name {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
.info__channel-topic {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
/* === CHATLOG === */
|
||||
|
||||
.chatlog__message-group {
|
||||
border-top: 1px solid #eceeef;
|
||||
}
|
||||
|
||||
.chatlog__author-name {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
.chatlog__timestamp {
|
||||
color: #99aab5;
|
||||
}
|
||||
|
||||
.chatlog__edited-timestamp {
|
||||
color: #99aab5;
|
||||
}
|
|
@ -0,0 +1,282 @@
|
|||
/* === GENERAL === */
|
||||
|
||||
body {
|
||||
font-family: "Whitney", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.pre-multiline {
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
border: 2px solid;
|
||||
border-radius: 5px;
|
||||
font-family: "Consolas", "Courier New", Courier, Monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.pre-inline {
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
font-family: "Consolas", "Courier New", Courier, Monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
vertical-align: -.4em;
|
||||
}
|
||||
|
||||
.emoji-jumboable {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.mention {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* === INFO === */
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.info__guild-icon-container {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.info__guild-icon {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
}
|
||||
|
||||
.info__metadata {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.info__guild-name {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.info__channel-name {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.info__channel-topic {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.info__channel-message-count {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* === CHATLOG === */
|
||||
|
||||
.chatlog {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.chatlog__message-group {
|
||||
display: flex;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
.chatlog__author-avatar-container {
|
||||
flex: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.chatlog__author-avatar {
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.chatlog__messages {
|
||||
flex: 1;
|
||||
min-width: 50%;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.chatlog__author-name {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.chatlog__timestamp {
|
||||
margin-left: 5px;
|
||||
font-size: .75em;
|
||||
}
|
||||
|
||||
.chatlog__content {
|
||||
padding-top: 5px;
|
||||
font-size: .9375em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.chatlog__edited-timestamp {
|
||||
margin-left: 5px;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.chatlog__attachment {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.chatlog__attachment-thumbnail {
|
||||
max-width: 50%;
|
||||
max-height: 500px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed {
|
||||
display: flex;
|
||||
max-width: 520px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.chatlog__embed-color-pill {
|
||||
flex-shrink: 0;
|
||||
width: 4px;
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 10px;
|
||||
padding-top: 8px;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 8px;
|
||||
border: 1px solid;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chatlog__embed-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chatlog__embed-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 9px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name {
|
||||
font-size: .875em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatlog__embed-title {
|
||||
margin-bottom: 4px;
|
||||
font-size: .875em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatlog__embed-description {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chatlog__embed-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chatlog__embed-field {
|
||||
flex: 0;
|
||||
min-width: 100%;
|
||||
max-width: 506px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.chatlog__embed-field--inline {
|
||||
flex: 1;
|
||||
flex-basis: auto;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-name {
|
||||
margin-bottom: 4px;
|
||||
font-size: .875em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-value {
|
||||
font-size: .875em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chatlog__embed-thumbnail {
|
||||
flex: 0;
|
||||
margin-left: 20px;
|
||||
max-width: 80px;
|
||||
max-height: 80px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-image-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chatlog__embed-image {
|
||||
max-width: 500px;
|
||||
max-height: 400px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer-icon {
|
||||
margin-right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer-text {
|
||||
font-weight: 600;
|
||||
font-size: .75em;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{{
|
||||
$SharedStyleSheet = include "Html.Shared.css"
|
||||
$ThemeStyleSheet = include "Html.DarkTheme.css"
|
||||
StyleSheet = $SharedStyleSheet + "\n" + $ThemeStyleSheet
|
||||
}}
|
||||
|
||||
{{ include "Html.Core.html" }}
|
|
@ -0,0 +1,7 @@
|
|||
{{
|
||||
$SharedStyleSheet = include "Html.Shared.css"
|
||||
$ThemeStyleSheet = include "Html.LightTheme.css"
|
||||
StyleSheet = $SharedStyleSheet + "\n" + $ThemeStyleSheet
|
||||
}}
|
||||
|
||||
{{ include "Html.Core.html" }}
|
|
@ -0,0 +1,17 @@
|
|||
==============================================================
|
||||
Guild: {{ Guild.Name }}
|
||||
Channel: {{ Channel.Name }}
|
||||
Topic: {{ Channel.Topic }}
|
||||
Messages: {{ TotalMessageCount | Format "N0" }}
|
||||
==============================================================
|
||||
|
||||
{{~ for group in MessageGroups ~}}
|
||||
{{~ group.Author.FullName }} [{{ group.TimeStamp | FormatDate }}]
|
||||
{{~ for message in group.Messages ~}}
|
||||
{{~ message.Content | FormatContent }}
|
||||
{{~ for attachment in message.Attachments ~}}
|
||||
{{~ attachment.Url }}
|
||||
{{~ end ~}}
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ end ~}}
|
183
DiscordChatExporter.Core/Services/DataService.Parsers.cs
Normal file
183
DiscordChatExporter.Core/Services/DataService.Parsers.cs
Normal file
|
@ -0,0 +1,183 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using DiscordChatExporter.Core.Internal;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class DataService
|
||||
{
|
||||
private User ParseUser(JToken json)
|
||||
{
|
||||
var id = json["id"].Value<string>();
|
||||
var discriminator = json["discriminator"].Value<int>();
|
||||
var name = json["username"].Value<string>();
|
||||
var avatarHash = json["avatar"].Value<string>();
|
||||
|
||||
return new User(id, discriminator, name, avatarHash);
|
||||
}
|
||||
|
||||
private Guild ParseGuild(JToken json)
|
||||
{
|
||||
var id = json["id"].Value<string>();
|
||||
var name = json["name"].Value<string>();
|
||||
var iconHash = json["icon"].Value<string>();
|
||||
|
||||
return new Guild(id, name, iconHash);
|
||||
}
|
||||
|
||||
private Channel ParseChannel(JToken json)
|
||||
{
|
||||
// Get basic data
|
||||
var id = json["id"].Value<string>();
|
||||
var type = (ChannelType) json["type"].Value<int>();
|
||||
var topic = json["topic"]?.Value<string>();
|
||||
|
||||
// Try to extract guild ID
|
||||
var guildId = json["guild_id"]?.Value<string>();
|
||||
|
||||
// If the guild ID is blank, it's direct messages
|
||||
if (guildId.IsBlank())
|
||||
guildId = Guild.DirectMessages.Id;
|
||||
|
||||
// Try to extract name
|
||||
var name = json["name"]?.Value<string>();
|
||||
|
||||
// If the name is blank, it's direct messages
|
||||
if (name.IsBlank())
|
||||
name = json["recipients"].Select(ParseUser).Select(u => u.Name).JoinToString(", ");
|
||||
|
||||
return new Channel(id, guildId, name, topic, type);
|
||||
}
|
||||
|
||||
private Role ParseRole(JToken json)
|
||||
{
|
||||
var id = json["id"].Value<string>();
|
||||
var name = json["name"].Value<string>();
|
||||
|
||||
return new Role(id, name);
|
||||
}
|
||||
|
||||
private EmbedAuthor ParseEmbedAuthor(JToken json)
|
||||
{
|
||||
var name = json["name"]?.Value<string>();
|
||||
var url = json["url"]?.Value<string>();
|
||||
var iconUrl = json["icon_url"]?.Value<string>();
|
||||
|
||||
return new EmbedAuthor(name, url, iconUrl);
|
||||
}
|
||||
|
||||
private EmbedField ParseEmbedField(JToken json)
|
||||
{
|
||||
var name = json["name"].Value<string>();
|
||||
var value = json["value"].Value<string>();
|
||||
var isInline = json["inline"]?.Value<bool>() ?? false;
|
||||
|
||||
return new EmbedField(name, value, isInline);
|
||||
}
|
||||
|
||||
private Attachment ParseAttachment(JToken json)
|
||||
{
|
||||
var id = json["id"].Value<string>();
|
||||
var url = json["url"].Value<string>();
|
||||
var isImage = json["width"] != null;
|
||||
var fileName = json["filename"].Value<string>();
|
||||
var fileSize = json["size"].Value<long>();
|
||||
|
||||
return new Attachment(id, isImage, url, fileName, fileSize);
|
||||
}
|
||||
|
||||
private EmbedImage ParseEmbedImage(JToken json)
|
||||
{
|
||||
var url = json["url"]?.Value<string>();
|
||||
var height = json["height"]?.Value<int>();
|
||||
var width = json["width"]?.Value<int>();
|
||||
|
||||
return new EmbedImage(url, height, width);
|
||||
}
|
||||
|
||||
private EmbedFooter ParseEmbedFooter(JToken json)
|
||||
{
|
||||
var text = json["text"].Value<string>();
|
||||
var iconUrl = json["icon_url"]?.Value<string>();
|
||||
|
||||
return new EmbedFooter(text, iconUrl);
|
||||
}
|
||||
|
||||
private Embed ParseEmbed(JToken json)
|
||||
{
|
||||
// Get basic data
|
||||
var title = json["title"]?.Value<string>();
|
||||
var description = json["description"]?.Value<string>();
|
||||
var url = json["url"]?.Value<string>();
|
||||
var timestamp = json["timestamp"]?.Value<DateTime>();
|
||||
|
||||
// Get color
|
||||
var color = json["color"] != null
|
||||
? Color.FromArgb(json["color"].Value<int>()).ResetAlpha()
|
||||
: Color.FromArgb(79, 84, 92); // default color
|
||||
|
||||
// Get author
|
||||
var author = json["author"] != null ? ParseEmbedAuthor(json["author"]) : null;
|
||||
|
||||
// Get fields
|
||||
var fields = json["fields"].EmptyIfNull().Select(ParseEmbedField).ToArray();
|
||||
|
||||
// Get thumbnail
|
||||
var thumbnail = json["thumbnail"] != null ? ParseEmbedImage(json["thumbnail"]) : null;
|
||||
|
||||
// Get image
|
||||
var image = json["image"] != null ? ParseEmbedImage(json["image"]) : null;
|
||||
|
||||
// Get footer
|
||||
var footer = json["footer"] != null ? ParseEmbedFooter(json["footer"]) : null;
|
||||
|
||||
return new Embed(title, url, timestamp, color, author, description, fields, thumbnail, image, footer);
|
||||
}
|
||||
|
||||
private Message ParseMessage(JToken json)
|
||||
{
|
||||
// Get basic data
|
||||
var id = json["id"].Value<string>();
|
||||
var channelId = json["channel_id"].Value<string>();
|
||||
var timestamp = json["timestamp"].Value<DateTime>();
|
||||
var editedTimestamp = json["edited_timestamp"]?.Value<DateTime?>();
|
||||
var content = json["content"].Value<string>();
|
||||
var type = (MessageType) json["type"].Value<int>();
|
||||
|
||||
// Workarounds for non-default types
|
||||
if (type == MessageType.RecipientAdd)
|
||||
content = "Added a recipient.";
|
||||
else if (type == MessageType.RecipientRemove)
|
||||
content = "Removed a recipient.";
|
||||
else if (type == MessageType.Call)
|
||||
content = "Started a call.";
|
||||
else if (type == MessageType.ChannelNameChange)
|
||||
content = "Changed the channel name.";
|
||||
else if (type == MessageType.ChannelIconChange)
|
||||
content = "Changed the channel icon.";
|
||||
else if (type == MessageType.ChannelPinnedMessage)
|
||||
content = "Pinned a message.";
|
||||
else if (type == MessageType.GuildMemberJoin)
|
||||
content = "Joined the server.";
|
||||
|
||||
// Get author
|
||||
var author = ParseUser(json["author"]);
|
||||
|
||||
// Get attachments
|
||||
var attachments = json["attachments"].EmptyIfNull().Select(ParseAttachment).ToArray();
|
||||
|
||||
// Get embeds
|
||||
var embeds = json["embeds"].EmptyIfNull().Select(ParseEmbed).ToArray();
|
||||
|
||||
// Get mentioned users
|
||||
var mentionedUsers = json["mentioned_users"].EmptyIfNull().Select(ParseUser).ToArray();
|
||||
|
||||
return new Message(id, channelId, type, author, timestamp, editedTimestamp, content, attachments, embeds,
|
||||
mentionedUsers);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,297 +2,29 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Exceptions;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Tyrrrz.Extensions;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using DiscordChatExporter.Core.Internal;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class DataService : IDataService, IDisposable
|
||||
{
|
||||
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>();
|
||||
|
||||
private User ParseUser(JToken token)
|
||||
private async Task<JToken> GetApiResponseAsync(string token, string resource, string endpoint, params string[] parameters)
|
||||
{
|
||||
var id = token["id"].Value<string>();
|
||||
var discriminator = token["discriminator"].Value<int>();
|
||||
var name = token["username"].Value<string>();
|
||||
var avatarHash = token["avatar"].Value<string>();
|
||||
// Format URL
|
||||
const string apiRoot = "https://discordapp.com/api/v6";
|
||||
var url = $"{apiRoot}/{resource}/{endpoint}?token={token}";
|
||||
|
||||
return new User(id, discriminator, name, avatarHash);
|
||||
}
|
||||
// Add parameters
|
||||
foreach (var parameter in parameters)
|
||||
url += $"&{parameter}";
|
||||
|
||||
private Role ParseRole(JToken token)
|
||||
{
|
||||
var id = token["id"].Value<string>();
|
||||
var name = token["name"].Value<string>();
|
||||
|
||||
return new Role(id, name);
|
||||
}
|
||||
|
||||
private Guild ParseGuild(JToken token)
|
||||
{
|
||||
var id = token["id"].Value<string>();
|
||||
var name = token["name"].Value<string>();
|
||||
var iconHash = token["icon"].Value<string>();
|
||||
var roles = token["roles"].Select(ParseRole).ToArray();
|
||||
|
||||
return new Guild(id, name, iconHash, roles);
|
||||
}
|
||||
|
||||
private Channel ParseChannel(JToken token)
|
||||
{
|
||||
// Get basic data
|
||||
var id = token["id"].Value<string>();
|
||||
var guildId = token["guild_id"]?.Value<string>();
|
||||
var type = (ChannelType) token["type"].Value<int>();
|
||||
var topic = token["topic"]?.Value<string>();
|
||||
|
||||
// Extract name based on type
|
||||
string name;
|
||||
if (type.IsEither(ChannelType.DirectTextChat, ChannelType.DirectGroupTextChat))
|
||||
{
|
||||
guildId = Guild.DirectMessages.Id;
|
||||
|
||||
// Try to get name if it's set
|
||||
name = token["name"]?.Value<string>();
|
||||
|
||||
// Otherwise use recipients as the name
|
||||
if (name.IsBlank())
|
||||
name = token["recipients"].Select(ParseUser).Select(u => u.Name).JoinToString(", ");
|
||||
}
|
||||
else
|
||||
{
|
||||
name = token["name"].Value<string>();
|
||||
}
|
||||
|
||||
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
|
||||
var id = token["id"].Value<string>();
|
||||
var channelId = token["channel_id"].Value<string>();
|
||||
var timeStamp = token["timestamp"].Value<DateTime>();
|
||||
var editedTimeStamp = token["edited_timestamp"]?.Value<DateTime?>();
|
||||
var content = token["content"].Value<string>();
|
||||
var type = (MessageType) token["type"].Value<int>();
|
||||
|
||||
// Workarounds for non-default types
|
||||
if (type == MessageType.RecipientAdd)
|
||||
content = "Added a recipient.";
|
||||
else if (type == MessageType.RecipientRemove)
|
||||
content = "Removed a recipient.";
|
||||
else if (type == MessageType.Call)
|
||||
content = "Started a call.";
|
||||
else if (type == MessageType.ChannelNameChange)
|
||||
content = "Changed the channel name.";
|
||||
else if (type == MessageType.ChannelIconChange)
|
||||
content = "Changed the channel icon.";
|
||||
else if (type == MessageType.ChannelPinnedMessage)
|
||||
content = "Pinned a message.";
|
||||
else if (type == MessageType.GuildMemberJoin)
|
||||
content = "Joined the server.";
|
||||
|
||||
// Get author
|
||||
var author = ParseUser(token["author"]);
|
||||
|
||||
// Get attachment
|
||||
var attachments = new List<Attachment>();
|
||||
foreach (var attachmentJson in token["attachments"].EmptyIfNull())
|
||||
{
|
||||
var attachmentId = attachmentJson["id"].Value<string>();
|
||||
var attachmentUrl = attachmentJson["url"].Value<string>();
|
||||
var attachmentType = attachmentJson["width"] != null
|
||||
? AttachmentType.Image
|
||||
: AttachmentType.Other;
|
||||
var attachmentFileName = attachmentJson["filename"].Value<string>();
|
||||
var attachmentFileSize = attachmentJson["size"].Value<long>();
|
||||
|
||||
var attachment = new Attachment(
|
||||
attachmentId, attachmentType, attachmentUrl,
|
||||
attachmentFileName, attachmentFileSize);
|
||||
attachments.Add(attachment);
|
||||
}
|
||||
|
||||
// Get embeds
|
||||
var embeds = token["embeds"].EmptyIfNull().Select(ParseEmbed).ToArray();
|
||||
|
||||
// Get user mentions
|
||||
var mentionedUsers = token["mentions"].Select(ParseUser).ToList();
|
||||
|
||||
// Get role mentions
|
||||
var mentionedRoles = token["mention_roles"]
|
||||
.Values<string>()
|
||||
.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(i))
|
||||
.ToList();
|
||||
|
||||
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)
|
||||
{
|
||||
// Send request
|
||||
using (var response = await _httpClient.GetAsync(url))
|
||||
{
|
||||
// Check status code
|
||||
|
@ -301,213 +33,84 @@ namespace DiscordChatExporter.Core.Services
|
|||
throw new HttpErrorStatusCodeException(response.StatusCode);
|
||||
|
||||
// Get content
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
var raw = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Parse
|
||||
return JToken.Parse(raw);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Guild> GetGuildAsync(string token, string guildId)
|
||||
{
|
||||
// Form request url
|
||||
var url = $"{ApiRoot}/guilds/{guildId}?token={token}";
|
||||
|
||||
// Get response
|
||||
var content = await GetStringAsync(url);
|
||||
|
||||
// Parse
|
||||
var guild = ParseGuild(JToken.Parse(content));
|
||||
|
||||
// Add roles to cache
|
||||
foreach (var role in guild.Roles)
|
||||
_roleCache[role.Id] = role;
|
||||
var response = await GetApiResponseAsync(token, "guilds", guildId);
|
||||
var guild = ParseGuild(response);
|
||||
|
||||
return guild;
|
||||
}
|
||||
|
||||
public async Task<Channel> GetChannelAsync(string token, string channelId)
|
||||
{
|
||||
// Form request url
|
||||
var url = $"{ApiRoot}/channels/{channelId}?token={token}";
|
||||
|
||||
// Get response
|
||||
var content = await GetStringAsync(url);
|
||||
|
||||
// Parse
|
||||
var channel = ParseChannel(JToken.Parse(content));
|
||||
|
||||
// Add channel to cache
|
||||
_channelCache[channel.Id] = channel;
|
||||
var response = await GetApiResponseAsync(token, "channels", channelId);
|
||||
var channel = ParseChannel(response);
|
||||
|
||||
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
|
||||
var url = $"{ApiRoot}/guilds/{guildId}/channels?token={token}";
|
||||
|
||||
// Get response
|
||||
var content = await GetStringAsync(url);
|
||||
|
||||
// Parse
|
||||
var channels = JArray.Parse(content).Select(ParseChannel).ToArray();
|
||||
|
||||
// Add channels to cache
|
||||
foreach (var channel in channels)
|
||||
_channelCache[channel.Id] = channel;
|
||||
|
||||
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
|
||||
var url = $"{ApiRoot}/users/@me/guilds?token={token}&limit=100";
|
||||
|
||||
// Get response
|
||||
var content = await GetStringAsync(url);
|
||||
|
||||
// Parse IDs
|
||||
var guildIds = JArray.Parse(content).Select(t => t["id"].Value<string>());
|
||||
|
||||
// Get full guild infos
|
||||
var guilds = new List<Guild>();
|
||||
foreach (var guildId in guildIds)
|
||||
{
|
||||
var guild = await GetGuildAsync(token, guildId);
|
||||
guilds.Add(guild);
|
||||
}
|
||||
var response = await GetApiResponseAsync(token, "users", "@me/guilds", "limit=100");
|
||||
var guilds = response.Select(ParseGuild).ToArray();
|
||||
|
||||
return guilds;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Channel>> GetDirectMessageChannelsAsync(string token)
|
||||
{
|
||||
// Form request url
|
||||
var url = $"{ApiRoot}/users/@me/channels?token={token}";
|
||||
|
||||
// Get response
|
||||
var content = await GetStringAsync(url);
|
||||
|
||||
// Parse
|
||||
var channels = JArray.Parse(content).Select(ParseChannel).ToArray();
|
||||
var response = await GetApiResponseAsync(token, "users", "@me/channels");
|
||||
var channels = response.Select(ParseChannel).ToArray();
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<User>> GetGuildMembersAsync(string token, string guildId)
|
||||
public async Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string token, string guildId)
|
||||
{
|
||||
var result = new List<User>();
|
||||
var response = await GetApiResponseAsync(token, "guilds", $"{guildId}/channels");
|
||||
var channels = response.Select(ParseChannel).ToArray();
|
||||
|
||||
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;
|
||||
return channels;
|
||||
}
|
||||
|
||||
// If no users - break
|
||||
if (currentUserId == null)
|
||||
break;
|
||||
public async Task<IReadOnlyList<Role>> GetGuildRolesAsync(string token, string guildId)
|
||||
{
|
||||
var response = await GetApiResponseAsync(token, "guilds", $"{guildId}/roles");
|
||||
var roles = response.Select(ParseRole).ToArray();
|
||||
|
||||
// Otherwise offset the next request
|
||||
afterId = currentUserId;
|
||||
}
|
||||
|
||||
return result;
|
||||
return roles;
|
||||
}
|
||||
|
||||
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
|
||||
// collecting everything between them in batches
|
||||
var beforeId = to != null ? DateTimeToSnowflake(to.Value) : null;
|
||||
var beforeId = to?.ToSnowflake() ?? DateTime.MaxValue.ToSnowflake();
|
||||
while (true)
|
||||
{
|
||||
// Form request url
|
||||
var url = $"{ApiRoot}/channels/{channelId}/messages?token={token}&limit=100";
|
||||
if (beforeId.IsNotBlank())
|
||||
url += $"&before={beforeId}";
|
||||
|
||||
// Get response
|
||||
var content = await GetStringAsync(url);
|
||||
var response = await GetApiResponseAsync(token, "channels", $"{channelId}/messages",
|
||||
"limit=100", $"before={beforeId}");
|
||||
|
||||
// Parse
|
||||
var messages = JArray.Parse(content).Select(ParseMessage);
|
||||
var messages = response.Select(ParseMessage);
|
||||
|
||||
// Add messages to list
|
||||
string currentMessageId = null;
|
||||
foreach (var message in messages)
|
||||
{
|
||||
// Break when the message is older than from date
|
||||
if (from != null && message.TimeStamp < from)
|
||||
if (from != null && message.Timestamp < from)
|
||||
{
|
||||
currentMessageId = null;
|
||||
break;
|
||||
|
@ -529,39 +132,39 @@ 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;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
public async Task<Mentionables> GetMentionablesAsync(string token, string guildId,
|
||||
IEnumerable<Message> messages)
|
||||
{
|
||||
if (disposing)
|
||||
// Get channels and roles
|
||||
var channels = guildId != Guild.DirectMessages.Id
|
||||
? await GetGuildChannelsAsync(token, guildId)
|
||||
: Array.Empty<Channel>();
|
||||
var roles = guildId != Guild.DirectMessages.Id
|
||||
? await GetGuildRolesAsync(token, guildId)
|
||||
: Array.Empty<Role>();
|
||||
|
||||
// Get users
|
||||
var userMap = new Dictionary<string, User>();
|
||||
foreach (var message in messages)
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
// Author
|
||||
userMap[message.Author.Id] = message.Author;
|
||||
|
||||
// Mentioned users
|
||||
foreach (var mentionedUser in message.MentionedUsers)
|
||||
userMap[mentionedUser.Id] = mentionedUser;
|
||||
}
|
||||
var users = userMap.Values.ToArray();
|
||||
|
||||
return new Mentionables(users, channels, roles);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class DataService
|
||||
{
|
||||
private static string DateTimeToSnowflake(DateTime dateTime)
|
||||
{
|
||||
const long epoch = 62135596800000;
|
||||
var unixTime = dateTime.ToUniversalTime().Ticks / TimeSpan.TicksPerMillisecond - epoch;
|
||||
var value = ((ulong) unixTime - 1420070400000UL) << 22;
|
||||
return value.ToString();
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CsvHelper;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class ExportService
|
||||
{
|
||||
private string FormatMessageContentCsv(Message message)
|
||||
{
|
||||
var content = message.Content;
|
||||
|
||||
// New lines
|
||||
content = content.Replace("\n", ", ");
|
||||
|
||||
// User mentions (<@id> and <@!id>)
|
||||
foreach (var mentionedUser in message.MentionedUsers)
|
||||
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>", $"@{mentionedUser}");
|
||||
|
||||
// Role mentions (<@&id>)
|
||||
foreach (var mentionedRole in message.MentionedRoles)
|
||||
content = content.Replace($"<@&{mentionedRole.Id}>", $"@{mentionedRole.Name}");
|
||||
|
||||
// Channel mentions (<#id>)
|
||||
foreach (var mentionedChannel in message.MentionedChannels)
|
||||
content = content.Replace($"<#{mentionedChannel.Id}>", $"#{mentionedChannel.Name}");
|
||||
|
||||
// Custom emojis (<:name:id>)
|
||||
content = Regex.Replace(content, "<(:.*?:)\\d*>", "$1");
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private async Task ExportAsCsvAsync(ChannelChatLog log, TextWriter output)
|
||||
{
|
||||
using (var writer = new CsvWriter(output))
|
||||
{
|
||||
// Headers
|
||||
writer.WriteField("Author");
|
||||
writer.WriteField("Date");
|
||||
writer.WriteField("Content");
|
||||
writer.WriteField("Attachments");
|
||||
await writer.NextRecordAsync();
|
||||
|
||||
// Chat log
|
||||
foreach (var group in log.MessageGroups)
|
||||
{
|
||||
// Messages
|
||||
foreach (var msg in group.Messages)
|
||||
{
|
||||
// Author
|
||||
writer.WriteField(msg.Author.FullName);
|
||||
|
||||
// Date
|
||||
var timeStampFormatted = msg.TimeStamp.ToString(_settingsService.DateFormat);
|
||||
writer.WriteField(timeStampFormatted);
|
||||
|
||||
// Content
|
||||
var contentFormatted = msg.Content.IsNotBlank() ? FormatMessageContentCsv(msg) : null;
|
||||
writer.WriteField(contentFormatted);
|
||||
|
||||
// Attachments
|
||||
var attachmentsFormatted = msg.Attachments.Select(a => a.Url).JoinToString(",");
|
||||
writer.WriteField(attachmentsFormatted);
|
||||
|
||||
await writer.NextRecordAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,365 +0,0 @@
|
|||
using System.IO;
|
||||
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 MarkdownToHtml(string content, IMentionable mentionable = null, bool allowLinks = false)
|
||||
{
|
||||
// A lot of these regexes were inspired by or taken from MarkdownSharp
|
||||
|
||||
// HTML-encode content
|
||||
content = HtmlEncode(content);
|
||||
|
||||
// Encode multiline codeblocks (```text```)
|
||||
content = Regex.Replace(content,
|
||||
@"```+(?:[^`]*?\n)?([^`]+)\n?```+",
|
||||
m => $"\x1AM{Base64Encode(m.Groups[1].Value)}\x1AM");
|
||||
|
||||
// Encode inline codeblocks (`text`)
|
||||
content = Regex.Replace(content,
|
||||
@"`([^`]+)`",
|
||||
m => $"\x1AI{Base64Encode(m.Groups[1].Value)}\x1AI");
|
||||
|
||||
// Encode URLs
|
||||
content = Regex.Replace(content,
|
||||
@"(\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**)
|
||||
content = Regex.Replace(content, @"(\*\*)(?=\S)(.+?[*_]*)(?<=\S)\1", "<b>$2</b>");
|
||||
|
||||
// Process underline (__text__)
|
||||
content = Regex.Replace(content, @"(__)(?=\S)(.+?)(?<=\S)\1", "<u>$2</u>");
|
||||
|
||||
// Process italic (*text* or _text_)
|
||||
content = Regex.Replace(content, @"(\*|_)(?=\S)(.+?)(?<=\S)\1", "<i>$2</i>");
|
||||
|
||||
// Process strike through (~~text~~)
|
||||
content = Regex.Replace(content, @"(~~)(?=\S)(.+?)(?<=\S)\1", "<s>$2</s>");
|
||||
|
||||
// Decode and process multiline codeblocks
|
||||
content = Regex.Replace(content, "\x1AM(.*?)\x1AM",
|
||||
m => $"<div class=\"pre\">{Base64Decode(m.Groups[1].Value)}</div>");
|
||||
|
||||
// Decode and process inline codeblocks
|
||||
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>");
|
||||
|
||||
// New lines
|
||||
content = content.Replace("\n", "<br />");
|
||||
|
||||
// Meta mentions (@everyone)
|
||||
content = content.Replace("@everyone", "<span class=\"mention\">@everyone</span>");
|
||||
|
||||
// Meta mentions (@here)
|
||||
content = content.Replace("@here", "<span class=\"mention\">@here</span>");
|
||||
|
||||
if (mentionable != null)
|
||||
{
|
||||
// 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 mentionable.MentionedRoles)
|
||||
{
|
||||
content = content.Replace($"<@&{mentionedRole.Id}>",
|
||||
"<span class=\"mention\">" +
|
||||
$"@{HtmlEncode(mentionedRole.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>)
|
||||
content = Regex.Replace(content, "<(:.*?:)(\\d*)>",
|
||||
"<img class=\"emoji\" title=\"$1\" src=\"https://cdn.discordapp.com/emojis/$2.png\" />");
|
||||
|
||||
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
|
||||
await output.WriteLineAsync("<!-- https://github.com/Tyrrrz/DiscordChatExporter -->");
|
||||
|
||||
// Html start
|
||||
await output.WriteLineAsync("<!DOCTYPE html>");
|
||||
await output.WriteLineAsync("<html lang=\"en\">");
|
||||
|
||||
// HEAD
|
||||
await output.WriteLineAsync("<head>");
|
||||
await output.WriteLineAsync($"<title>{log.Guild.Name} - {log.Channel.Name}</title>");
|
||||
await output.WriteLineAsync("<meta charset=\"utf-8\" />");
|
||||
await output.WriteLineAsync("<meta name=\"viewport\" content=\"width=device-width\" />");
|
||||
await output.WriteLineAsync($"<style>{css}</style>");
|
||||
await output.WriteLineAsync("</head>");
|
||||
|
||||
// Body start
|
||||
await output.WriteLineAsync("<body>");
|
||||
|
||||
// Guild and channel info
|
||||
await output.WriteLineAsync("<div id=\"info\">");
|
||||
await output.WriteLineAsync("<div class=\"info-left\">");
|
||||
await output.WriteLineAsync($"<img class=\"guild-icon\" src=\"{log.Guild.IconUrl}\" />");
|
||||
await output.WriteLineAsync("</div>"); // info-left
|
||||
await output.WriteLineAsync("<div class=\"info-right\">");
|
||||
await output.WriteLineAsync($"<div class=\"guild-name\">{log.Guild.Name}</div>");
|
||||
await output.WriteLineAsync($"<div class=\"channel-name\">{log.Channel.Name}</div>");
|
||||
await output.WriteLineAsync($"<div class=\"channel-topic\">{log.Channel.Topic}</div>");
|
||||
await output.WriteLineAsync(
|
||||
$"<div class=\"channel-messagecount\">{log.TotalMessageCount:N0} messages</div>");
|
||||
await output.WriteLineAsync("</div>"); // info-right
|
||||
await output.WriteLineAsync("</div>"); // info
|
||||
|
||||
// Chat log
|
||||
await output.WriteLineAsync("<div id=\"log\">");
|
||||
foreach (var group in log.MessageGroups)
|
||||
{
|
||||
await output.WriteLineAsync("<div class=\"msg\">");
|
||||
await output.WriteLineAsync("<div class=\"msg-left\">");
|
||||
await output.WriteLineAsync($"<img class=\"msg-avatar\" src=\"{group.Author.AvatarUrl}\" />");
|
||||
await output.WriteLineAsync("</div>");
|
||||
|
||||
await output.WriteLineAsync("<div class=\"msg-right\">");
|
||||
await output.WriteAsync(
|
||||
$"<span class=\"msg-user\" title=\"{HtmlEncode(group.Author.FullName)}\">");
|
||||
await output.WriteAsync(HtmlEncode(group.Author.Name));
|
||||
await output.WriteLineAsync("</span>");
|
||||
var timeStampFormatted = HtmlEncode(group.TimeStamp.ToString(_settingsService.DateFormat));
|
||||
await output.WriteLineAsync($"<span class=\"msg-date\">{timeStampFormatted}</span>");
|
||||
|
||||
// Messages
|
||||
foreach (var message in group.Messages)
|
||||
{
|
||||
// Content
|
||||
if (message.Content.IsNotBlank())
|
||||
{
|
||||
await output.WriteLineAsync("<div class=\"msg-content\">");
|
||||
var contentFormatted = FormatMessageContentHtml(message);
|
||||
await output.WriteAsync(contentFormatted);
|
||||
|
||||
// Edited timestamp
|
||||
if (message.EditedTimeStamp != null)
|
||||
{
|
||||
var editedTimeStampFormatted =
|
||||
HtmlEncode(message.EditedTimeStamp.Value.ToString(_settingsService.DateFormat));
|
||||
await output.WriteAsync(
|
||||
$"<span class=\"msg-edited\" title=\"{editedTimeStampFormatted}\">(edited)</span>");
|
||||
}
|
||||
|
||||
await output.WriteLineAsync("</div>"); // msg-content
|
||||
}
|
||||
|
||||
// Attachments
|
||||
foreach (var attachment in message.Attachments)
|
||||
{
|
||||
if (attachment.Type == AttachmentType.Image)
|
||||
{
|
||||
await output.WriteLineAsync("<div class=\"msg-attachment\">");
|
||||
await output.WriteLineAsync($"<a href=\"{attachment.Url}\">");
|
||||
await output.WriteLineAsync($"<img class=\"msg-attachment\" src=\"{attachment.Url}\" />");
|
||||
await output.WriteLineAsync("</a>");
|
||||
await output.WriteLineAsync("</div>");
|
||||
}
|
||||
else
|
||||
{
|
||||
await output.WriteLineAsync("<div class=\"msg-attachment\">");
|
||||
await output.WriteLineAsync($"<a href=\"{attachment.Url}\">");
|
||||
var fileSizeFormatted = FormatFileSize(attachment.FileSize);
|
||||
await output.WriteLineAsync($"Attachment: {attachment.FileName} ({fileSizeFormatted})");
|
||||
await output.WriteLineAsync("</a>");
|
||||
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
|
||||
await output.WriteLineAsync("</div>"); // msg
|
||||
}
|
||||
|
||||
await output.WriteLineAsync("</div>"); // log
|
||||
|
||||
await output.WriteLineAsync("</body>");
|
||||
await output.WriteLineAsync("</html>");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class ExportService
|
||||
{
|
||||
private string FormatMessageContentPlainText(Message message)
|
||||
{
|
||||
var content = message.Content;
|
||||
|
||||
// New lines
|
||||
content = content.Replace("\n", Environment.NewLine);
|
||||
|
||||
// User mentions (<@id> and <@!id>)
|
||||
foreach (var mentionedUser in message.MentionedUsers)
|
||||
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>", $"@{mentionedUser}");
|
||||
|
||||
// Role mentions (<@&id>)
|
||||
foreach (var mentionedRole in message.MentionedRoles)
|
||||
content = content.Replace($"<@&{mentionedRole.Id}>", $"@{mentionedRole.Name}");
|
||||
|
||||
// Channel mentions (<#id>)
|
||||
foreach (var mentionedChannel in message.MentionedChannels)
|
||||
content = content.Replace($"<#{mentionedChannel.Id}>", $"#{mentionedChannel.Name}");
|
||||
|
||||
// Custom emojis (<:name:id>)
|
||||
content = Regex.Replace(content, "<(:.*?:)\\d*>", "$1");
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private async Task ExportAsPlainTextAsync(ChannelChatLog log, TextWriter output)
|
||||
{
|
||||
// Generation info
|
||||
await output.WriteLineAsync("https://github.com/Tyrrrz/DiscordChatExporter");
|
||||
await output.WriteLineAsync();
|
||||
|
||||
// Guild and channel info
|
||||
await output.WriteLineAsync('='.Repeat(48));
|
||||
await output.WriteLineAsync($"Guild: {log.Guild.Name}");
|
||||
await output.WriteLineAsync($"Channel: {log.Channel.Name}");
|
||||
await output.WriteLineAsync($"Topic: {log.Channel.Topic}");
|
||||
await output.WriteLineAsync($"Messages: {log.TotalMessageCount:N0}");
|
||||
await output.WriteLineAsync('='.Repeat(48));
|
||||
await output.WriteLineAsync();
|
||||
|
||||
// Chat log
|
||||
foreach (var group in log.MessageGroups)
|
||||
{
|
||||
var timeStampFormatted = group.TimeStamp.ToString(_settingsService.DateFormat);
|
||||
await output.WriteLineAsync($"{group.Author.FullName} [{timeStampFormatted}]");
|
||||
|
||||
// Messages
|
||||
foreach (var message in group.Messages)
|
||||
{
|
||||
// Content
|
||||
if (message.Content.IsNotBlank())
|
||||
{
|
||||
var contentFormatted = FormatMessageContentPlainText(message);
|
||||
await output.WriteLineAsync(contentFormatted);
|
||||
}
|
||||
|
||||
// Attachments
|
||||
foreach (var attachment in message.Attachments)
|
||||
{
|
||||
await output.WriteLineAsync(attachment.Url);
|
||||
}
|
||||
}
|
||||
|
||||
await output.WriteLineAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using System.Reflection;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Scriban;
|
||||
using Scriban.Parsing;
|
||||
using Scriban.Runtime;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class ExportService
|
||||
{
|
||||
private class TemplateLoader : ITemplateLoader
|
||||
{
|
||||
private const string ResourceRootNamespace = "DiscordChatExporter.Core.Resources.ExportTemplates";
|
||||
|
||||
public string GetPath(TemplateContext context, SourceSpan callerSpan, string templateName)
|
||||
{
|
||||
return $"{ResourceRootNamespace}.{templateName}";
|
||||
}
|
||||
|
||||
public string GetPath(ExportFormat format)
|
||||
{
|
||||
return $"{ResourceRootNamespace}.{format}.{format.GetFileExtension()}";
|
||||
}
|
||||
|
||||
public string Load(TemplateContext context, SourceSpan callerSpan, string templatePath)
|
||||
{
|
||||
return Assembly.GetExecutingAssembly().GetManifestResourceString(templatePath);
|
||||
}
|
||||
|
||||
public string Load(ExportFormat format)
|
||||
{
|
||||
return Assembly.GetExecutingAssembly().GetManifestResourceString(GetPath(format));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
325
DiscordChatExporter.Core/Services/ExportService.TemplateModel.cs
Normal file
325
DiscordChatExporter.Core/Services/ExportService.TemplateModel.cs
Normal file
|
@ -0,0 +1,325 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using DiscordChatExporter.Core.Internal;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Scriban.Runtime;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class ExportService
|
||||
{
|
||||
private class TemplateModel
|
||||
{
|
||||
private readonly ExportFormat _format;
|
||||
private readonly ChatLog _log;
|
||||
private readonly string _dateFormat;
|
||||
|
||||
public TemplateModel(ExportFormat format, ChatLog log, string dateFormat)
|
||||
{
|
||||
_format = format;
|
||||
_log = log;
|
||||
_dateFormat = dateFormat;
|
||||
}
|
||||
|
||||
private string HtmlEncode(string str) => WebUtility.HtmlEncode(str);
|
||||
|
||||
private string HtmlDecode(string str) => WebUtility.HtmlDecode(str);
|
||||
|
||||
private string Format(IFormattable obj, string format) =>
|
||||
obj.ToString(format, CultureInfo.InvariantCulture);
|
||||
|
||||
private string FormatDate(DateTime dateTime) => Format(dateTime, _dateFormat);
|
||||
|
||||
private string FormatFileSize(long fileSize)
|
||||
{
|
||||
string[] units = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
|
||||
double size = fileSize;
|
||||
var unit = 0;
|
||||
|
||||
while (size >= 1024)
|
||||
{
|
||||
size /= 1024;
|
||||
++unit;
|
||||
}
|
||||
|
||||
return $"{size:0.#} {units[unit]}";
|
||||
}
|
||||
|
||||
private string FormatColor(Color color)
|
||||
{
|
||||
return $"{color.R},{color.G},{color.B},{color.A}";
|
||||
}
|
||||
|
||||
private string FormatContentPlainText(string content)
|
||||
{
|
||||
// New lines
|
||||
content = content.Replace("\n", Environment.NewLine);
|
||||
|
||||
// User mentions (<@id> and <@!id>)
|
||||
var mentionedUserIds = Regex.Matches(content, "<@!?(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.ToArray();
|
||||
|
||||
foreach (var mentionedUserId in mentionedUserIds)
|
||||
{
|
||||
var mentionedUser = _log.Mentionables.GetUser(mentionedUserId);
|
||||
content = Regex.Replace(content, $"<@!?{mentionedUserId}>", $"@{mentionedUser.FullName}");
|
||||
}
|
||||
|
||||
// Channel mentions (<#id>)
|
||||
var mentionedChannelIds = Regex.Matches(content, "<#(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.ToArray();
|
||||
|
||||
foreach (var mentionedChannelId in mentionedChannelIds)
|
||||
{
|
||||
var mentionedChannel = _log.Mentionables.GetChannel(mentionedChannelId);
|
||||
content = content.Replace($"<#{mentionedChannelId}>", $"#{mentionedChannel.Name}");
|
||||
}
|
||||
|
||||
// Role mentions (<@&id>)
|
||||
var mentionedRoleIds = Regex.Matches(content, "<@&(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.ToArray();
|
||||
|
||||
foreach (var mentionedRoleId in mentionedRoleIds)
|
||||
{
|
||||
var mentionedRole = _log.Mentionables.GetRole(mentionedRoleId);
|
||||
content = content.Replace($"<@&{mentionedRoleId}>", $"@{mentionedRole.Name}");
|
||||
}
|
||||
|
||||
// Custom emojis (<:name:id>)
|
||||
content = Regex.Replace(content, "<(:.*?:)\\d*>", "$1");
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private string FormatContentHtml(string content, bool allowLinks = false)
|
||||
{
|
||||
// HTML-encode content
|
||||
content = HtmlEncode(content);
|
||||
|
||||
// Encode multiline codeblocks (```text```)
|
||||
content = Regex.Replace(content,
|
||||
@"```+(?:[^`]*?\n)?([^`]+)\n?```+",
|
||||
m => $"\x1AM{m.Groups[1].Value.Base64Encode()}\x1AM");
|
||||
|
||||
// Encode inline codeblocks (`text`)
|
||||
content = Regex.Replace(content,
|
||||
@"`([^`]+)`",
|
||||
m => $"\x1AI{m.Groups[1].Value.Base64Encode()}\x1AI");
|
||||
|
||||
// Encode links
|
||||
if (allowLinks)
|
||||
{
|
||||
content = Regex.Replace(content, @"\[(.*?)\]\((.*?)\)",
|
||||
m => $"\x1AL{m.Groups[1].Value.Base64Encode()}|{m.Groups[2].Value.Base64Encode()}\x1AL");
|
||||
}
|
||||
|
||||
// Encode URLs
|
||||
content = Regex.Replace(content,
|
||||
@"(\b(?:(?:https?|ftp|file)://|www\.|ftp\.)(?:\([-a-zA-Z0-9+&@#/%?=~_|!:,\.\[\];]*\)|[-a-zA-Z0-9+&@#/%?=~_|!:,\.\[\];])*(?:\([-a-zA-Z0-9+&@#/%?=~_|!:,\.\[\];]*\)|[-a-zA-Z0-9+&@#/%=~_|$]))",
|
||||
m => $"\x1AU{m.Groups[1].Value.Base64Encode()}\x1AU");
|
||||
|
||||
// Process bold (**text**)
|
||||
content = Regex.Replace(content, @"(\*\*)(?=\S)(.+?[*_]*)(?<=\S)\1", "<b>$2</b>");
|
||||
|
||||
// Process underline (__text__)
|
||||
content = Regex.Replace(content, @"(__)(?=\S)(.+?)(?<=\S)\1", "<u>$2</u>");
|
||||
|
||||
// Process italic (*text* or _text_)
|
||||
content = Regex.Replace(content, @"(\*|_)(?=\S)(.+?)(?<=\S)\1", "<i>$2</i>");
|
||||
|
||||
// Process strike through (~~text~~)
|
||||
content = Regex.Replace(content, @"(~~)(?=\S)(.+?)(?<=\S)\1", "<s>$2</s>");
|
||||
|
||||
// Decode and process multiline codeblocks
|
||||
content = Regex.Replace(content, "\x1AM(.*?)\x1AM",
|
||||
m => $"<div class=\"pre-multiline\">{m.Groups[1].Value.Base64Decode()}</div>");
|
||||
|
||||
// Decode and process inline codeblocks
|
||||
content = Regex.Replace(content, "\x1AI(.*?)\x1AI",
|
||||
m => $"<span class=\"pre-inline\">{m.Groups[1].Value.Base64Decode()}</span>");
|
||||
|
||||
// Decode and process links
|
||||
if (allowLinks)
|
||||
{
|
||||
content = Regex.Replace(content, "\x1AL(.*?)\\|(.*?)\x1AL",
|
||||
m => $"<a href=\"{m.Groups[2].Value.Base64Decode()}\">{m.Groups[1].Value.Base64Decode()}</a>");
|
||||
}
|
||||
|
||||
// Decode and process URLs
|
||||
content = Regex.Replace(content, "\x1AU(.*?)\x1AU",
|
||||
m => $"<a href=\"{m.Groups[1].Value.Base64Decode()}\">{m.Groups[1].Value.Base64Decode()}</a>");
|
||||
|
||||
// Process new lines
|
||||
content = content.Replace("\n", "<br />");
|
||||
|
||||
// Meta mentions (@everyone)
|
||||
content = content.Replace("@everyone", "<span class=\"mention\">@everyone</span>");
|
||||
|
||||
// Meta mentions (@here)
|
||||
content = content.Replace("@here", "<span class=\"mention\">@here</span>");
|
||||
|
||||
// User mentions (<@id> and <@!id>)
|
||||
var mentionedUserIds = Regex.Matches(content, "<@!?(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.ToArray();
|
||||
|
||||
foreach (var mentionedUserId in mentionedUserIds)
|
||||
{
|
||||
var mentionedUser = _log.Mentionables.GetUser(mentionedUserId);
|
||||
content = Regex.Replace(content, $"<@!?{mentionedUserId}>",
|
||||
$"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser.FullName)}\">" +
|
||||
$"@{HtmlEncode(mentionedUser.Name)}" +
|
||||
"</span>");
|
||||
}
|
||||
|
||||
// Channel mentions (<#id>)
|
||||
var mentionedChannelIds = Regex.Matches(content, "<#(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.ToArray();
|
||||
|
||||
foreach (var mentionedChannelId in mentionedChannelIds)
|
||||
{
|
||||
var mentionedChannel = _log.Mentionables.GetChannel(mentionedChannelId);
|
||||
content = content.Replace($"<#{mentionedChannelId}>",
|
||||
"<span class=\"mention\">" +
|
||||
$"#{HtmlEncode(mentionedChannel.Name)}" +
|
||||
"</span>");
|
||||
}
|
||||
|
||||
// Role mentions (<@&id>)
|
||||
var mentionedRoleIds = Regex.Matches(content, "<@&(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.ToArray();
|
||||
|
||||
foreach (var mentionedRoleId in mentionedRoleIds)
|
||||
{
|
||||
var mentionedRole = _log.Mentionables.GetRole(mentionedRoleId);
|
||||
content = content.Replace($"<@&{mentionedRoleId}>",
|
||||
"<span class=\"mention\">" +
|
||||
$"@{HtmlEncode(mentionedRole.Name)}" +
|
||||
"</span>");
|
||||
}
|
||||
|
||||
// Custom emojis (<:name:id>)
|
||||
content = Regex.Replace(content, "<(:.*?:)(\\d*)>",
|
||||
"<img class=\"emoji\" title=\"$1\" src=\"https://cdn.discordapp.com/emojis/$2.png\" />");
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private string FormatContentCsv(string content)
|
||||
{
|
||||
// New lines
|
||||
content = content.Replace("\n", ", ");
|
||||
|
||||
// Escape quotes
|
||||
content = content.Replace("\"", "\"\"");
|
||||
|
||||
// Escape commas and semicolons
|
||||
if (content.Contains(",") || content.Contains(";"))
|
||||
content = $"\"{content}\"";
|
||||
|
||||
// User mentions (<@id> and <@!id>)
|
||||
var mentionedUserIds = Regex.Matches(content, "<@!?(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.ToArray();
|
||||
|
||||
foreach (var mentionedUserId in mentionedUserIds)
|
||||
{
|
||||
var mentionedUser = _log.Mentionables.GetUser(mentionedUserId);
|
||||
content = Regex.Replace(content, $"<@!?{mentionedUserId}>", $"@{mentionedUser.FullName}");
|
||||
}
|
||||
|
||||
// Channel mentions (<#id>)
|
||||
var mentionedChannelIds = Regex.Matches(content, "<#(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.ToArray();
|
||||
|
||||
foreach (var mentionedChannelId in mentionedChannelIds)
|
||||
{
|
||||
var mentionedChannel = _log.Mentionables.GetChannel(mentionedChannelId);
|
||||
content = content.Replace($"<#{mentionedChannelId}>", $"#{mentionedChannel.Name}");
|
||||
}
|
||||
|
||||
// Role mentions (<@&id>)
|
||||
var mentionedRoleIds = Regex.Matches(content, "<@&(\\d+)>")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.ExceptBlank()
|
||||
.ToArray();
|
||||
|
||||
foreach (var mentionedRoleId in mentionedRoleIds)
|
||||
{
|
||||
var mentionedRole = _log.Mentionables.GetRole(mentionedRoleId);
|
||||
content = content.Replace($"<@&{mentionedRoleId}>", $"@{mentionedRole.Name}");
|
||||
}
|
||||
|
||||
// Custom emojis (<:name:id>)
|
||||
content = Regex.Replace(content, "<(:.*?:)\\d*>", "$1");
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private string FormatContent(string content, bool allowLinks = false)
|
||||
{
|
||||
if (_format == ExportFormat.PlainText)
|
||||
return FormatContentPlainText(content);
|
||||
|
||||
if (_format == ExportFormat.HtmlDark)
|
||||
return FormatContentHtml(content, allowLinks);
|
||||
|
||||
if (_format == ExportFormat.HtmlLight)
|
||||
return FormatContentHtml(content, allowLinks);
|
||||
|
||||
if (_format == ExportFormat.Csv)
|
||||
return FormatContentCsv(content);
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(_format));
|
||||
}
|
||||
|
||||
public ScriptObject GetScriptObject()
|
||||
{
|
||||
// Create instance
|
||||
var scriptObject = new ScriptObject();
|
||||
|
||||
// Import chat log
|
||||
scriptObject.Import(_log, TemplateMemberFilter, TemplateMemberRenamer);
|
||||
|
||||
// Import functions
|
||||
scriptObject.Import(nameof(HtmlEncode), new Func<string, string>(HtmlEncode));
|
||||
scriptObject.Import(nameof(HtmlDecode), new Func<string, string>(HtmlDecode));
|
||||
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));
|
||||
scriptObject.Import(nameof(FormatColor), new Func<Color, string>(FormatColor));
|
||||
scriptObject.Import(nameof(FormatContent), new Func<string, bool, string>(FormatContent));
|
||||
|
||||
return scriptObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +1,15 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Internal;
|
||||
using System.IO;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Tyrrrz.Extensions;
|
||||
using Scriban;
|
||||
using Scriban.Runtime;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public partial class ExportService : IExportService
|
||||
{
|
||||
private static readonly MemberRenamerDelegate TemplateMemberRenamer = m => m.Name;
|
||||
private static readonly MemberFilterDelegate TemplateMemberFilter = m => true;
|
||||
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public ExportService(ISettingsService settingsService)
|
||||
|
@ -18,69 +17,36 @@ namespace DiscordChatExporter.Core.Services
|
|||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log)
|
||||
public void Export(ExportFormat format, string filePath, ChatLog log)
|
||||
{
|
||||
// Create template loader
|
||||
var loader = new TemplateLoader();
|
||||
|
||||
// Get template
|
||||
var templateCode = loader.Load(format);
|
||||
var template = Template.Parse(templateCode);
|
||||
|
||||
// Create template context
|
||||
var context = new TemplateContext
|
||||
{
|
||||
TemplateLoader = loader,
|
||||
MemberRenamer = TemplateMemberRenamer,
|
||||
MemberFilter = TemplateMemberFilter
|
||||
};
|
||||
|
||||
// Create template model
|
||||
var templateModel = new TemplateModel(format, log, _settingsService.DateFormat);
|
||||
context.PushGlobal(templateModel.GetScriptObject());
|
||||
|
||||
// Render output
|
||||
using (var output = File.CreateText(filePath))
|
||||
{
|
||||
var sharedCss = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceString("DiscordChatExporter.Core.Resources.ExportService.Shared.css");
|
||||
// Configure output
|
||||
context.PushOutput(new TextWriterOutput(output));
|
||||
|
||||
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, $"{sharedCss}\n{css}");
|
||||
}
|
||||
else if (format == ExportFormat.HtmlLight)
|
||||
{
|
||||
var css = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceString("DiscordChatExporter.Core.Resources.ExportService.LightTheme.css");
|
||||
await ExportAsHtmlAsync(log, output, $"{sharedCss}\n{css}");
|
||||
}
|
||||
else if (format == ExportFormat.Csv)
|
||||
{
|
||||
await ExportAsCsvAsync(log, output);
|
||||
}
|
||||
|
||||
else throw new ArgumentOutOfRangeException(nameof(format));
|
||||
// Render template
|
||||
template.Render(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ExportService
|
||||
{
|
||||
private static string Base64Encode(string str)
|
||||
{
|
||||
return str.GetBytes().ToBase64();
|
||||
}
|
||||
|
||||
private static string Base64Decode(string str)
|
||||
{
|
||||
return str.FromBase64().GetString();
|
||||
}
|
||||
|
||||
private static string HtmlEncode(string str)
|
||||
{
|
||||
return WebUtility.HtmlEncode(str);
|
||||
}
|
||||
|
||||
private static string FormatFileSize(long fileSize)
|
||||
{
|
||||
string[] units = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
|
||||
double size = fileSize;
|
||||
var unit = 0;
|
||||
|
||||
while (size >= 1024)
|
||||
{
|
||||
size /= 1024;
|
||||
++unit;
|
||||
}
|
||||
|
||||
return $"{size:0.#} {units[unit]}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,13 +11,18 @@ namespace DiscordChatExporter.Core.Services
|
|||
|
||||
Task<Channel> GetChannelAsync(string token, string channelId);
|
||||
|
||||
Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string token, string guildId);
|
||||
|
||||
Task<IReadOnlyList<Guild>> GetUserGuildsAsync(string token);
|
||||
|
||||
Task<IReadOnlyList<Channel>> GetDirectMessageChannelsAsync(string token);
|
||||
|
||||
Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string token, string guildId);
|
||||
|
||||
Task<IReadOnlyList<Role>> GetGuildRolesAsync(string token, string guildId);
|
||||
|
||||
Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
|
||||
DateTime? from, DateTime? to);
|
||||
|
||||
Task<Mentionables> GetMentionablesAsync(string token, string guildId,
|
||||
IEnumerable<Message> messages);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
|
||||
namespace DiscordChatExporter.Core.Services
|
||||
{
|
||||
public interface IExportService
|
||||
{
|
||||
Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log);
|
||||
void Export(ExportFormat format, string filePath, ChatLog log);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,6 @@ namespace DiscordChatExporter.Core.Services
|
|||
{
|
||||
public interface IMessageGroupService
|
||||
{
|
||||
IReadOnlyList<MessageGroup> GroupMessages(IReadOnlyList<Message> messages);
|
||||
IReadOnlyList<MessageGroup> GroupMessages(IEnumerable<Message> messages);
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ namespace DiscordChatExporter.Core.Services
|
|||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public IReadOnlyList<MessageGroup> GroupMessages(IReadOnlyList<Message> messages)
|
||||
public IReadOnlyList<MessageGroup> GroupMessages(IEnumerable<Message> messages)
|
||||
{
|
||||
var result = new List<MessageGroup>();
|
||||
|
||||
|
@ -28,15 +28,15 @@ namespace DiscordChatExporter.Core.Services
|
|||
groupFirst != null &&
|
||||
(
|
||||
message.Author.Id != groupFirst.Author.Id ||
|
||||
(message.TimeStamp - groupFirst.TimeStamp).TotalHours > 1 ||
|
||||
message.TimeStamp.Hour != groupFirst.TimeStamp.Hour ||
|
||||
(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());
|
||||
var group = new MessageGroup(groupFirst.Author, groupFirst.Timestamp, groupBuffer.ToArray());
|
||||
result.Add(group);
|
||||
groupBuffer.Clear();
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ namespace DiscordChatExporter.Core.Services
|
|||
if (groupBuffer.Any())
|
||||
{
|
||||
var groupFirst = groupBuffer.First();
|
||||
var group = new MessageGroup(groupFirst.Author, groupFirst.TimeStamp, groupBuffer.ToArray());
|
||||
var group = new MessageGroup(groupFirst.Author, groupFirst.Timestamp, groupBuffer.ToArray());
|
||||
result.Add(group);
|
||||
}
|
||||
|
||||
|
|
|
@ -68,8 +68,8 @@
|
|||
<Reference Include="System.Xaml">
|
||||
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="Tyrrrz.Extensions, Version=1.5.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Tyrrrz.Extensions.1.5.0\lib\net45\Tyrrrz.Extensions.dll</HintPath>
|
||||
<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">
|
||||
|
|
|
@ -72,7 +72,7 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||
set
|
||||
{
|
||||
Set(ref _selectedGuild, value);
|
||||
AvailableChannels = value != null ? _guildChannelsMap[value] : new Channel[0];
|
||||
AvailableChannels = value != null ? _guildChannelsMap[value] : Array.Empty<Channel>();
|
||||
ShowExportSetupCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
@ -222,19 +222,25 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||
// Get last used token
|
||||
var token = _settingsService.LastToken;
|
||||
|
||||
// Get guild
|
||||
var guild = SelectedGuild;
|
||||
|
||||
try
|
||||
{
|
||||
// Get messages
|
||||
var messages = await _dataService.GetChannelMessagesAsync(token, channel.Id, from, to);
|
||||
|
||||
// Group them
|
||||
// Group messages
|
||||
var messageGroups = _messageGroupService.GroupMessages(messages);
|
||||
|
||||
// Get mentionables
|
||||
var mentionables = await _dataService.GetMentionablesAsync(token, guild.Id, messages);
|
||||
|
||||
// Create log
|
||||
var log = new ChannelChatLog(SelectedGuild, channel, messageGroups, messages.Count);
|
||||
var log = new ChatLog(guild, channel, messageGroups, mentionables);
|
||||
|
||||
// Export
|
||||
await _exportService.ExportAsync(format, filePath, log);
|
||||
_exportService.Export(format, filePath, log);
|
||||
|
||||
// Open
|
||||
Process.Start(filePath);
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
<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.0" targetFramework="net461" />
|
||||
<package id="Tyrrrz.Extensions" version="1.5.1" targetFramework="net461" />
|
||||
<package id="Tyrrrz.WpfExtensions" version="1.0.5" targetFramework="net461" />
|
||||
</packages>
|
|
@ -27,6 +27,7 @@ DiscordChatExporter can be used to export message history from a [Discord](https
|
|||
- Dark and light themes
|
||||
- User avatars
|
||||
- Inline image attachments
|
||||
- Embeds and webhooks
|
||||
- Full markdown support
|
||||
- Automatic links
|
||||
- Styled similarly to the app
|
||||
|
@ -39,7 +40,7 @@ DiscordChatExporter can be used to export message history from a [Discord](https
|
|||
- [GalaSoft.MVVMLight](http://www.mvvmlight.net)
|
||||
- [MaterialDesignInXamlToolkit](https://github.com/ButchersBoy/MaterialDesignInXamlToolkit)
|
||||
- [Newtonsoft.Json](http://www.newtonsoft.com/json)
|
||||
- [CsvHelper](https://github.com/JoshClose/CsvHelper)
|
||||
- [Scriban](https://github.com/lunet-io/scriban)
|
||||
- [Onova](https://github.com/Tyrrrz/Onova)
|
||||
- [FluentCommandLineParser](https://github.com/fclp/fluent-command-line-parser)
|
||||
- [Tyrrrz.Extensions](https://github.com/Tyrrrz/Extensions)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue