mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-23 11:16:59 -04:00
Add mention support (#17)
* Support for user mentions and incomplete support for role mentions * Improve formatting a bit * Implement a hack to get roles
This commit is contained in:
parent
171718989a
commit
4d5118de76
7 changed files with 146 additions and 12 deletions
|
@ -92,6 +92,7 @@
|
||||||
<Compile Include="Models\ChannelType.cs" />
|
<Compile Include="Models\ChannelType.cs" />
|
||||||
<Compile Include="Models\ExportFormat.cs" />
|
<Compile Include="Models\ExportFormat.cs" />
|
||||||
<Compile Include="Models\Extensions.cs" />
|
<Compile Include="Models\Extensions.cs" />
|
||||||
|
<Compile Include="Models\Role.cs" />
|
||||||
<Compile Include="Models\MessageType.cs" />
|
<Compile Include="Models\MessageType.cs" />
|
||||||
<Compile Include="Services\IMessageGroupService.cs" />
|
<Compile Include="Services\IMessageGroupService.cs" />
|
||||||
<Compile Include="Services\MessageGroupService.cs" />
|
<Compile Include="Services\MessageGroupService.cs" />
|
||||||
|
|
|
@ -17,9 +17,14 @@ namespace DiscordChatExporter.Models
|
||||||
|
|
||||||
public IReadOnlyList<Attachment> Attachments { get; }
|
public IReadOnlyList<Attachment> Attachments { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<User> MentionedUsers { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Role> MentionedRoles { get; }
|
||||||
|
|
||||||
public Message(string id, User author,
|
public Message(string id, User author,
|
||||||
DateTime timeStamp, DateTime? editedTimeStamp,
|
DateTime timeStamp, DateTime? editedTimeStamp,
|
||||||
string content, IReadOnlyList<Attachment> attachments)
|
string content, IReadOnlyList<Attachment> attachments,
|
||||||
|
IReadOnlyList<User> mentionedUsers, IReadOnlyList<Role> mentionedRoles)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Author = author;
|
Author = author;
|
||||||
|
@ -27,6 +32,8 @@ namespace DiscordChatExporter.Models
|
||||||
EditedTimeStamp = editedTimeStamp;
|
EditedTimeStamp = editedTimeStamp;
|
||||||
Content = content;
|
Content = content;
|
||||||
Attachments = attachments;
|
Attachments = attachments;
|
||||||
|
MentionedUsers = mentionedUsers;
|
||||||
|
MentionedRoles = mentionedRoles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|
20
DiscordChatExporter/Models/Role.cs
Normal file
20
DiscordChatExporter/Models/Role.cs
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -123,3 +123,7 @@ img.emoji {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
vertical-align: -.4em;
|
vertical-align: -.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.mention {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
|
@ -123,3 +123,7 @@ img.emoji {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
vertical-align: -.4em;
|
vertical-align: -.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.mention {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
|
@ -13,7 +13,9 @@ namespace DiscordChatExporter.Services
|
||||||
public partial class DataService : IDataService, IDisposable
|
public partial class DataService : IDataService, IDisposable
|
||||||
{
|
{
|
||||||
private const string ApiRoot = "https://discordapp.com/api/v6";
|
private const string ApiRoot = "https://discordapp.com/api/v6";
|
||||||
|
|
||||||
private readonly HttpClient _httpClient = new HttpClient();
|
private readonly HttpClient _httpClient = new HttpClient();
|
||||||
|
private readonly Dictionary<string, Role> _rolesCache = new Dictionary<string, Role>();
|
||||||
|
|
||||||
private async Task<string> GetStringAsync(string url)
|
private async Task<string> GetStringAsync(string url)
|
||||||
{
|
{
|
||||||
|
@ -29,6 +31,20 @@ namespace DiscordChatExporter.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<Role>> 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<IReadOnlyList<Guild>> GetGuildsAsync(string token)
|
public async Task<IReadOnlyList<Guild>> GetGuildsAsync(string token)
|
||||||
{
|
{
|
||||||
// Form request url
|
// Form request url
|
||||||
|
@ -40,6 +56,14 @@ namespace DiscordChatExporter.Services
|
||||||
// Parse
|
// Parse
|
||||||
var guilds = JArray.Parse(content).Select(ParseGuild).ToArray();
|
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;
|
return guilds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +114,7 @@ namespace DiscordChatExporter.Services
|
||||||
var content = await GetStringAsync(url);
|
var content = await GetStringAsync(url);
|
||||||
|
|
||||||
// Parse
|
// Parse
|
||||||
var messages = JArray.Parse(content).Select(ParseMessage);
|
var messages = JArray.Parse(content).Select(j => ParseMessage(j, _rolesCache));
|
||||||
|
|
||||||
// Add messages to list
|
// Add messages to list
|
||||||
string currentMessageId = null;
|
string currentMessageId = null;
|
||||||
|
@ -141,6 +165,15 @@ namespace DiscordChatExporter.Services
|
||||||
|
|
||||||
public partial class DataService
|
public partial class DataService
|
||||||
{
|
{
|
||||||
|
private static Guild ParseGuild(JToken token)
|
||||||
|
{
|
||||||
|
var id = token.Value<string>("id");
|
||||||
|
var name = token.Value<string>("name");
|
||||||
|
var iconHash = token.Value<string>("icon");
|
||||||
|
|
||||||
|
return new Guild(id, name, iconHash);
|
||||||
|
}
|
||||||
|
|
||||||
private static User ParseUser(JToken token)
|
private static User ParseUser(JToken token)
|
||||||
{
|
{
|
||||||
var id = token.Value<string>("id");
|
var id = token.Value<string>("id");
|
||||||
|
@ -151,13 +184,12 @@ namespace DiscordChatExporter.Services
|
||||||
return new User(id, discriminator, name, avatarHash);
|
return new User(id, discriminator, name, avatarHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Guild ParseGuild(JToken token)
|
private static Role ParseRole(JToken token)
|
||||||
{
|
{
|
||||||
var id = token.Value<string>("id");
|
var id = token.Value<string>("id");
|
||||||
var name = token.Value<string>("name");
|
var name = token.Value<string>("name");
|
||||||
var iconHash = token.Value<string>("icon");
|
|
||||||
|
|
||||||
return new Guild(id, name, iconHash);
|
return new Role(id, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Channel ParseChannel(JToken token)
|
private static Channel ParseChannel(JToken token)
|
||||||
|
@ -181,7 +213,7 @@ namespace DiscordChatExporter.Services
|
||||||
return new Channel(id, name, type);
|
return new Channel(id, name, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Message ParseMessage(JToken token)
|
private static Message ParseMessage(JToken token, IDictionary<string, Role> roles)
|
||||||
{
|
{
|
||||||
// Get basic data
|
// Get basic data
|
||||||
var id = token.Value<string>("id");
|
var id = token.Value<string>("id");
|
||||||
|
@ -227,7 +259,15 @@ namespace DiscordChatExporter.Services
|
||||||
attachments.Add(attachment);
|
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<string>()
|
||||||
|
.Select(i => roles.GetOrDefault(i) ?? new Role(i, "deleted-role"))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new Message(id, author, timeStamp, editedTimeStamp, content, attachments,
|
||||||
|
mentionedUsers, mentionedRoles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
@ -46,7 +47,7 @@ namespace DiscordChatExporter.Services
|
||||||
// Content
|
// Content
|
||||||
if (message.Content.IsNotBlank())
|
if (message.Content.IsNotBlank())
|
||||||
{
|
{
|
||||||
var contentFormatted = message.Content.Replace("\n", Environment.NewLine);
|
var contentFormatted = FormatMessageContentText(message);
|
||||||
await writer.WriteLineAsync(contentFormatted);
|
await writer.WriteLineAsync(contentFormatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +120,7 @@ namespace DiscordChatExporter.Services
|
||||||
if (message.Content.IsNotBlank())
|
if (message.Content.IsNotBlank())
|
||||||
{
|
{
|
||||||
await writer.WriteLineAsync("<div class=\"msg-content\">");
|
await writer.WriteLineAsync("<div class=\"msg-content\">");
|
||||||
var contentFormatted = FormatMessageContentHtml(message.Content);
|
var contentFormatted = FormatMessageContentHtml(message);
|
||||||
await writer.WriteAsync(contentFormatted);
|
await writer.WriteAsync(contentFormatted);
|
||||||
|
|
||||||
// Edited timestamp
|
// Edited timestamp
|
||||||
|
@ -215,8 +216,39 @@ namespace DiscordChatExporter.Services
|
||||||
return $"{size:0.#} {units[unit]}";
|
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
|
// Encode HTML
|
||||||
content = HtmlEncode(content);
|
content = HtmlEncode(content);
|
||||||
|
|
||||||
|
@ -248,9 +280,35 @@ namespace DiscordChatExporter.Services
|
||||||
// New lines
|
// New lines
|
||||||
content = content.Replace("\n", "<br />");
|
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>)
|
||||||
|
content = Regex.Replace(content, "<@(\\d*)>",
|
||||||
|
m =>
|
||||||
|
{
|
||||||
|
var mentionedUser = message.MentionedUsers.First(u => u.Id == m.Groups[1].Value);
|
||||||
|
return $"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser)}\">" +
|
||||||
|
$"@{HtmlEncode(mentionedUser.Name)}" +
|
||||||
|
"</span>";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Role mentions (<@&id>)
|
||||||
|
content = Regex.Replace(content, "<@&(\\d*)>",
|
||||||
|
m =>
|
||||||
|
{
|
||||||
|
var mentionedRole = message.MentionedRoles.First(r => r.Id == m.Groups[1].Value);
|
||||||
|
return "<span class=\"mention\">" +
|
||||||
|
$"@{HtmlEncode(mentionedRole.Name)}" +
|
||||||
|
"</span>";
|
||||||
|
});
|
||||||
|
|
||||||
// Custom emojis (<:name:id>)
|
// Custom emojis (<:name:id>)
|
||||||
content = Regex.Replace(content, "<:.*?:(\\d+)>",
|
content = Regex.Replace(content, "<(:.*?:)(\\d*)>",
|
||||||
"<img class=\"emoji\" src=\"https://cdn.discordapp.com/emojis/$1.png\" />");
|
"<img class=\"emoji\" title=\"$1\" src=\"https://cdn.discordapp.com/emojis/$2.png\" />");
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue