mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-23 11:16:59 -04:00
More refactoring
This commit is contained in:
parent
9d0d7cd5dd
commit
d03be8b1dd
43 changed files with 617 additions and 655 deletions
|
@ -1,8 +1,9 @@
|
||||||
using System.Linq;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
|
@ -11,10 +12,8 @@ namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var directMessageChannels = await GetDiscordClient().GetDirectMessageChannelsAsync();
|
var dmChannels = await GetDiscordClient().GetGuildChannelsAsync(Guild.DirectMessages.Id);
|
||||||
var channels = directMessageChannels.OrderBy(c => c.Name).ToArray();
|
await ExportMultipleAsync(console, dmChannels);
|
||||||
|
|
||||||
await ExportMultipleAsync(console, channels);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
using System.Linq;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
|
@ -15,13 +15,7 @@ namespace DiscordChatExporter.Cli.Commands
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var guildChannels = await GetDiscordClient().GetGuildChannelsAsync(GuildId);
|
var guildChannels = await GetDiscordClient().GetGuildChannelsAsync(GuildId);
|
||||||
|
await ExportMultipleAsync(console, guildChannels);
|
||||||
var channels = guildChannels
|
|
||||||
.Where(c => c.IsTextChannel)
|
|
||||||
.OrderBy(c => c.Name)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
await ExportMultipleAsync(console, channels);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
|
@ -16,12 +17,7 @@ namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
var guildChannels = await GetDiscordClient().GetGuildChannelsAsync(GuildId);
|
var guildChannels = await GetDiscordClient().GetGuildChannelsAsync(GuildId);
|
||||||
|
|
||||||
var channels = guildChannels
|
foreach (var channel in guildChannels.OrderBy(c => c.Name))
|
||||||
.Where(c => c.IsTextChannel)
|
|
||||||
.OrderBy(c => c.Name)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach (var channel in channels)
|
|
||||||
console.Output.WriteLine($"{channel.Id} | {channel.Name}");
|
console.Output.WriteLine($"{channel.Id} | {channel.Name}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ using System.Threading.Tasks;
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
|
@ -11,10 +13,9 @@ namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var directMessageChannels = await GetDiscordClient().GetDirectMessageChannelsAsync();
|
var dmChannels = await GetDiscordClient().GetGuildChannelsAsync(Guild.DirectMessages.Id);
|
||||||
var channels = directMessageChannels.OrderBy(c => c.Name).ToArray();
|
|
||||||
|
|
||||||
foreach (var channel in channels)
|
foreach (var channel in dmChannels.OrderBy(c => c.Name))
|
||||||
console.Output.WriteLine($"{channel.Id} | {channel.Name}");
|
console.Output.WriteLine($"{channel.Id} | {channel.Name}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,232 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
|
||||||
using DiscordChatExporter.Domain.Internal;
|
|
||||||
using Tyrrrz.Extensions;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord
|
|
||||||
{
|
|
||||||
public partial class DiscordClient
|
|
||||||
{
|
|
||||||
private string ParseId(JsonElement json) =>
|
|
||||||
json.GetProperty("id").GetString();
|
|
||||||
|
|
||||||
private User ParseUser(JsonElement json)
|
|
||||||
{
|
|
||||||
var id = ParseId(json);
|
|
||||||
var discriminator = json.GetProperty("discriminator").GetString().Pipe(int.Parse);
|
|
||||||
var name = json.GetProperty("username").GetString();
|
|
||||||
var avatarHash = json.GetProperty("avatar").GetString();
|
|
||||||
var isBot = json.GetPropertyOrNull("bot")?.GetBoolean() ?? false;
|
|
||||||
|
|
||||||
return new User(id, discriminator, name, avatarHash, isBot);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Member ParseMember(JsonElement json)
|
|
||||||
{
|
|
||||||
var userId = json.GetProperty("user").Pipe(ParseId);
|
|
||||||
var nick = json.GetPropertyOrNull("nick")?.GetString();
|
|
||||||
|
|
||||||
var roleIds =
|
|
||||||
json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString()).ToArray() ??
|
|
||||||
Array.Empty<string>();
|
|
||||||
|
|
||||||
return new Member(userId, nick, roleIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Guild ParseGuild(JsonElement json)
|
|
||||||
{
|
|
||||||
var id = ParseId(json);
|
|
||||||
var name = json.GetProperty("name").GetString();
|
|
||||||
var iconHash = json.GetProperty("icon").GetString();
|
|
||||||
|
|
||||||
var roles =
|
|
||||||
json.GetPropertyOrNull("roles")?.EnumerateArray().Select(ParseRole).ToArray() ??
|
|
||||||
Array.Empty<Role>();
|
|
||||||
|
|
||||||
return new Guild(id, name, iconHash, roles);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Channel ParseChannel(JsonElement json)
|
|
||||||
{
|
|
||||||
var id = ParseId(json);
|
|
||||||
var parentId = json.GetPropertyOrNull("parent_id")?.GetString();
|
|
||||||
var type = (ChannelType) json.GetProperty("type").GetInt32();
|
|
||||||
var topic = json.GetPropertyOrNull("topic")?.GetString();
|
|
||||||
|
|
||||||
var guildId =
|
|
||||||
json.GetPropertyOrNull("guild_id")?.GetString() ??
|
|
||||||
Guild.DirectMessages.Id;
|
|
||||||
|
|
||||||
var name =
|
|
||||||
json.GetPropertyOrNull("name")?.GetString() ??
|
|
||||||
json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(ParseUser).Select(u => u.Name).JoinToString(", ") ??
|
|
||||||
id;
|
|
||||||
|
|
||||||
return new Channel(id, guildId, parentId, type, name, topic);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Role ParseRole(JsonElement json)
|
|
||||||
{
|
|
||||||
var id = ParseId(json);
|
|
||||||
var name = json.GetProperty("name").GetString();
|
|
||||||
var color = json.GetPropertyOrNull("color")?.GetInt32().Pipe(Color.FromArgb).ResetAlpha().NullIf(c => c.ToRgb() <= 0);
|
|
||||||
var position = json.GetProperty("position").GetInt32();
|
|
||||||
|
|
||||||
return new Role(id, name, color, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Attachment ParseAttachment(JsonElement json)
|
|
||||||
{
|
|
||||||
var id = ParseId(json);
|
|
||||||
var url = json.GetProperty("url").GetString();
|
|
||||||
var width = json.GetPropertyOrNull("width")?.GetInt32();
|
|
||||||
var height = json.GetPropertyOrNull("height")?.GetInt32();
|
|
||||||
var fileName = json.GetProperty("filename").GetString();
|
|
||||||
var fileSize = json.GetProperty("size").GetInt64().Pipe(FileSize.FromBytes);
|
|
||||||
|
|
||||||
return new Attachment(id, url, fileName, width, height, fileSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
private EmbedAuthor ParseEmbedAuthor(JsonElement json)
|
|
||||||
{
|
|
||||||
var name = json.GetPropertyOrNull("name")?.GetString();
|
|
||||||
var url = json.GetPropertyOrNull("url")?.GetString();
|
|
||||||
var iconUrl = json.GetPropertyOrNull("icon_url")?.GetString();
|
|
||||||
|
|
||||||
return new EmbedAuthor(name, url, iconUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private EmbedField ParseEmbedField(JsonElement json)
|
|
||||||
{
|
|
||||||
var name = json.GetProperty("name").GetString();
|
|
||||||
var value = json.GetProperty("value").GetString();
|
|
||||||
var isInline = json.GetPropertyOrNull("inline")?.GetBoolean() ?? false;
|
|
||||||
|
|
||||||
return new EmbedField(name, value, isInline);
|
|
||||||
}
|
|
||||||
|
|
||||||
private EmbedImage ParseEmbedImage(JsonElement json)
|
|
||||||
{
|
|
||||||
var url = json.GetPropertyOrNull("url")?.GetString();
|
|
||||||
var width = json.GetPropertyOrNull("width")?.GetInt32();
|
|
||||||
var height = json.GetPropertyOrNull("height")?.GetInt32();
|
|
||||||
|
|
||||||
return new EmbedImage(url, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private EmbedFooter ParseEmbedFooter(JsonElement json)
|
|
||||||
{
|
|
||||||
var text = json.GetProperty("text").GetString();
|
|
||||||
var iconUrl = json.GetPropertyOrNull("icon_url")?.GetString();
|
|
||||||
|
|
||||||
return new EmbedFooter(text, iconUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Embed ParseEmbed(JsonElement json)
|
|
||||||
{
|
|
||||||
var title = json.GetPropertyOrNull("title")?.GetString();
|
|
||||||
var description = json.GetPropertyOrNull("description")?.GetString();
|
|
||||||
var url = json.GetPropertyOrNull("url")?.GetString();
|
|
||||||
var timestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset();
|
|
||||||
var color = json.GetPropertyOrNull("color")?.GetInt32().Pipe(Color.FromArgb).ResetAlpha();
|
|
||||||
|
|
||||||
var author = json.GetPropertyOrNull("author")?.Pipe(ParseEmbedAuthor);
|
|
||||||
var thumbnail = json.GetPropertyOrNull("thumbnail")?.Pipe(ParseEmbedImage);
|
|
||||||
var image = json.GetPropertyOrNull("image")?.Pipe(ParseEmbedImage);
|
|
||||||
var footer = json.GetPropertyOrNull("footer")?.Pipe(ParseEmbedFooter);
|
|
||||||
|
|
||||||
var fields =
|
|
||||||
json.GetPropertyOrNull("fields")?.EnumerateArray().Select(ParseEmbedField).ToArray() ??
|
|
||||||
Array.Empty<EmbedField>();
|
|
||||||
|
|
||||||
return new Embed(
|
|
||||||
title,
|
|
||||||
url,
|
|
||||||
timestamp,
|
|
||||||
color,
|
|
||||||
author,
|
|
||||||
description,
|
|
||||||
fields,
|
|
||||||
thumbnail,
|
|
||||||
image,
|
|
||||||
footer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Emoji ParseEmoji(JsonElement json)
|
|
||||||
{
|
|
||||||
var id = json.GetPropertyOrNull("id")?.GetString();
|
|
||||||
var name = json.GetProperty("name").GetString();
|
|
||||||
var isAnimated = json.GetPropertyOrNull("animated")?.GetBoolean() ?? false;
|
|
||||||
|
|
||||||
return new Emoji(id, name, isAnimated);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Reaction ParseReaction(JsonElement json)
|
|
||||||
{
|
|
||||||
var count = json.GetProperty("count").GetInt32();
|
|
||||||
var emoji = json.GetProperty("emoji").Pipe(ParseEmoji);
|
|
||||||
|
|
||||||
return new Reaction(emoji, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Message ParseMessage(JsonElement json)
|
|
||||||
{
|
|
||||||
var id = ParseId(json);
|
|
||||||
var channelId = json.GetProperty("channel_id").GetString();
|
|
||||||
var timestamp = json.GetProperty("timestamp").GetDateTimeOffset();
|
|
||||||
var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffset();
|
|
||||||
var type = (MessageType) json.GetProperty("type").GetInt32();
|
|
||||||
var isPinned = json.GetPropertyOrNull("pinned")?.GetBoolean() ?? false;
|
|
||||||
|
|
||||||
var content = type switch
|
|
||||||
{
|
|
||||||
MessageType.RecipientAdd => "Added a recipient.",
|
|
||||||
MessageType.RecipientRemove => "Removed a recipient.",
|
|
||||||
MessageType.Call => "Started a call.",
|
|
||||||
MessageType.ChannelNameChange => "Changed the channel name.",
|
|
||||||
MessageType.ChannelIconChange => "Changed the channel icon.",
|
|
||||||
MessageType.ChannelPinnedMessage => "Pinned a message.",
|
|
||||||
MessageType.GuildMemberJoin => "Joined the server.",
|
|
||||||
_ => json.GetPropertyOrNull("content")?.GetString() ?? ""
|
|
||||||
};
|
|
||||||
|
|
||||||
var author = json.GetProperty("author").Pipe(ParseUser);
|
|
||||||
|
|
||||||
var attachments =
|
|
||||||
json.GetPropertyOrNull("attachments")?.EnumerateArray().Select(ParseAttachment).ToArray() ??
|
|
||||||
Array.Empty<Attachment>();
|
|
||||||
|
|
||||||
var embeds =
|
|
||||||
json.GetPropertyOrNull("embeds")?.EnumerateArray().Select(ParseEmbed).ToArray() ??
|
|
||||||
Array.Empty<Embed>();
|
|
||||||
|
|
||||||
var reactions =
|
|
||||||
json.GetPropertyOrNull("reactions")?.EnumerateArray().Select(ParseReaction).ToArray() ??
|
|
||||||
Array.Empty<Reaction>();
|
|
||||||
|
|
||||||
var mentionedUsers =
|
|
||||||
json.GetPropertyOrNull("mentions")?.EnumerateArray().Select(ParseUser).ToArray() ??
|
|
||||||
Array.Empty<User>();
|
|
||||||
|
|
||||||
return new Message(
|
|
||||||
id,
|
|
||||||
channelId,
|
|
||||||
type,
|
|
||||||
author,
|
|
||||||
timestamp,
|
|
||||||
editedTimestamp,
|
|
||||||
isPinned,
|
|
||||||
content,
|
|
||||||
attachments,
|
|
||||||
embeds,
|
|
||||||
reactions,
|
|
||||||
mentionedUsers
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -68,7 +68,6 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
return await response.Content.ReadAsJsonAsync();
|
return await response.Content.ReadAsJsonAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: do we need this?
|
|
||||||
private async Task<JsonElement?> TryGetApiResponseAsync(string url)
|
private async Task<JsonElement?> TryGetApiResponseAsync(string url)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -81,36 +80,11 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Guild> GetGuildAsync(string guildId)
|
|
||||||
{
|
|
||||||
// Special case for direct messages pseudo-guild
|
|
||||||
if (guildId == Guild.DirectMessages.Id)
|
|
||||||
return Guild.DirectMessages;
|
|
||||||
|
|
||||||
var response = await GetApiResponseAsync($"guilds/{guildId}");
|
|
||||||
var guild = ParseGuild(response);
|
|
||||||
|
|
||||||
return guild;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Member?> GetGuildMemberAsync(string guildId, string userId)
|
|
||||||
{
|
|
||||||
var response = await TryGetApiResponseAsync($"guilds/{guildId}/members/{userId}");
|
|
||||||
return response?.Pipe(ParseMember);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Channel> GetChannelAsync(string channelId)
|
|
||||||
{
|
|
||||||
var response = await GetApiResponseAsync($"channels/{channelId}");
|
|
||||||
var channel = ParseChannel(response);
|
|
||||||
|
|
||||||
return channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async IAsyncEnumerable<Guild> GetUserGuildsAsync()
|
public async IAsyncEnumerable<Guild> GetUserGuildsAsync()
|
||||||
{
|
{
|
||||||
var afterId = "";
|
yield return Guild.DirectMessages;
|
||||||
|
|
||||||
|
var afterId = "";
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var url = new UrlBuilder()
|
var url = new UrlBuilder()
|
||||||
|
@ -122,15 +96,12 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
var response = await GetApiResponseAsync(url);
|
var response = await GetApiResponseAsync(url);
|
||||||
|
|
||||||
var isEmpty = true;
|
var isEmpty = true;
|
||||||
|
|
||||||
// Get full guild object
|
|
||||||
foreach (var guildJson in response.EnumerateArray())
|
foreach (var guildJson in response.EnumerateArray())
|
||||||
{
|
{
|
||||||
var guildId = ParseId(guildJson);
|
var guild = Guild.Parse(guildJson);
|
||||||
|
yield return guild;
|
||||||
yield return await GetGuildAsync(guildId);
|
|
||||||
afterId = guildId;
|
|
||||||
|
|
||||||
|
afterId = guild.Id;
|
||||||
isEmpty = false;
|
isEmpty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,27 +110,93 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Channel>> GetDirectMessageChannelsAsync()
|
public async Task<Guild> GetGuildAsync(string guildId)
|
||||||
|
{
|
||||||
|
if (guildId == Guild.DirectMessages.Id)
|
||||||
|
return Guild.DirectMessages;
|
||||||
|
|
||||||
|
var response = await GetApiResponseAsync($"guilds/{guildId}");
|
||||||
|
return Guild.Parse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<Channel> GetGuildChannelsAsync(string guildId)
|
||||||
|
{
|
||||||
|
if (guildId == Guild.DirectMessages.Id)
|
||||||
{
|
{
|
||||||
var response = await GetApiResponseAsync("users/@me/channels");
|
var response = await GetApiResponseAsync("users/@me/channels");
|
||||||
var channels = response.EnumerateArray().Select(ParseChannel).ToArray();
|
foreach (var channelJson in response.EnumerateArray())
|
||||||
|
yield return Channel.Parse(channelJson);
|
||||||
return channels;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
public async Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string guildId)
|
|
||||||
{
|
{
|
||||||
// Direct messages pseudo-guild
|
|
||||||
if (guildId == Guild.DirectMessages.Id)
|
|
||||||
return Array.Empty<Channel>();
|
|
||||||
|
|
||||||
var response = await GetApiResponseAsync($"guilds/{guildId}/channels");
|
var response = await GetApiResponseAsync($"guilds/{guildId}/channels");
|
||||||
var channels = response.EnumerateArray().Select(ParseChannel).ToArray();
|
|
||||||
|
|
||||||
return channels;
|
var categories = response
|
||||||
|
.EnumerateArray()
|
||||||
|
.ToDictionary(
|
||||||
|
j => j.GetProperty("id").GetString(),
|
||||||
|
j => j.GetProperty("name").GetString()
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var channelJson in response.EnumerateArray())
|
||||||
|
{
|
||||||
|
var parentId = channelJson.GetPropertyOrNull("parent_id")?.GetString();
|
||||||
|
var category = !string.IsNullOrWhiteSpace(parentId)
|
||||||
|
? categories.GetValueOrDefault(parentId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var channel = Channel.Parse(channelJson, category);
|
||||||
|
|
||||||
|
// Skip non-text channels
|
||||||
|
if (!channel.IsTextChannel)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
yield return channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Message> GetLastMessageAsync(string channelId, DateTimeOffset? before = null)
|
public async IAsyncEnumerable<Role> GetGuildRolesAsync(string guildId)
|
||||||
|
{
|
||||||
|
if (guildId == Guild.DirectMessages.Id)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
var response = await GetApiResponseAsync($"guilds/{guildId}/roles");
|
||||||
|
|
||||||
|
foreach (var roleJson in response.EnumerateArray())
|
||||||
|
{
|
||||||
|
yield return Role.Parse(roleJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Member?> TryGetGuildMemberAsync(string guildId, User user)
|
||||||
|
{
|
||||||
|
if (guildId == Guild.DirectMessages.Id)
|
||||||
|
return Member.CreateForUser(user);
|
||||||
|
|
||||||
|
var response = await TryGetApiResponseAsync($"guilds/{guildId}/members/{user.Id}");
|
||||||
|
return response?.Pipe(Member.Parse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetChannelCategoryAsync(string channelParentId)
|
||||||
|
{
|
||||||
|
var response = await GetApiResponseAsync($"channels/{channelParentId}");
|
||||||
|
return response.GetProperty("name").GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Channel> GetChannelAsync(string channelId)
|
||||||
|
{
|
||||||
|
var response = await GetApiResponseAsync($"channels/{channelId}");
|
||||||
|
|
||||||
|
var parentId = response.GetPropertyOrNull("parent_id")?.GetString();
|
||||||
|
var category = !string.IsNullOrWhiteSpace(parentId)
|
||||||
|
? await GetChannelCategoryAsync(parentId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return Channel.Parse(response, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Message?> TryGetLastMessageAsync(string channelId, DateTimeOffset? before = null)
|
||||||
{
|
{
|
||||||
var url = new UrlBuilder()
|
var url = new UrlBuilder()
|
||||||
.SetPath($"channels/{channelId}/messages")
|
.SetPath($"channels/{channelId}/messages")
|
||||||
|
@ -168,8 +205,7 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var response = await GetApiResponseAsync(url);
|
var response = await GetApiResponseAsync(url);
|
||||||
|
return response.EnumerateArray().Select(Message.Parse).LastOrDefault();
|
||||||
return response.EnumerateArray().Select(ParseMessage).FirstOrDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<Message> GetMessagesAsync(
|
public async IAsyncEnumerable<Message> GetMessagesAsync(
|
||||||
|
@ -178,9 +214,8 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
DateTimeOffset? before = null,
|
DateTimeOffset? before = null,
|
||||||
IProgress<double>? progress = null)
|
IProgress<double>? progress = null)
|
||||||
{
|
{
|
||||||
var lastMessage = await GetLastMessageAsync(channelId, before);
|
// Get the last message in the specified range
|
||||||
|
var lastMessage = await TryGetLastMessageAsync(channelId, before);
|
||||||
// If the last message doesn't exist or it's outside of range - return
|
|
||||||
if (lastMessage == null || lastMessage.Timestamp < after)
|
if (lastMessage == null || lastMessage.Timestamp < after)
|
||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
|
@ -199,13 +234,13 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
|
|
||||||
var messages = response
|
var messages = response
|
||||||
.EnumerateArray()
|
.EnumerateArray()
|
||||||
.Select(ParseMessage)
|
.Select(Message.Parse)
|
||||||
.Reverse() // reverse because messages appear newest first
|
.Reverse() // reverse because messages appear newest first
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
// Break if there are no messages (can happen if messages are deleted during execution)
|
// Break if there are no messages (can happen if messages are deleted during execution)
|
||||||
if (!messages.Any())
|
if (!messages.Any())
|
||||||
break;
|
yield break;
|
||||||
|
|
||||||
foreach (var message in messages)
|
foreach (var message in messages)
|
||||||
{
|
{
|
||||||
|
@ -223,10 +258,6 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
|
|
||||||
yield return message;
|
yield return message;
|
||||||
afterId = message.Id;
|
afterId = message.Id;
|
||||||
|
|
||||||
// Yielded last message - break loop
|
|
||||||
if (message.Id == lastMessage.Id)
|
|
||||||
yield break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/channel#attachment-object
|
// https://discordapp.com/developers/docs/resources/channel#attachment-object
|
||||||
|
|
||||||
public partial class Attachment : IHasId
|
public partial class Attachment : IHasId
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
|
@ -19,9 +20,11 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public int? Height { get; }
|
public int? Height { get; }
|
||||||
|
|
||||||
public bool IsImage => ImageFileExtensions.Contains(Path.GetExtension(FileName), StringComparer.OrdinalIgnoreCase);
|
public bool IsImage =>
|
||||||
|
ImageFileExtensions.Contains(Path.GetExtension(FileName), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public bool IsSpoiler => IsImage && FileName.StartsWith("SPOILER_", StringComparison.Ordinal);
|
public bool IsSpoiler =>
|
||||||
|
IsImage && FileName.StartsWith("SPOILER_", StringComparison.Ordinal);
|
||||||
|
|
||||||
public FileSize FileSize { get; }
|
public FileSize FileSize { get; }
|
||||||
|
|
||||||
|
@ -41,5 +44,17 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
public partial class Attachment
|
public partial class Attachment
|
||||||
{
|
{
|
||||||
private static readonly string[] ImageFileExtensions = {".jpg", ".jpeg", ".png", ".gif", ".bmp"};
|
private static readonly string[] ImageFileExtensions = {".jpg", ".jpeg", ".png", ".gif", ".bmp"};
|
||||||
|
|
||||||
|
public static Attachment Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var id = json.GetProperty("id").GetString();
|
||||||
|
var url = json.GetProperty("url").GetString();
|
||||||
|
var width = json.GetPropertyOrNull("width")?.GetInt32();
|
||||||
|
var height = json.GetPropertyOrNull("height")?.GetInt32();
|
||||||
|
var fileName = json.GetProperty("filename").GetString();
|
||||||
|
var fileSize = json.GetProperty("size").GetInt64().Pipe(FileSize.FromBytes);
|
||||||
|
|
||||||
|
return new Attachment(id, url, fileName, width, height, fileSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/channel#channel-object-channel-types
|
// https://discordapp.com/developers/docs/resources/channel#channel-object-channel-types
|
||||||
// Order of enum fields needs to match the order in the docs.
|
// Order of enum fields needs to match the order in the docs.
|
||||||
|
|
||||||
public enum ChannelType
|
public enum ChannelType
|
||||||
{
|
{
|
||||||
GuildTextChat,
|
GuildTextChat,
|
||||||
|
@ -17,15 +20,10 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://discordapp.com/developers/docs/resources/channel#channel-object
|
// https://discordapp.com/developers/docs/resources/channel#channel-object
|
||||||
|
|
||||||
public partial class Channel : IHasId
|
public partial class Channel : IHasId
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
|
|
||||||
public string GuildId { get; }
|
|
||||||
|
|
||||||
public string? ParentId { get; }
|
|
||||||
|
|
||||||
public ChannelType Type { get; }
|
public ChannelType Type { get; }
|
||||||
|
|
||||||
public bool IsTextChannel =>
|
public bool IsTextChannel =>
|
||||||
|
@ -35,16 +33,20 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
Type == ChannelType.GuildNews ||
|
Type == ChannelType.GuildNews ||
|
||||||
Type == ChannelType.GuildStore;
|
Type == ChannelType.GuildStore;
|
||||||
|
|
||||||
|
public string GuildId { get; }
|
||||||
|
|
||||||
|
public string Category { get; }
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public string? Topic { get; }
|
public string? Topic { get; }
|
||||||
|
|
||||||
public Channel(string id, string guildId, string? parentId, ChannelType type, string name, string? topic)
|
public Channel(string id, ChannelType type, string guildId, string category, string name, string? topic)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
GuildId = guildId;
|
|
||||||
ParentId = parentId;
|
|
||||||
Type = type;
|
Type = type;
|
||||||
|
GuildId = guildId;
|
||||||
|
Category = category;
|
||||||
Name = name;
|
Name = name;
|
||||||
Topic = topic;
|
Topic = topic;
|
||||||
}
|
}
|
||||||
|
@ -54,7 +56,36 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public partial class Channel
|
public partial class Channel
|
||||||
{
|
{
|
||||||
public static Channel CreateDeletedChannel(string id) =>
|
private static string GetDefaultCategory(ChannelType channelType) => channelType switch
|
||||||
new Channel(id, "unknown-guild", null, ChannelType.GuildTextChat, "deleted-channel", null);
|
{
|
||||||
|
ChannelType.GuildTextChat => "Text",
|
||||||
|
ChannelType.DirectTextChat => "Private",
|
||||||
|
ChannelType.DirectGroupTextChat => "Group",
|
||||||
|
ChannelType.GuildNews => "News",
|
||||||
|
ChannelType.GuildStore => "Store",
|
||||||
|
_ => "Default"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Channel Parse(JsonElement json, string? category = null)
|
||||||
|
{
|
||||||
|
var id = json.GetProperty("id").GetString();
|
||||||
|
var guildId = json.GetPropertyOrNull("guild_id")?.GetString();
|
||||||
|
var topic = json.GetPropertyOrNull("topic")?.GetString();
|
||||||
|
|
||||||
|
var type = (ChannelType) json.GetProperty("type").GetInt32();
|
||||||
|
|
||||||
|
var name =
|
||||||
|
json.GetPropertyOrNull("name")?.GetString() ??
|
||||||
|
json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(User.Parse).Select(u => u.Name).JoinToString(", ") ??
|
||||||
|
id;
|
||||||
|
|
||||||
|
return new Channel(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
guildId ?? Guild.DirectMessages.Id,
|
||||||
|
category ?? GetDefaultCategory(type),
|
||||||
|
name,
|
||||||
|
topic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,7 +3,6 @@
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models.Common
|
namespace DiscordChatExporter.Domain.Discord.Models.Common
|
||||||
{
|
{
|
||||||
// Loosely based on https://github.com/omar/ByteSize (MIT license)
|
// Loosely based on https://github.com/omar/ByteSize (MIT license)
|
||||||
|
|
||||||
public readonly partial struct FileSize
|
public readonly partial struct FileSize
|
||||||
{
|
{
|
||||||
public long TotalBytes { get; }
|
public long TotalBytes { get; }
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/channel#embed-object
|
// https://discordapp.com/developers/docs/resources/channel#embed-object
|
||||||
|
public partial class Embed
|
||||||
public class Embed
|
|
||||||
{
|
{
|
||||||
public string? Title { get; }
|
public string? Title { get; }
|
||||||
|
|
||||||
|
@ -54,4 +56,38 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public override string ToString() => Title ?? "<untitled embed>";
|
public override string ToString() => Title ?? "<untitled embed>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class Embed
|
||||||
|
{
|
||||||
|
public static Embed Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var title = json.GetPropertyOrNull("title")?.GetString();
|
||||||
|
var description = json.GetPropertyOrNull("description")?.GetString();
|
||||||
|
var url = json.GetPropertyOrNull("url")?.GetString();
|
||||||
|
var timestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset();
|
||||||
|
var color = json.GetPropertyOrNull("color")?.GetInt32().Pipe(System.Drawing.Color.FromArgb).ResetAlpha();
|
||||||
|
|
||||||
|
var author = json.GetPropertyOrNull("author")?.Pipe(EmbedAuthor.Parse);
|
||||||
|
var thumbnail = json.GetPropertyOrNull("thumbnail")?.Pipe(EmbedImage.Parse);
|
||||||
|
var image = json.GetPropertyOrNull("image")?.Pipe(EmbedImage.Parse);
|
||||||
|
var footer = json.GetPropertyOrNull("footer")?.Pipe(EmbedFooter.Parse);
|
||||||
|
|
||||||
|
var fields =
|
||||||
|
json.GetPropertyOrNull("fields")?.EnumerateArray().Select(EmbedField.Parse).ToArray() ??
|
||||||
|
Array.Empty<EmbedField>();
|
||||||
|
|
||||||
|
return new Embed(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
timestamp,
|
||||||
|
color,
|
||||||
|
author,
|
||||||
|
description,
|
||||||
|
fields,
|
||||||
|
thumbnail,
|
||||||
|
image,
|
||||||
|
footer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-author-structure
|
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-author-structure
|
||||||
|
public partial class EmbedAuthor
|
||||||
public class EmbedAuthor
|
|
||||||
{
|
{
|
||||||
public string? Name { get; }
|
public string? Name { get; }
|
||||||
|
|
||||||
|
@ -19,4 +21,16 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public override string ToString() => Name ?? "<unnamed author>";
|
public override string ToString() => Name ?? "<unnamed author>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class EmbedAuthor
|
||||||
|
{
|
||||||
|
public static EmbedAuthor Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var name = json.GetPropertyOrNull("name")?.GetString();
|
||||||
|
var url = json.GetPropertyOrNull("url")?.GetString();
|
||||||
|
var iconUrl = json.GetPropertyOrNull("icon_url")?.GetString();
|
||||||
|
|
||||||
|
return new EmbedAuthor(name, url, iconUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-field-structure
|
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-field-structure
|
||||||
|
public partial class EmbedField
|
||||||
public class EmbedField
|
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
|
@ -19,4 +21,16 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public override string ToString() => $"{Name} | {Value}";
|
public override string ToString() => $"{Name} | {Value}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class EmbedField
|
||||||
|
{
|
||||||
|
public static EmbedField Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var name = json.GetProperty("name").GetString();
|
||||||
|
var value = json.GetProperty("value").GetString();
|
||||||
|
var isInline = json.GetPropertyOrNull("inline")?.GetBoolean() ?? false;
|
||||||
|
|
||||||
|
return new EmbedField(name, value, isInline);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-footer-structure
|
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-footer-structure
|
||||||
|
public partial class EmbedFooter
|
||||||
public class EmbedFooter
|
|
||||||
{
|
{
|
||||||
public string Text { get; }
|
public string Text { get; }
|
||||||
|
|
||||||
|
@ -16,4 +18,15 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public override string ToString() => Text;
|
public override string ToString() => Text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class EmbedFooter
|
||||||
|
{
|
||||||
|
public static EmbedFooter Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var text = json.GetProperty("text").GetString();
|
||||||
|
var iconUrl = json.GetPropertyOrNull("icon_url")?.GetString();
|
||||||
|
|
||||||
|
return new EmbedFooter(text, iconUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-image-structure
|
// https://discordapp.com/developers/docs/resources/channel#embed-object-embed-image-structure
|
||||||
|
public partial class EmbedImage
|
||||||
public class EmbedImage
|
|
||||||
{
|
{
|
||||||
public string? Url { get; }
|
public string? Url { get; }
|
||||||
|
|
||||||
|
@ -17,4 +19,16 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
Width = width;
|
Width = width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class EmbedImage
|
||||||
|
{
|
||||||
|
public static EmbedImage Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var url = json.GetPropertyOrNull("url")?.GetString();
|
||||||
|
var width = json.GetPropertyOrNull("width")?.GetInt32();
|
||||||
|
var height = json.GetPropertyOrNull("height")?.GetInt32();
|
||||||
|
|
||||||
|
return new EmbedImage(url, width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/emoji#emoji-object
|
// https://discordapp.com/developers/docs/resources/emoji#emoji-object
|
||||||
|
|
||||||
public partial class Emoji
|
public partial class Emoji
|
||||||
{
|
{
|
||||||
public string? Id { get; }
|
public string? Id { get; }
|
||||||
|
@ -60,12 +61,19 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
return $"https://cdn.discordapp.com/emojis/{id}.png";
|
return $"https://cdn.discordapp.com/emojis/{id}.png";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get runes
|
// Standard emoji
|
||||||
var emojiRunes = GetRunes(name).ToArray();
|
var emojiRunes = GetRunes(name).ToArray();
|
||||||
|
|
||||||
// Get corresponding Twemoji image
|
|
||||||
var twemojiName = GetTwemojiName(emojiRunes);
|
var twemojiName = GetTwemojiName(emojiRunes);
|
||||||
return $"https://twemoji.maxcdn.com/2/72x72/{twemojiName}.png";
|
return $"https://twemoji.maxcdn.com/2/72x72/{twemojiName}.png";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Emoji Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var id = json.GetPropertyOrNull("id")?.GetString();
|
||||||
|
var name = json.GetProperty("name").GetString();
|
||||||
|
var isAnimated = json.GetPropertyOrNull("animated")?.GetBoolean() ?? false;
|
||||||
|
|
||||||
|
return new Emoji(id, name, isAnimated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,36 +1,23 @@
|
||||||
using System;
|
using System.Text.Json;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
using DiscordChatExporter.Domain.Internal;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/guild#guild-object
|
// https://discordapp.com/developers/docs/resources/guild#guild-object
|
||||||
|
|
||||||
public partial class Guild : IHasId
|
public partial class Guild : IHasId
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public string? IconHash { get; }
|
public string IconUrl { get; }
|
||||||
|
|
||||||
public string IconUrl => !string.IsNullOrWhiteSpace(IconHash)
|
public Guild(string id, string name, string? iconHash)
|
||||||
? $"https://cdn.discordapp.com/icons/{Id}/{IconHash}.png"
|
|
||||||
: "https://cdn.discordapp.com/embed/avatars/0.png";
|
|
||||||
|
|
||||||
public IReadOnlyList<Role> Roles { get; }
|
|
||||||
|
|
||||||
public Dictionary<string, Member?> Members { get; }
|
|
||||||
|
|
||||||
public Guild(string id, string name, string? iconHash, IReadOnlyList<Role> roles)
|
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
IconHash = iconHash;
|
|
||||||
Roles = roles;
|
IconUrl = GetIconUrl(id, iconHash);
|
||||||
Members = new Dictionary<string, Member?>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public override string ToString() => Name;
|
||||||
|
@ -38,18 +25,21 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public partial class Guild
|
public partial class Guild
|
||||||
{
|
{
|
||||||
public static string GetUserColor(Guild guild, User user) =>
|
private static string GetIconUrl(string? id, string? iconHash) =>
|
||||||
guild.Members.GetValueOrDefault(user.Id, null)?
|
!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(iconHash)
|
||||||
.RoleIds
|
? $"https://cdn.discordapp.com/icons/{id}/{iconHash}.png"
|
||||||
.Select(r => guild.Roles.FirstOrDefault(role => r == role.Id))
|
: "https://cdn.discordapp.com/embed/avatars/0.png";
|
||||||
.Where(r => r != null)
|
|
||||||
.Where(r => r.Color != null)
|
|
||||||
.Aggregate<Role, Role?>(null, (a, b) => (a?.Position ?? 0) > b.Position ? a : b)?
|
|
||||||
.Color?
|
|
||||||
.ToHexString() ?? "";
|
|
||||||
|
|
||||||
public static string GetUserNick(Guild guild, User user) => guild.Members.GetValueOrDefault(user.Id)?.Nick ?? user.Name;
|
public static Guild DirectMessages { get; } =
|
||||||
|
new Guild("@me", "Direct Messages", null);
|
||||||
|
|
||||||
public static Guild DirectMessages { get; } = new Guild("@me", "Direct Messages", null, Array.Empty<Role>());
|
public static Guild Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var id = json.GetProperty("id").GetString();
|
||||||
|
var name = json.GetProperty("name").GetString();
|
||||||
|
var iconHash = json.GetProperty("icon").GetString();
|
||||||
|
|
||||||
|
return new Guild(id, name, iconHash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,22 +1,52 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/guild#guild-member-object
|
// https://discordapp.com/developers/docs/resources/guild#guild-member-object
|
||||||
|
public partial class Member : IHasId
|
||||||
public class Member
|
|
||||||
{
|
{
|
||||||
public string UserId { get; }
|
public string Id => User.Id;
|
||||||
|
|
||||||
public string? Nick { get; }
|
public User User { get; }
|
||||||
|
|
||||||
|
public string Nick { get; }
|
||||||
|
|
||||||
public IReadOnlyList<string> RoleIds { get; }
|
public IReadOnlyList<string> RoleIds { get; }
|
||||||
|
|
||||||
public Member(string userId, string? nick, IReadOnlyList<string> roleIds)
|
public Member(User user, string? nick, IReadOnlyList<string> roleIds)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
User = user;
|
||||||
Nick = nick;
|
Nick = nick ?? user.Name;
|
||||||
RoleIds = roleIds;
|
RoleIds = roleIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString() => Nick;
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class Member
|
||||||
|
{
|
||||||
|
public static Member CreateForUser(User user) =>
|
||||||
|
new Member(user, null, Array.Empty<string>());
|
||||||
|
|
||||||
|
public static Member Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var user = json.GetProperty("user").Pipe(User.Parse);
|
||||||
|
var nick = json.GetPropertyOrNull("nick")?.GetString();
|
||||||
|
|
||||||
|
var roleIds =
|
||||||
|
json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString()).ToArray() ??
|
||||||
|
Array.Empty<string>();
|
||||||
|
|
||||||
|
return new Member(
|
||||||
|
user,
|
||||||
|
nick,
|
||||||
|
roleIds
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/channel#message-object-message-types
|
// https://discordapp.com/developers/docs/resources/channel#message-object-message-types
|
||||||
|
|
||||||
public enum MessageType
|
public enum MessageType
|
||||||
{
|
{
|
||||||
Default,
|
Default,
|
||||||
|
@ -20,13 +21,10 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://discordapp.com/developers/docs/resources/channel#message-object
|
// https://discordapp.com/developers/docs/resources/channel#message-object
|
||||||
|
public partial class Message : IHasId
|
||||||
public class Message : IHasId
|
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
|
|
||||||
public string ChannelId { get; }
|
|
||||||
|
|
||||||
public MessageType Type { get; }
|
public MessageType Type { get; }
|
||||||
|
|
||||||
public User Author { get; }
|
public User Author { get; }
|
||||||
|
@ -49,7 +47,6 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public Message(
|
public Message(
|
||||||
string id,
|
string id,
|
||||||
string channelId,
|
|
||||||
MessageType type,
|
MessageType type,
|
||||||
User author,
|
User author,
|
||||||
DateTimeOffset timestamp,
|
DateTimeOffset timestamp,
|
||||||
|
@ -62,7 +59,6 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
IReadOnlyList<User> mentionedUsers)
|
IReadOnlyList<User> mentionedUsers)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
ChannelId = channelId;
|
|
||||||
Type = type;
|
Type = type;
|
||||||
Author = author;
|
Author = author;
|
||||||
Timestamp = timestamp;
|
Timestamp = timestamp;
|
||||||
|
@ -80,4 +76,59 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
? "<embed>"
|
? "<embed>"
|
||||||
: "<no content>");
|
: "<no content>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class Message
|
||||||
|
{
|
||||||
|
public static Message Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var id = json.GetProperty("id").GetString();
|
||||||
|
var author = json.GetProperty("author").Pipe(User.Parse);
|
||||||
|
var timestamp = json.GetProperty("timestamp").GetDateTimeOffset();
|
||||||
|
var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffset();
|
||||||
|
var type = (MessageType) json.GetProperty("type").GetInt32();
|
||||||
|
var isPinned = json.GetPropertyOrNull("pinned")?.GetBoolean() ?? false;
|
||||||
|
|
||||||
|
var content = type switch
|
||||||
|
{
|
||||||
|
MessageType.RecipientAdd => "Added a recipient.",
|
||||||
|
MessageType.RecipientRemove => "Removed a recipient.",
|
||||||
|
MessageType.Call => "Started a call.",
|
||||||
|
MessageType.ChannelNameChange => "Changed the channel name.",
|
||||||
|
MessageType.ChannelIconChange => "Changed the channel icon.",
|
||||||
|
MessageType.ChannelPinnedMessage => "Pinned a message.",
|
||||||
|
MessageType.GuildMemberJoin => "Joined the server.",
|
||||||
|
_ => json.GetPropertyOrNull("content")?.GetString() ?? ""
|
||||||
|
};
|
||||||
|
|
||||||
|
var attachments =
|
||||||
|
json.GetPropertyOrNull("attachments")?.EnumerateArray().Select(Attachment.Parse).ToArray() ??
|
||||||
|
Array.Empty<Attachment>();
|
||||||
|
|
||||||
|
var embeds =
|
||||||
|
json.GetPropertyOrNull("embeds")?.EnumerateArray().Select(Embed.Parse).ToArray() ??
|
||||||
|
Array.Empty<Embed>();
|
||||||
|
|
||||||
|
var reactions =
|
||||||
|
json.GetPropertyOrNull("reactions")?.EnumerateArray().Select(Reaction.Parse).ToArray() ??
|
||||||
|
Array.Empty<Reaction>();
|
||||||
|
|
||||||
|
var mentionedUsers =
|
||||||
|
json.GetPropertyOrNull("mentions")?.EnumerateArray().Select(User.Parse).ToArray() ??
|
||||||
|
Array.Empty<User>();
|
||||||
|
|
||||||
|
return new Message(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
author,
|
||||||
|
timestamp,
|
||||||
|
editedTimestamp,
|
||||||
|
isPinned,
|
||||||
|
content,
|
||||||
|
attachments,
|
||||||
|
embeds,
|
||||||
|
reactions,
|
||||||
|
mentionedUsers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/channel#reaction-object
|
// https://discordapp.com/developers/docs/resources/channel#reaction-object
|
||||||
|
public partial class Reaction
|
||||||
public class Reaction
|
|
||||||
{
|
{
|
||||||
public Emoji Emoji { get; }
|
public Emoji Emoji { get; }
|
||||||
|
|
||||||
|
@ -16,4 +18,15 @@
|
||||||
|
|
||||||
public override string ToString() => $"{Emoji} ({Count})";
|
public override string ToString() => $"{Emoji} ({Count})";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class Reaction
|
||||||
|
{
|
||||||
|
public static Reaction Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var count = json.GetProperty("count").GetInt32();
|
||||||
|
var emoji = json.GetProperty("emoji").Pipe(Emoji.Parse);
|
||||||
|
|
||||||
|
return new Reaction(emoji, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,26 +1,26 @@
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/topics/permissions#role-object
|
// https://discordapp.com/developers/docs/topics/permissions#role-object
|
||||||
|
public partial class Role
|
||||||
public partial class Role : IHasId
|
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public Color? Color { get; }
|
|
||||||
|
|
||||||
public int Position { get; }
|
public int Position { get; }
|
||||||
|
|
||||||
public Role(string id, string name, Color? color, int position)
|
public Color? Color { get; }
|
||||||
|
|
||||||
|
public Role(string id, string name, int position, Color? color)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
Color = color;
|
|
||||||
Position = position;
|
Position = position;
|
||||||
|
Color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public override string ToString() => Name;
|
||||||
|
@ -28,6 +28,19 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public partial class Role
|
public partial class Role
|
||||||
{
|
{
|
||||||
public static Role CreateDeletedRole(string id) => new Role(id, "deleted-role", null, -1);
|
public static Role Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var id = json.GetProperty("id").GetString();
|
||||||
|
var name = json.GetProperty("name").GetString();
|
||||||
|
var position = json.GetProperty("position").GetInt32();
|
||||||
|
|
||||||
|
var color = json.GetPropertyOrNull("color")?
|
||||||
|
.GetInt32()
|
||||||
|
.Pipe(System.Drawing.Color.FromArgb)
|
||||||
|
.ResetAlpha()
|
||||||
|
.NullIf(c => c.ToRgb() <= 0);
|
||||||
|
|
||||||
|
return new Role(id, name, position, color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,33 +1,31 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discordapp.com/developers/docs/resources/user#user-object
|
// https://discordapp.com/developers/docs/resources/user#user-object
|
||||||
|
|
||||||
public partial class User : IHasId
|
public partial class User : IHasId
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
|
|
||||||
|
public bool IsBot { get; }
|
||||||
|
|
||||||
public int Discriminator { get; }
|
public int Discriminator { get; }
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public string FullName => $"{Name}#{Discriminator:0000}";
|
public string FullName => $"{Name}#{Discriminator:0000}";
|
||||||
|
|
||||||
public string? AvatarHash { get; }
|
|
||||||
|
|
||||||
public string AvatarUrl { get; }
|
public string AvatarUrl { get; }
|
||||||
|
|
||||||
public bool IsBot { get; }
|
public User(string id, bool isBot, int discriminator, string name, string? avatarHash)
|
||||||
|
|
||||||
public User(string id, int discriminator, string name, string? avatarHash, bool isBot)
|
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
|
IsBot = isBot;
|
||||||
Discriminator = discriminator;
|
Discriminator = discriminator;
|
||||||
Name = name;
|
Name = name;
|
||||||
AvatarHash = avatarHash;
|
|
||||||
IsBot = isBot;
|
|
||||||
|
|
||||||
AvatarUrl = GetAvatarUrl(id, discriminator, avatarHash);
|
AvatarUrl = GetAvatarUrl(id, discriminator, avatarHash);
|
||||||
}
|
}
|
||||||
|
@ -54,6 +52,15 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
return $"https://cdn.discordapp.com/embed/avatars/{discriminator % 5}.png";
|
return $"https://cdn.discordapp.com/embed/avatars/{discriminator % 5}.png";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static User CreateUnknownUser(string id) => new User(id, 0, "Unknown", null, false);
|
public static User Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var id = json.GetProperty("id").GetString();
|
||||||
|
var discriminator = json.GetProperty("discriminator").GetString().Pipe(int.Parse);
|
||||||
|
var name = json.GetProperty("username").GetString();
|
||||||
|
var avatarHash = json.GetProperty("avatar").GetString();
|
||||||
|
var isBot = json.GetPropertyOrNull("bot")?.GetBoolean() ?? false;
|
||||||
|
|
||||||
|
return new User(id, isBot, discriminator, name, avatarHash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -40,9 +40,7 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
buffer.Append(_path);
|
buffer.Append(_path);
|
||||||
|
|
||||||
if (_queryParameters.Any())
|
if (_queryParameters.Any())
|
||||||
buffer.Append('?');
|
buffer.Append('?').AppendJoin('&', _queryParameters.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
||||||
|
|
||||||
buffer.AppendJoin('&', _queryParameters.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
|
||||||
|
|
||||||
return buffer.ToString();
|
return buffer.ToString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Discord;
|
using DiscordChatExporter.Domain.Discord;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
using DiscordChatExporter.Domain.Exceptions;
|
using DiscordChatExporter.Domain.Exceptions;
|
||||||
using Tyrrrz.Extensions;
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting
|
namespace DiscordChatExporter.Domain.Exporting
|
||||||
{
|
{
|
||||||
|
@ -36,37 +37,35 @@ namespace DiscordChatExporter.Domain.Exporting
|
||||||
var options = new ExportOptions(baseFilePath, format, partitionLimit);
|
var options = new ExportOptions(baseFilePath, format, partitionLimit);
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
var mentionableUsers = new HashSet<User>(IdBasedEqualityComparer.Instance);
|
var contextMembers = new HashSet<Member>(IdBasedEqualityComparer.Instance);
|
||||||
var mentionableChannels = await _discord.GetGuildChannelsAsync(guild.Id);
|
var contextChannels = await _discord.GetGuildChannelsAsync(guild.Id);
|
||||||
var mentionableRoles = guild.Roles;
|
var contextRoles = await _discord.GetGuildRolesAsync(guild.Id);
|
||||||
|
|
||||||
var context = new ExportContext(
|
var context = new ExportContext(
|
||||||
guild, channel, after, before, dateFormat,
|
guild, channel, after, before, dateFormat,
|
||||||
mentionableUsers, mentionableChannels, mentionableRoles
|
contextMembers, contextChannels, contextRoles
|
||||||
);
|
);
|
||||||
|
|
||||||
await using var messageExporter = new MessageExporter(options, context);
|
await using var messageExporter = new MessageExporter(options, context);
|
||||||
|
|
||||||
var exportedAnything = false;
|
var exportedAnything = false;
|
||||||
|
var encounteredUsers = new HashSet<User>(IdBasedEqualityComparer.Instance);
|
||||||
await foreach (var message in _discord.GetMessagesAsync(channel.Id, after, before, progress))
|
await foreach (var message in _discord.GetMessagesAsync(channel.Id, after, before, progress))
|
||||||
{
|
{
|
||||||
// Add encountered users to the list of mentionable users
|
// Resolve members for referenced users
|
||||||
var encounteredUsers = new List<User>();
|
foreach (var referencedUser in message.MentionedUsers.Prepend(message.Author))
|
||||||
encounteredUsers.Add(message.Author);
|
|
||||||
encounteredUsers.AddRange(message.MentionedUsers);
|
|
||||||
|
|
||||||
mentionableUsers.AddRange(encounteredUsers);
|
|
||||||
|
|
||||||
foreach (User u in encounteredUsers)
|
|
||||||
{
|
{
|
||||||
if (!guild.Members.ContainsKey(u.Id))
|
if (encounteredUsers.Add(referencedUser))
|
||||||
{
|
{
|
||||||
var member = await _discord.GetGuildMemberAsync(guild.Id, u.Id);
|
var member =
|
||||||
guild.Members[u.Id] = member;
|
await _discord.TryGetGuildMemberAsync(guild.Id, referencedUser) ??
|
||||||
|
Member.CreateForUser(referencedUser);
|
||||||
|
|
||||||
|
contextMembers.Add(member);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render message
|
// Export message
|
||||||
await messageExporter.ExportMessageAsync(message);
|
await messageExporter.ExportMessageAsync(message);
|
||||||
exportedAnything = true;
|
exportedAnything = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting
|
namespace DiscordChatExporter.Domain.Exporting
|
||||||
|
@ -16,11 +18,11 @@ namespace DiscordChatExporter.Domain.Exporting
|
||||||
|
|
||||||
public string DateFormat { get; }
|
public string DateFormat { get; }
|
||||||
|
|
||||||
public IReadOnlyCollection<User> MentionableUsers { get; }
|
public IReadOnlyCollection<Member> Members { get; }
|
||||||
|
|
||||||
public IReadOnlyCollection<Channel> MentionableChannels { get; }
|
public IReadOnlyCollection<Channel> Channels { get; }
|
||||||
|
|
||||||
public IReadOnlyCollection<Role> MentionableRoles { get; }
|
public IReadOnlyCollection<Role> Roles { get; }
|
||||||
|
|
||||||
public ExportContext(
|
public ExportContext(
|
||||||
Guild guild,
|
Guild guild,
|
||||||
|
@ -28,19 +30,41 @@ namespace DiscordChatExporter.Domain.Exporting
|
||||||
DateTimeOffset? after,
|
DateTimeOffset? after,
|
||||||
DateTimeOffset? before,
|
DateTimeOffset? before,
|
||||||
string dateFormat,
|
string dateFormat,
|
||||||
IReadOnlyCollection<User> mentionableUsers,
|
IReadOnlyCollection<Member> members,
|
||||||
IReadOnlyCollection<Channel> mentionableChannels,
|
IReadOnlyCollection<Channel> channels,
|
||||||
IReadOnlyCollection<Role> mentionableRoles)
|
IReadOnlyCollection<Role> roles)
|
||||||
|
|
||||||
{
|
{
|
||||||
Guild = guild;
|
Guild = guild;
|
||||||
Channel = channel;
|
Channel = channel;
|
||||||
After = after;
|
After = after;
|
||||||
Before = before;
|
Before = before;
|
||||||
DateFormat = dateFormat;
|
DateFormat = dateFormat;
|
||||||
MentionableUsers = mentionableUsers;
|
Members = members;
|
||||||
MentionableChannels = mentionableChannels;
|
Channels = channels;
|
||||||
MentionableRoles = mentionableRoles;
|
Roles = roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Member? TryGetMentionedMember(string id) =>
|
||||||
|
Members.FirstOrDefault(m => m.Id == id);
|
||||||
|
|
||||||
|
public Channel? TryGetMentionedChannel(string id) =>
|
||||||
|
Channels.FirstOrDefault(c => c.Id == id);
|
||||||
|
|
||||||
|
public Role? TryGetMentionedRole(string id) =>
|
||||||
|
Roles.FirstOrDefault(r => r.Id == id);
|
||||||
|
|
||||||
|
public Member? TryGetUserMember(User user) => Members
|
||||||
|
.FirstOrDefault(m => m.Id == user.Id);
|
||||||
|
|
||||||
|
public Color? TryGetUserColor(User user)
|
||||||
|
{
|
||||||
|
var member = TryGetUserMember(user);
|
||||||
|
var roles = member?.RoleIds.Join(Roles, i => i, r => r.Id, (_, role) => role);
|
||||||
|
|
||||||
|
return roles?
|
||||||
|
.OrderByDescending(r => r.Position)
|
||||||
|
.Select(r => r.Color)
|
||||||
|
.FirstOrDefault(c => c != null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -48,7 +48,7 @@ img {
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown {
|
.markdown {
|
||||||
display: inline-block;
|
max-width: 100%;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="preamble__entries-container">
|
<div class="preamble__entries-container">
|
||||||
<div class="preamble__entry">{{ Context.Guild.Name | html.escape }}</div>
|
<div class="preamble__entry">{{ Context.Guild.Name | html.escape }}</div>
|
||||||
<div class="preamble__entry">{{ Context.Channel.Name | html.escape }}</div>
|
<div class="preamble__entry">{{ Context.Channel.Category | html.escape }} / {{ Context.Channel.Name | html.escape }}</div>
|
||||||
|
|
||||||
{{~ if Context.Channel.Topic ~}}
|
{{~ if Context.Channel.Topic ~}}
|
||||||
<div class="preamble__entry preamble__entry--small">{{ Context.Channel.Topic | html.escape }}</div>
|
<div class="preamble__entry preamble__entry--small">{{ Context.Channel.Topic | html.escape }}</div>
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="chatlog__messages">
|
<div class="chatlog__messages">
|
||||||
{{~ # Author name and timestamp ~}}
|
{{~ # Author name and timestamp ~}}
|
||||||
<span class="chatlog__author-name" title="{{ MessageGroup.Author.FullName | html.escape }}" data-user-id="{{ MessageGroup.Author.Id | html.escape }}" {{ if GetUserColor Context.Guild MessageGroup.Author }} style="color: {{ GetUserColor Context.Guild MessageGroup.Author }}" {{ end }}>{{ GetUserNick Context.Guild MessageGroup.Author | html.escape }}</span>
|
{{~ userColor = TryGetUserColor MessageGroup.Author | FormatColorRgb ~}}
|
||||||
|
<span class="chatlog__author-name" title="{{ MessageGroup.Author.FullName | html.escape }}" data-user-id="{{ MessageGroup.Author.Id | html.escape }}" {{ if userColor }} style="color: {{ userColor }}" {{ end }}>{{ (TryGetUserNick MessageGroup.Author ?? MessageGroup.Author.Name) | html.escape }}</span>
|
||||||
|
|
||||||
{{~ # Bot tag ~}}
|
{{~ # Bot tag ~}}
|
||||||
{{~ if MessageGroup.Author.IsBot ~}}
|
{{~ if MessageGroup.Author.IsBot ~}}
|
||||||
|
@ -20,12 +21,14 @@
|
||||||
{{~ # Content ~}}
|
{{~ # Content ~}}
|
||||||
{{~ if message.Content ~}}
|
{{~ if message.Content ~}}
|
||||||
<div class="chatlog__content">
|
<div class="chatlog__content">
|
||||||
<div class="markdown">{{ message.Content | FormatMarkdown }}</div>
|
<div class="markdown">
|
||||||
|
{{- message.Content | FormatMarkdown -}}
|
||||||
|
|
||||||
{{~ # Edited timestamp ~}}
|
{{- # Edited timestamp -}}
|
||||||
{{~ if message.EditedTimestamp ~}}
|
{{- if message.EditedTimestamp -}}
|
||||||
<span class="chatlog__edited-timestamp" title="{{ message.EditedTimestamp | FormatDate | html.escape }}">(edited)</span>
|
{{-}}<span class="chatlog__edited-timestamp" title="{{ message.EditedTimestamp | FormatDate | html.escape }}">(edited)</span>{{-}}
|
||||||
{{~ end ~}}
|
{{- end -}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
|
|
||||||
|
@ -89,16 +92,16 @@
|
||||||
{{~ if embed.Title ~}}
|
{{~ if embed.Title ~}}
|
||||||
<div class="chatlog__embed-title">
|
<div class="chatlog__embed-title">
|
||||||
{{~ if embed.Url ~}}
|
{{~ if embed.Url ~}}
|
||||||
<a class="chatlog__embed-title-link" href="{{ embed.Url }}"><span class="markdown">{{ embed.Title | FormatMarkdown }}</span></a>
|
<a class="chatlog__embed-title-link" href="{{ embed.Url }}"><div class="markdown">{{ embed.Title | FormatEmbedMarkdown }}</div></a>
|
||||||
{{~ else ~}}
|
{{~ else ~}}
|
||||||
<span class="markdown">{{ embed.Title | FormatMarkdown }}</span>
|
<div class="markdown">{{ embed.Title | FormatEmbedMarkdown }}</div>
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
</div>
|
</div>
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
|
|
||||||
{{~ # Description ~}}
|
{{~ # Description ~}}
|
||||||
{{~ if embed.Description ~}}
|
{{~ if embed.Description ~}}
|
||||||
<div class="chatlog__embed-description"><span class="markdown">{{ embed.Description | FormatMarkdown }}</span></div>
|
<div class="chatlog__embed-description"><div class="markdown">{{ embed.Description | FormatEmbedMarkdown }}</div></div>
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
|
|
||||||
{{~ # Fields ~}}
|
{{~ # Fields ~}}
|
||||||
|
@ -107,10 +110,10 @@
|
||||||
{{~ for field in embed.Fields ~}}
|
{{~ for field in embed.Fields ~}}
|
||||||
<div class="chatlog__embed-field {{ if field.IsInline }} chatlog__embed-field--inline {{ end }}">
|
<div class="chatlog__embed-field {{ if field.IsInline }} chatlog__embed-field--inline {{ end }}">
|
||||||
{{~ if field.Name ~}}
|
{{~ if field.Name ~}}
|
||||||
<div class="chatlog__embed-field-name"><span class="markdown">{{ field.Name | FormatMarkdown }}</span></div>
|
<div class="chatlog__embed-field-name"><div class="markdown">{{ field.Name | FormatEmbedMarkdown }}</div></div>
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
{{~ if field.Value ~}}
|
{{~ if field.Value ~}}
|
||||||
<div class="chatlog__embed-field-value"><span class="markdown">{{ field.Value | FormatMarkdown }}</span></div>
|
<div class="chatlog__embed-field-value"><div class="markdown">{{ field.Value | FormatEmbedMarkdown }}</div></div>
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
</div>
|
</div>
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
@ -73,12 +74,20 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
scriptObject.Import("FormatDate",
|
scriptObject.Import("FormatDate",
|
||||||
new Func<DateTimeOffset, string>(d => d.ToLocalString(Context.DateFormat)));
|
new Func<DateTimeOffset, string>(d => d.ToLocalString(Context.DateFormat)));
|
||||||
|
|
||||||
|
scriptObject.Import("FormatColorRgb",
|
||||||
|
new Func<Color?, string?>(c => c != null ? $"rgb({c?.R}, {c?.G}, {c?.B})" : null));
|
||||||
|
|
||||||
|
scriptObject.Import("TryGetUserColor",
|
||||||
|
new Func<User, Color?>(Context.TryGetUserColor));
|
||||||
|
|
||||||
|
scriptObject.Import("TryGetUserNick",
|
||||||
|
new Func<User, string?>(u => Context.TryGetUserMember(u)?.Nick));
|
||||||
|
|
||||||
scriptObject.Import("FormatMarkdown",
|
scriptObject.Import("FormatMarkdown",
|
||||||
new Func<string?, string>(FormatMarkdown));
|
new Func<string?, string>(m => FormatMarkdown(m)));
|
||||||
|
|
||||||
scriptObject.Import("GetUserColor", new Func<Guild, User, string>(Guild.GetUserColor));
|
scriptObject.Import("FormatEmbedMarkdown",
|
||||||
|
new Func<string?, string>(m => FormatMarkdown(m, false)));
|
||||||
scriptObject.Import("GetUserNick", new Func<Guild, User, string>(Guild.GetUserNick));
|
|
||||||
|
|
||||||
// Push model
|
// Push model
|
||||||
templateContext.PushGlobal(scriptObject);
|
templateContext.PushGlobal(scriptObject);
|
||||||
|
@ -89,8 +98,8 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
return templateContext;
|
return templateContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FormatMarkdown(string? markdown) =>
|
private string FormatMarkdown(string? markdown, bool isJumboAllowed = true) =>
|
||||||
HtmlMarkdownVisitor.Format(Context, markdown ?? "");
|
HtmlMarkdownVisitor.Format(Context, markdown ?? "", isJumboAllowed);
|
||||||
|
|
||||||
private async Task RenderCurrentMessageGroupAsync()
|
private async Task RenderCurrentMessageGroupAsync()
|
||||||
{
|
{
|
||||||
|
|
|
@ -84,8 +84,8 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
{
|
{
|
||||||
_writer.WriteStartObject();
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
_writer.WriteString("name", embedField.Name);
|
_writer.WriteString("name", FormatMarkdown(embedField.Name));
|
||||||
_writer.WriteString("value", embedField.Value);
|
_writer.WriteString("value", FormatMarkdown(embedField.Value));
|
||||||
_writer.WriteBoolean("isInline", embedField.IsInline);
|
_writer.WriteBoolean("isInline", embedField.IsInline);
|
||||||
|
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
|
@ -156,6 +156,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
_writer.WriteStartObject("channel");
|
_writer.WriteStartObject("channel");
|
||||||
_writer.WriteString("id", Context.Channel.Id);
|
_writer.WriteString("id", Context.Channel.Id);
|
||||||
_writer.WriteString("type", Context.Channel.Type.ToString());
|
_writer.WriteString("type", Context.Channel.Type.ToString());
|
||||||
|
_writer.WriteString("category", Context.Channel.Category);
|
||||||
_writer.WriteString("name", Context.Channel.Name);
|
_writer.WriteString("name", Context.Channel.Name);
|
||||||
_writer.WriteString("topic", Context.Channel.Topic);
|
_writer.WriteString("topic", Context.Channel.Topic);
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
|
|
|
@ -85,38 +85,38 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.User)
|
else if (mention.Type == MentionType.User)
|
||||||
{
|
{
|
||||||
var user = _context.MentionableUsers.FirstOrDefault(u => u.Id == mention.Id) ??
|
var member = _context.TryGetMentionedMember(mention.Id);
|
||||||
User.CreateUnknownUser(mention.Id);
|
var fullName = member?.User.FullName ?? "Unknown";
|
||||||
|
var nick = member?.Nick ?? "Unknown";
|
||||||
var nick = Guild.GetUserNick(_context.Guild, user);
|
|
||||||
|
|
||||||
_buffer
|
_buffer
|
||||||
.Append($"<span class=\"mention\" title=\"{HtmlEncode(user.FullName)}\">")
|
.Append($"<span class=\"mention\" title=\"{HtmlEncode(fullName)}\">")
|
||||||
.Append("@").Append(HtmlEncode(nick))
|
.Append("@").Append(HtmlEncode(nick))
|
||||||
.Append("</span>");
|
.Append("</span>");
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.Channel)
|
else if (mention.Type == MentionType.Channel)
|
||||||
{
|
{
|
||||||
var channel = _context.MentionableChannels.FirstOrDefault(c => c.Id == mention.Id) ??
|
var channel = _context.TryGetMentionedChannel(mention.Id);
|
||||||
Channel.CreateDeletedChannel(mention.Id);
|
var name = channel?.Name ?? "deleted-channel";
|
||||||
|
|
||||||
_buffer
|
_buffer
|
||||||
.Append("<span class=\"mention\">")
|
.Append("<span class=\"mention\">")
|
||||||
.Append("#").Append(HtmlEncode(channel.Name))
|
.Append("#").Append(HtmlEncode(name))
|
||||||
.Append("</span>");
|
.Append("</span>");
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.Role)
|
else if (mention.Type == MentionType.Role)
|
||||||
{
|
{
|
||||||
var role = _context.MentionableRoles.FirstOrDefault(r => r.Id == mention.Id) ??
|
var role = _context.TryGetMentionedRole(mention.Id);
|
||||||
Role.CreateDeletedRole(mention.Id);
|
var name = role?.Name ?? "deleted-role";
|
||||||
|
var color = role?.Color;
|
||||||
|
|
||||||
var style = role.Color != null
|
var style = color != null
|
||||||
? $"color: {role.Color.Value.ToHexString()}; background-color: rgba({role.Color.Value.ToRgbString()}, 0.1);"
|
? $"color: rgb({color?.R}, {color?.G}, {color?.B}); background-color: rgba({color?.R}, {color?.G}, {color?.B}, 0.1);"
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
_buffer
|
_buffer
|
||||||
.Append($"<span class=\"mention\" style=\"{style}\">")
|
.Append($"<span class=\"mention\" style=\"{style}\">")
|
||||||
.Append("@").Append(HtmlEncode(role.Name))
|
.Append("@").Append(HtmlEncode(name))
|
||||||
.Append("</span>");
|
.Append("</span>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,10 +162,13 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors
|
||||||
{
|
{
|
||||||
private static string HtmlEncode(string text) => WebUtility.HtmlEncode(text);
|
private static string HtmlEncode(string text) => WebUtility.HtmlEncode(text);
|
||||||
|
|
||||||
public static string Format(ExportContext context, string markdown)
|
public static string Format(ExportContext context, string markdown, bool isJumboAllowed = true)
|
||||||
{
|
{
|
||||||
var nodes = MarkdownParser.Parse(markdown);
|
var nodes = MarkdownParser.Parse(markdown);
|
||||||
var isJumbo = nodes.All(n => n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text));
|
|
||||||
|
var isJumbo =
|
||||||
|
isJumboAllowed &&
|
||||||
|
nodes.All(n => n is EmojiNode || n is TextNode textNode && string.IsNullOrWhiteSpace(textNode.Text));
|
||||||
|
|
||||||
var buffer = new StringBuilder();
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
using System.Linq;
|
using System.Text;
|
||||||
using System.Text;
|
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
|
||||||
using DiscordChatExporter.Domain.Markdown;
|
using DiscordChatExporter.Domain.Markdown;
|
||||||
using DiscordChatExporter.Domain.Markdown.Ast;
|
using DiscordChatExporter.Domain.Markdown.Ast;
|
||||||
|
|
||||||
|
@ -27,24 +25,24 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors
|
||||||
{
|
{
|
||||||
if (mention.Type == MentionType.User)
|
if (mention.Type == MentionType.User)
|
||||||
{
|
{
|
||||||
var user = _context.MentionableUsers.FirstOrDefault(u => u.Id == mention.Id) ??
|
var member = _context.TryGetMentionedMember(mention.Id);
|
||||||
User.CreateUnknownUser(mention.Id);
|
var name = member?.User.Name ?? "Unknown";
|
||||||
|
|
||||||
_buffer.Append($"@{user.Name}");
|
_buffer.Append($"@{name}");
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.Channel)
|
else if (mention.Type == MentionType.Channel)
|
||||||
{
|
{
|
||||||
var channel = _context.MentionableChannels.FirstOrDefault(c => c.Id == mention.Id) ??
|
var channel = _context.TryGetMentionedChannel(mention.Id);
|
||||||
Channel.CreateDeletedChannel(mention.Id);
|
var name = channel?.Name ?? "deleted-channel";
|
||||||
|
|
||||||
_buffer.Append($"#{channel.Name}");
|
_buffer.Append($"#{name}");
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.Role)
|
else if (mention.Type == MentionType.Role)
|
||||||
{
|
{
|
||||||
var role = _context.MentionableRoles.FirstOrDefault(r => r.Id == mention.Id) ??
|
var role = _context.TryGetMentionedRole(mention.Id);
|
||||||
Role.CreateDeletedRole(mention.Id);
|
var name = role?.Name ?? "deleted-role";
|
||||||
|
|
||||||
_buffer.Append($"@{role.Name}");
|
_buffer.Append($"@{name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.VisitMention(mention);
|
return base.VisitMention(mention);
|
||||||
|
|
|
@ -76,8 +76,8 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
foreach (var field in embed.Fields)
|
foreach (var field in embed.Fields)
|
||||||
{
|
{
|
||||||
buffer
|
buffer
|
||||||
.AppendLineIfNotNullOrWhiteSpace(field.Name)
|
.AppendLineIfNotNullOrWhiteSpace(FormatMarkdown(field.Name))
|
||||||
.AppendLineIfNotNullOrWhiteSpace(field.Value);
|
.AppendLineIfNotNullOrWhiteSpace(FormatMarkdown(field.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer
|
buffer
|
||||||
|
@ -135,7 +135,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
|
|
||||||
buffer.Append('=', 62).AppendLine();
|
buffer.Append('=', 62).AppendLine();
|
||||||
buffer.AppendLine($"Guild: {Context.Guild.Name}");
|
buffer.AppendLine($"Guild: {Context.Guild.Name}");
|
||||||
buffer.AppendLine($"Channel: {Context.Channel.Name}");
|
buffer.AppendLine($"Channel: {Context.Channel.Category} / {Context.Channel.Name}");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(Context.Channel.Topic))
|
if (!string.IsNullOrWhiteSpace(Context.Channel.Topic))
|
||||||
buffer.AppendLine($"Topic: {Context.Channel.Topic}");
|
buffer.AppendLine($"Topic: {Context.Channel.Topic}");
|
||||||
|
|
|
@ -4,12 +4,10 @@ namespace DiscordChatExporter.Domain.Internal
|
||||||
{
|
{
|
||||||
internal static class ColorExtensions
|
internal static class ColorExtensions
|
||||||
{
|
{
|
||||||
public static Color ResetAlpha(this Color color) => Color.FromArgb(1, color);
|
public static Color WithAlpha(this Color color, int alpha) => Color.FromArgb(alpha, color);
|
||||||
|
|
||||||
|
public static Color ResetAlpha(this Color color) => color.WithAlpha(255);
|
||||||
|
|
||||||
public static int ToRgb(this Color color) => color.ToArgb() & 0xffffff;
|
public static int ToRgb(this Color color) => color.ToArgb() & 0xffffff;
|
||||||
|
|
||||||
public static string ToHexString(this Color color) => $"#{color.ToRgb():x6}";
|
|
||||||
|
|
||||||
public static string ToRgbString(this Color color) => $"{color.R}, {color.G}, {color.B}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Gui.Behaviors
|
||||||
|
{
|
||||||
|
public class ChannelMultiSelectionListBoxBehavior : MultiSelectionListBoxBehavior<Channel>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Gui.Behaviors
|
|
||||||
{
|
|
||||||
public class ChannelViewModelMultiSelectionListBoxBehavior : MultiSelectionListBoxBehavior<ChannelViewModel>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
|
||||||
using Stylet;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Gui.ViewModels.Components
|
|
||||||
{
|
|
||||||
public partial class ChannelViewModel : PropertyChangedBase
|
|
||||||
{
|
|
||||||
public Channel? Model { get; set; }
|
|
||||||
|
|
||||||
public string? Category { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class ChannelViewModel
|
|
||||||
{
|
|
||||||
public static implicit operator Channel?(ChannelViewModel? viewModel) => viewModel?.Model;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
|
||||||
using Stylet;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Gui.ViewModels.Components
|
|
||||||
{
|
|
||||||
public partial class GuildViewModel : PropertyChangedBase
|
|
||||||
{
|
|
||||||
public Guild? Model { get; set; }
|
|
||||||
|
|
||||||
public IReadOnlyList<ChannelViewModel>? Channels { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class GuildViewModel
|
|
||||||
{
|
|
||||||
public static implicit operator Guild?(GuildViewModel? viewModel) => viewModel?.Model;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
using DiscordChatExporter.Domain.Exporting;
|
using DiscordChatExporter.Domain.Exporting;
|
||||||
using DiscordChatExporter.Gui.Services;
|
using DiscordChatExporter.Gui.Services;
|
||||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
|
||||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||||
|
@ -13,9 +13,9 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||||
private readonly DialogManager _dialogManager;
|
private readonly DialogManager _dialogManager;
|
||||||
private readonly SettingsService _settingsService;
|
private readonly SettingsService _settingsService;
|
||||||
|
|
||||||
public GuildViewModel? Guild { get; set; }
|
public Guild? Guild { get; set; }
|
||||||
|
|
||||||
public IReadOnlyList<ChannelViewModel>? Channels { get; set; }
|
public IReadOnlyList<Channel>? Channels { get; set; }
|
||||||
|
|
||||||
public bool IsSingleChannel => Channels == null || Channels.Count == 1;
|
public bool IsSingleChannel => Channels == null || Channels.Count == 1;
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||||
var channel = Channels.Single();
|
var channel = Channels.Single();
|
||||||
|
|
||||||
// Generate default file name
|
// Generate default file name
|
||||||
var defaultFileName = ChannelExporter.GetDefaultExportFileName(Guild!, channel!, SelectedFormat, After, Before);
|
var defaultFileName = ChannelExporter.GetDefaultExportFileName(Guild!, channel, SelectedFormat, After, Before);
|
||||||
|
|
||||||
// Generate filter
|
// Generate filter
|
||||||
var ext = SelectedFormat.GetFileExtension();
|
var ext = SelectedFormat.GetFileExtension();
|
||||||
|
|
|
@ -1,33 +1,13 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
|
||||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Gui.ViewModels.Framework
|
namespace DiscordChatExporter.Gui.ViewModels.Framework
|
||||||
{
|
{
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
public static ChannelViewModel CreateChannelViewModel(this IViewModelFactory factory, Channel model, string? category = null)
|
|
||||||
{
|
|
||||||
var viewModel = factory.CreateChannelViewModel();
|
|
||||||
viewModel.Model = model;
|
|
||||||
viewModel.Category = category;
|
|
||||||
|
|
||||||
return viewModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GuildViewModel CreateGuildViewModel(this IViewModelFactory factory, Guild model,
|
|
||||||
IReadOnlyList<ChannelViewModel> channels)
|
|
||||||
{
|
|
||||||
var viewModel = factory.CreateGuildViewModel();
|
|
||||||
viewModel.Model = model;
|
|
||||||
viewModel.Channels = channels;
|
|
||||||
|
|
||||||
return viewModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ExportSetupViewModel CreateExportSetupViewModel(this IViewModelFactory factory,
|
public static ExportSetupViewModel CreateExportSetupViewModel(this IViewModelFactory factory,
|
||||||
GuildViewModel guild, IReadOnlyList<ChannelViewModel> channels)
|
Guild guild, IReadOnlyList<Channel> channels)
|
||||||
{
|
{
|
||||||
var viewModel = factory.CreateExportSetupViewModel();
|
var viewModel = factory.CreateExportSetupViewModel();
|
||||||
viewModel.Guild = guild;
|
viewModel.Guild = guild;
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Gui.ViewModels.Framework
|
namespace DiscordChatExporter.Gui.ViewModels.Framework
|
||||||
{
|
{
|
||||||
// Used to instantiate new view models while making use of dependency injection
|
// Used to instantiate new view models while making use of dependency injection
|
||||||
public interface IViewModelFactory
|
public interface IViewModelFactory
|
||||||
{
|
{
|
||||||
ChannelViewModel CreateChannelViewModel();
|
|
||||||
|
|
||||||
GuildViewModel CreateGuildViewModel();
|
|
||||||
|
|
||||||
ExportSetupViewModel CreateExportSetupViewModel();
|
ExportSetupViewModel CreateExportSetupViewModel();
|
||||||
|
|
||||||
SettingsViewModel CreateSettingsViewModel();
|
SettingsViewModel CreateSettingsViewModel();
|
||||||
|
|
|
@ -9,7 +9,6 @@ using DiscordChatExporter.Domain.Exceptions;
|
||||||
using DiscordChatExporter.Domain.Exporting;
|
using DiscordChatExporter.Domain.Exporting;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
using DiscordChatExporter.Gui.Services;
|
using DiscordChatExporter.Gui.Services;
|
||||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
|
||||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||||
using Gress;
|
using Gress;
|
||||||
using MaterialDesignThemes.Wpf;
|
using MaterialDesignThemes.Wpf;
|
||||||
|
@ -37,11 +36,17 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||||
|
|
||||||
public string? TokenValue { get; set; }
|
public string? TokenValue { get; set; }
|
||||||
|
|
||||||
public IReadOnlyList<GuildViewModel>? AvailableGuilds { get; private set; }
|
private IReadOnlyDictionary<Guild, IReadOnlyList<Channel>>? GuildChannelMap { get; set; }
|
||||||
|
|
||||||
public GuildViewModel? SelectedGuild { get; set; }
|
public IReadOnlyList<Guild>? AvailableGuilds => GuildChannelMap?.Keys.ToArray();
|
||||||
|
|
||||||
public IReadOnlyList<ChannelViewModel>? SelectedChannels { get; set; }
|
public Guild? SelectedGuild { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Channel>? AvailableChannels => SelectedGuild != null
|
||||||
|
? GuildChannelMap?[SelectedGuild]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
public IReadOnlyList<Channel>? SelectedChannels { get; set; }
|
||||||
|
|
||||||
public RootViewModel(
|
public RootViewModel(
|
||||||
IViewModelFactory viewModelFactory,
|
IViewModelFactory viewModelFactory,
|
||||||
|
@ -142,71 +147,18 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||||
|
|
||||||
var discord = new DiscordClient(token);
|
var discord = new DiscordClient(token);
|
||||||
|
|
||||||
var availableGuilds = new List<GuildViewModel>();
|
var guildChannelMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
||||||
|
await foreach (var guild in discord.GetUserGuildsAsync())
|
||||||
// Direct messages
|
|
||||||
{
|
|
||||||
var guild = Guild.DirectMessages;
|
|
||||||
var channels = await discord.GetDirectMessageChannelsAsync();
|
|
||||||
|
|
||||||
// Create channel view models
|
|
||||||
var channelViewModels = new List<ChannelViewModel>();
|
|
||||||
foreach (var channel in channels)
|
|
||||||
{
|
|
||||||
// Get fake category
|
|
||||||
var category = channel.Type == ChannelType.DirectTextChat ? "Private" : "Group";
|
|
||||||
|
|
||||||
// Create channel view model
|
|
||||||
var channelViewModel = _viewModelFactory.CreateChannelViewModel(channel, category);
|
|
||||||
|
|
||||||
// Add to list
|
|
||||||
channelViewModels.Add(channelViewModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create guild view model
|
|
||||||
var guildViewModel = _viewModelFactory.CreateGuildViewModel(guild,
|
|
||||||
channelViewModels.OrderBy(c => c.Category)
|
|
||||||
.ThenBy(c => c.Model!.Name)
|
|
||||||
.ToArray());
|
|
||||||
|
|
||||||
// Add to list
|
|
||||||
availableGuilds.Add(guildViewModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guilds
|
|
||||||
var guilds = await discord.GetUserGuildsAsync();
|
|
||||||
foreach (var guild in guilds)
|
|
||||||
{
|
{
|
||||||
var channels = await discord.GetGuildChannelsAsync(guild.Id);
|
var channels = await discord.GetGuildChannelsAsync(guild.Id);
|
||||||
var categoryChannels = channels.Where(c => c.Type == ChannelType.GuildCategory).ToArray();
|
guildChannelMap[guild] = channels
|
||||||
var exportableChannels = channels.Where(c => c.IsTextChannel).ToArray();
|
.OrderBy(c => c.Category)
|
||||||
|
.ThenBy(c => c.Name)
|
||||||
// Create channel view models
|
.ToArray();
|
||||||
var channelViewModels = new List<ChannelViewModel>();
|
|
||||||
foreach (var channel in exportableChannels)
|
|
||||||
{
|
|
||||||
// Get category
|
|
||||||
var category = categoryChannels.FirstOrDefault(c => c.Id == channel.ParentId)?.Name;
|
|
||||||
|
|
||||||
// Create channel view model
|
|
||||||
var channelViewModel = _viewModelFactory.CreateChannelViewModel(channel, category);
|
|
||||||
|
|
||||||
// Add to list
|
|
||||||
channelViewModels.Add(channelViewModel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create guild view model
|
GuildChannelMap = guildChannelMap;
|
||||||
var guildViewModel = _viewModelFactory.CreateGuildViewModel(guild,
|
SelectedGuild = guildChannelMap.Keys.FirstOrDefault();
|
||||||
channelViewModels.OrderBy(c => c.Category)
|
|
||||||
.ThenBy(c => c.Model!.Name)
|
|
||||||
.ToArray());
|
|
||||||
|
|
||||||
// Add to list
|
|
||||||
availableGuilds.Add(guildViewModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
AvailableGuilds = availableGuilds;
|
|
||||||
SelectedGuild = AvailableGuilds.FirstOrDefault();
|
|
||||||
}
|
}
|
||||||
catch (DiscordChatExporterException ex) when (!ex.IsCritical)
|
catch (DiscordChatExporterException ex) when (!ex.IsCritical)
|
||||||
{
|
{
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
Width="32"
|
Width="32"
|
||||||
Height="32">
|
Height="32">
|
||||||
<Ellipse.Fill>
|
<Ellipse.Fill>
|
||||||
<ImageBrush ImageSource="{Binding Guild.Model.IconUrl}" />
|
<ImageBrush ImageSource="{Binding Guild.IconUrl}" />
|
||||||
</Ellipse.Fill>
|
</Ellipse.Fill>
|
||||||
</Ellipse>
|
</Ellipse>
|
||||||
|
|
||||||
|
@ -54,8 +54,8 @@
|
||||||
<Run Text="/" />
|
<Run Text="/" />
|
||||||
<Run
|
<Run
|
||||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||||
Text="{Binding Channels[0].Model.Name, Mode=OneWay}"
|
Text="{Binding Channels[0].Name, Mode=OneWay}"
|
||||||
ToolTip="{Binding Channels[0].Model.Name, Mode=OneWay}" />
|
ToolTip="{Binding Channels[0].Name, Mode=OneWay}" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
|
@ -226,7 +226,7 @@
|
||||||
Margin="-8"
|
Margin="-8"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
ToolTip="{Binding Model.Name}">
|
ToolTip="{Binding Name}">
|
||||||
<!-- Guild icon placeholder -->
|
<!-- Guild icon placeholder -->
|
||||||
<Ellipse
|
<Ellipse
|
||||||
Width="48"
|
Width="48"
|
||||||
|
@ -240,7 +240,7 @@
|
||||||
Height="48"
|
Height="48"
|
||||||
Margin="12,4,12,4">
|
Margin="12,4,12,4">
|
||||||
<Ellipse.Fill>
|
<Ellipse.Fill>
|
||||||
<ImageBrush ImageSource="{Binding Model.IconUrl}" />
|
<ImageBrush ImageSource="{Binding IconUrl}" />
|
||||||
</Ellipse.Fill>
|
</Ellipse.Fill>
|
||||||
</Ellipse>
|
</Ellipse>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -253,11 +253,11 @@
|
||||||
<Border Grid.Column="1">
|
<Border Grid.Column="1">
|
||||||
<ListBox
|
<ListBox
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
ItemsSource="{Binding SelectedGuild.Channels}"
|
ItemsSource="{Binding AvailableChannels}"
|
||||||
SelectionMode="Extended"
|
SelectionMode="Extended"
|
||||||
TextSearch.TextPath="Model.Name">
|
TextSearch.TextPath="Model.Name">
|
||||||
<i:Interaction.Behaviors>
|
<i:Interaction.Behaviors>
|
||||||
<behaviors:ChannelViewModelMultiSelectionListBoxBehavior SelectedItems="{Binding SelectedChannels}" />
|
<behaviors:ChannelMultiSelectionListBoxBehavior SelectedItems="{Binding SelectedChannels}" />
|
||||||
</i:Interaction.Behaviors>
|
</i:Interaction.Behaviors>
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
|
@ -286,7 +286,7 @@
|
||||||
FontSize="14">
|
FontSize="14">
|
||||||
<Run Foreground="{DynamicResource SecondaryTextBrush}" Text="{Binding Category, Mode=OneWay}" />
|
<Run Foreground="{DynamicResource SecondaryTextBrush}" Text="{Binding Category, Mode=OneWay}" />
|
||||||
<Run Text="/" />
|
<Run Text="/" />
|
||||||
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="{Binding Model.Name, Mode=OneWay}" />
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="{Binding Name, Mode=OneWay}" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
|
||||||
<!-- Is selected checkmark -->
|
<!-- Is selected checkmark -->
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue