From 4d5118de768b3dbe9ee945ab16af4975b06bfd04 Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Thu, 26 Oct 2017 22:21:44 +0200 Subject: [PATCH] Add mention support (#17) * Support for user mentions and incomplete support for role mentions * Improve formatting a bit * Implement a hack to get roles --- .../DiscordChatExporter.csproj | 1 + DiscordChatExporter/Models/Message.cs | 9 ++- DiscordChatExporter/Models/Role.cs | 20 ++++++ .../Resources/ExportService/DarkTheme.css | 4 ++ .../Resources/ExportService/LightTheme.css | 4 ++ DiscordChatExporter/Services/DataService.cs | 52 ++++++++++++-- DiscordChatExporter/Services/ExportService.cs | 68 +++++++++++++++++-- 7 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 DiscordChatExporter/Models/Role.cs diff --git a/DiscordChatExporter/DiscordChatExporter.csproj b/DiscordChatExporter/DiscordChatExporter.csproj index b206f0a..10a932c 100644 --- a/DiscordChatExporter/DiscordChatExporter.csproj +++ b/DiscordChatExporter/DiscordChatExporter.csproj @@ -92,6 +92,7 @@ + diff --git a/DiscordChatExporter/Models/Message.cs b/DiscordChatExporter/Models/Message.cs index a672333..c54d195 100644 --- a/DiscordChatExporter/Models/Message.cs +++ b/DiscordChatExporter/Models/Message.cs @@ -17,9 +17,14 @@ namespace DiscordChatExporter.Models public IReadOnlyList Attachments { get; } + public IReadOnlyList MentionedUsers { get; } + + public IReadOnlyList MentionedRoles { get; } + public Message(string id, User author, DateTime timeStamp, DateTime? editedTimeStamp, - string content, IReadOnlyList attachments) + string content, IReadOnlyList attachments, + IReadOnlyList mentionedUsers, IReadOnlyList mentionedRoles) { Id = id; Author = author; @@ -27,6 +32,8 @@ namespace DiscordChatExporter.Models EditedTimeStamp = editedTimeStamp; Content = content; Attachments = attachments; + MentionedUsers = mentionedUsers; + MentionedRoles = mentionedRoles; } public override string ToString() diff --git a/DiscordChatExporter/Models/Role.cs b/DiscordChatExporter/Models/Role.cs new file mode 100644 index 0000000..a714f5e --- /dev/null +++ b/DiscordChatExporter/Models/Role.cs @@ -0,0 +1,20 @@ +namespace DiscordChatExporter.Models +{ + public class Role + { + public string Id { get; } + + public string Name { get; } + + public Role(string id, string name) + { + Id = id; + Name = name; + } + + public override string ToString() + { + return Name; + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter/Resources/ExportService/DarkTheme.css b/DiscordChatExporter/Resources/ExportService/DarkTheme.css index fcef534..1fef534 100644 --- a/DiscordChatExporter/Resources/ExportService/DarkTheme.css +++ b/DiscordChatExporter/Resources/ExportService/DarkTheme.css @@ -122,4 +122,8 @@ img.emoji { height: 24px; width: 24px; vertical-align: -.4em; +} + +span.mention { + font-weight: 600; } \ No newline at end of file diff --git a/DiscordChatExporter/Resources/ExportService/LightTheme.css b/DiscordChatExporter/Resources/ExportService/LightTheme.css index 1d6e135..be60c25 100644 --- a/DiscordChatExporter/Resources/ExportService/LightTheme.css +++ b/DiscordChatExporter/Resources/ExportService/LightTheme.css @@ -122,4 +122,8 @@ img.emoji { height: 24px; width: 24px; vertical-align: -.4em; +} + +span.mention { + font-weight: 600; } \ No newline at end of file diff --git a/DiscordChatExporter/Services/DataService.cs b/DiscordChatExporter/Services/DataService.cs index 711b3bf..b639630 100644 --- a/DiscordChatExporter/Services/DataService.cs +++ b/DiscordChatExporter/Services/DataService.cs @@ -13,7 +13,9 @@ namespace DiscordChatExporter.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 _rolesCache = new Dictionary(); private async Task GetStringAsync(string url) { @@ -29,6 +31,20 @@ namespace DiscordChatExporter.Services } } + private async Task> GetGuildRolesAsync(string token, string guildId) + { + // Form request url + var url = $"{ApiRoot}/guilds/{guildId}?token={token}"; + + // Get response + var content = await GetStringAsync(url); + + // Parse + var roles = JToken.Parse(content)["roles"].Select(ParseRole).ToArray(); + + return roles; + } + public async Task> GetGuildsAsync(string token) { // Form request url @@ -40,6 +56,14 @@ namespace DiscordChatExporter.Services // Parse var guilds = JArray.Parse(content).Select(ParseGuild).ToArray(); + // HACK: also get roles for all of them + foreach (var guild in guilds) + { + var roles = await GetGuildRolesAsync(token, guild.Id); + foreach (var role in roles) + _rolesCache[role.Id] = role; + } + return guilds; } @@ -90,7 +114,7 @@ namespace DiscordChatExporter.Services var content = await GetStringAsync(url); // Parse - var messages = JArray.Parse(content).Select(ParseMessage); + var messages = JArray.Parse(content).Select(j => ParseMessage(j, _rolesCache)); // Add messages to list string currentMessageId = null; @@ -141,6 +165,15 @@ namespace DiscordChatExporter.Services public partial class DataService { + private static Guild ParseGuild(JToken token) + { + var id = token.Value("id"); + var name = token.Value("name"); + var iconHash = token.Value("icon"); + + return new Guild(id, name, iconHash); + } + private static User ParseUser(JToken token) { var id = token.Value("id"); @@ -151,13 +184,12 @@ namespace DiscordChatExporter.Services return new User(id, discriminator, name, avatarHash); } - private static Guild ParseGuild(JToken token) + private static Role ParseRole(JToken token) { var id = token.Value("id"); var name = token.Value("name"); - var iconHash = token.Value("icon"); - return new Guild(id, name, iconHash); + return new Role(id, name); } private static Channel ParseChannel(JToken token) @@ -181,7 +213,7 @@ namespace DiscordChatExporter.Services return new Channel(id, name, type); } - private static Message ParseMessage(JToken token) + private static Message ParseMessage(JToken token, IDictionary roles) { // Get basic data var id = token.Value("id"); @@ -227,7 +259,15 @@ namespace DiscordChatExporter.Services attachments.Add(attachment); } - return new Message(id, author, timeStamp, editedTimeStamp, content, attachments); + // Get mentions + var mentionedUsers = token["mentions"].Select(ParseUser).ToArray(); + var mentionedRoles = token["mention_roles"] + .Values() + .Select(i => roles.GetOrDefault(i) ?? new Role(i, "deleted-role")) + .ToArray(); + + return new Message(id, author, timeStamp, editedTimeStamp, content, attachments, + mentionedUsers, mentionedRoles); } } } \ No newline at end of file diff --git a/DiscordChatExporter/Services/ExportService.cs b/DiscordChatExporter/Services/ExportService.cs index 9ace7c5..a435427 100644 --- a/DiscordChatExporter/Services/ExportService.cs +++ b/DiscordChatExporter/Services/ExportService.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; @@ -46,7 +47,7 @@ namespace DiscordChatExporter.Services // Content if (message.Content.IsNotBlank()) { - var contentFormatted = message.Content.Replace("\n", Environment.NewLine); + var contentFormatted = FormatMessageContentText(message); await writer.WriteLineAsync(contentFormatted); } @@ -119,7 +120,7 @@ namespace DiscordChatExporter.Services if (message.Content.IsNotBlank()) { await writer.WriteLineAsync("
"); - var contentFormatted = FormatMessageContentHtml(message.Content); + var contentFormatted = FormatMessageContentHtml(message); await writer.WriteAsync(contentFormatted); // Edited timestamp @@ -215,8 +216,39 @@ namespace DiscordChatExporter.Services return $"{size:0.#} {units[unit]}"; } - private static string FormatMessageContentHtml(string content) + public static string FormatMessageContentText(Message message) { + var content = message.Content; + + // New lines + content = content.Replace("\n", Environment.NewLine); + + // User mentions (<@id>) + content = Regex.Replace(content, "<@(\\d*)>", + m => + { + var mentionedUser = message.MentionedUsers.First(u => u.Id == m.Groups[1].Value); + return $"@{mentionedUser}"; + }); + + // Role mentions (<@&id>) + content = Regex.Replace(content, "<@&(\\d*)>", + m => + { + var mentionedRole = message.MentionedRoles.First(r => r.Id == m.Groups[1].Value); + return $"@{mentionedRole.Name}"; + }); + + // Custom emojis (<:name:id>) + content = Regex.Replace(content, "<(:.*?:)\\d*>", "$1"); + + return content; + } + + private static string FormatMessageContentHtml(Message message) + { + var content = message.Content; + // Encode HTML content = HtmlEncode(content); @@ -248,9 +280,35 @@ namespace DiscordChatExporter.Services // New lines content = content.Replace("\n", "
"); + // Meta mentions (@everyone) + content = content.Replace("@everyone", "@everyone"); + + // Meta mentions (@here) + content = content.Replace("@here", "@here"); + + // User mentions (<@id>) + content = Regex.Replace(content, "<@(\\d*)>", + m => + { + var mentionedUser = message.MentionedUsers.First(u => u.Id == m.Groups[1].Value); + return $"" + + $"@{HtmlEncode(mentionedUser.Name)}" + + ""; + }); + + // Role mentions (<@&id>) + content = Regex.Replace(content, "<@&(\\d*)>", + m => + { + var mentionedRole = message.MentionedRoles.First(r => r.Id == m.Groups[1].Value); + return "" + + $"@{HtmlEncode(mentionedRole.Name)}" + + ""; + }); + // Custom emojis (<:name:id>) - content = Regex.Replace(content, "<:.*?:(\\d+)>", - ""); + content = Regex.Replace(content, "<(:.*?:)(\\d*)>", + ""); return content; }