mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-02-15 00:03:38 -07:00
Cleanup
This commit is contained in:
parent
9491e18e2f
commit
29552be6e5
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord
|
namespace DiscordChatExporter.Core.Discord
|
||||||
{
|
{
|
||||||
public enum AuthTokenType { User, Bot }
|
|
||||||
|
|
||||||
public class AuthToken
|
public class AuthToken
|
||||||
{
|
{
|
||||||
public AuthTokenType Type { get; }
|
public AuthTokenType Type { get; }
|
||||||
|
|
|
||||||
8
DiscordChatExporter.Core/Discord/AuthTokenType.cs
Normal file
8
DiscordChatExporter.Core/Discord/AuthTokenType.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace DiscordChatExporter.Core.Discord
|
||||||
|
{
|
||||||
|
public enum AuthTokenType
|
||||||
|
{
|
||||||
|
User,
|
||||||
|
Bot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,32 +7,19 @@ using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
|
||||||
// Order of enum fields needs to match the order in the docs.
|
|
||||||
public enum ChannelType
|
|
||||||
{
|
|
||||||
GuildTextChat,
|
|
||||||
DirectTextChat,
|
|
||||||
GuildVoiceChat,
|
|
||||||
DirectGroupTextChat,
|
|
||||||
GuildCategory,
|
|
||||||
GuildNews,
|
|
||||||
GuildStore
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://discord.com/developers/docs/resources/channel#channel-object
|
// https://discord.com/developers/docs/resources/channel#channel-object
|
||||||
public partial class Channel : IHasId, IHasPosition
|
public partial class Channel : IHasId
|
||||||
{
|
{
|
||||||
public Snowflake Id { get; }
|
public Snowflake Id { get; }
|
||||||
|
|
||||||
public ChannelType Type { get; }
|
public ChannelType Type { get; }
|
||||||
|
|
||||||
public bool IsTextChannel =>
|
public bool IsTextChannel => Type is
|
||||||
Type == ChannelType.GuildTextChat ||
|
ChannelType.GuildTextChat or
|
||||||
Type == ChannelType.DirectTextChat ||
|
ChannelType.DirectTextChat or
|
||||||
Type == ChannelType.DirectGroupTextChat ||
|
ChannelType.DirectGroupTextChat or
|
||||||
Type == ChannelType.GuildNews ||
|
ChannelType.GuildNews or
|
||||||
Type == ChannelType.GuildStore;
|
ChannelType.GuildStore;
|
||||||
|
|
||||||
public Snowflake GuildId { get; }
|
public Snowflake GuildId { get; }
|
||||||
|
|
||||||
|
|
@ -48,7 +35,7 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
Snowflake id,
|
Snowflake id,
|
||||||
ChannelType type,
|
ChannelType type,
|
||||||
Snowflake guildId,
|
Snowflake guildId,
|
||||||
ChannelCategory? category,
|
ChannelCategory category,
|
||||||
string name,
|
string name,
|
||||||
int? position,
|
int? position,
|
||||||
string? topic)
|
string? topic)
|
||||||
|
|
@ -56,14 +43,13 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
Id = id;
|
Id = id;
|
||||||
Type = type;
|
Type = type;
|
||||||
GuildId = guildId;
|
GuildId = guildId;
|
||||||
Category = category ?? GetFallbackCategory(type);
|
Category = category;
|
||||||
Name = name;
|
Name = name;
|
||||||
Position = position;
|
Position = position;
|
||||||
Topic = topic;
|
Topic = topic;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public override string ToString() => Name;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class Channel
|
public partial class Channel
|
||||||
|
|
@ -79,7 +65,7 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
ChannelType.GuildStore => "Store",
|
ChannelType.GuildStore => "Store",
|
||||||
_ => "Default"
|
_ => "Default"
|
||||||
},
|
},
|
||||||
0
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
public static Channel Parse(JsonElement json, ChannelCategory? category = null, int? position = null)
|
public static Channel Parse(JsonElement json, ChannelCategory? category = null, int? position = null)
|
||||||
|
|
@ -87,23 +73,23 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
||||||
var guildId = json.GetPropertyOrNull("guild_id")?.GetString().Pipe(Snowflake.Parse);
|
var guildId = json.GetPropertyOrNull("guild_id")?.GetString().Pipe(Snowflake.Parse);
|
||||||
var topic = json.GetPropertyOrNull("topic")?.GetString();
|
var topic = json.GetPropertyOrNull("topic")?.GetString();
|
||||||
|
|
||||||
var type = (ChannelType) json.GetProperty("type").GetInt32();
|
var type = (ChannelType) json.GetProperty("type").GetInt32();
|
||||||
|
|
||||||
var name =
|
var name =
|
||||||
|
// Guild channel
|
||||||
json.GetPropertyOrNull("name")?.GetString() ??
|
json.GetPropertyOrNull("name")?.GetString() ??
|
||||||
|
// DM channel
|
||||||
json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(User.Parse).Select(u => u.Name).JoinToString(", ") ??
|
json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(User.Parse).Select(u => u.Name).JoinToString(", ") ??
|
||||||
|
// Fallback
|
||||||
id.ToString();
|
id.ToString();
|
||||||
|
|
||||||
position ??= json.GetPropertyOrNull("position")?.GetInt32();
|
|
||||||
|
|
||||||
return new Channel(
|
return new Channel(
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
guildId ?? Guild.DirectMessages.Id,
|
guildId ?? Guild.DirectMessages.Id,
|
||||||
category ?? GetFallbackCategory(type),
|
category ?? GetFallbackCategory(type),
|
||||||
name,
|
name,
|
||||||
position,
|
position ?? json.GetPropertyOrNull("position")?.GetInt32(),
|
||||||
topic
|
topic
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
using System.Linq;
|
using System.Text.Json;
|
||||||
using System.Text.Json;
|
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
using Tyrrrz.Extensions;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
public partial class ChannelCategory : IHasId, IHasPosition
|
public partial class ChannelCategory : IHasId
|
||||||
{
|
{
|
||||||
public Snowflake Id { get; }
|
public Snowflake Id { get; }
|
||||||
|
|
||||||
|
|
@ -23,7 +21,6 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public override string ToString() => Name;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class ChannelCategory
|
public partial class ChannelCategory
|
||||||
|
|
@ -31,16 +28,15 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
public static ChannelCategory Parse(JsonElement json, int? position = null)
|
public static ChannelCategory Parse(JsonElement json, int? position = null)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
||||||
position ??= json.GetPropertyOrNull("position")?.GetInt32();
|
|
||||||
|
|
||||||
var name = json.GetPropertyOrNull("name")?.GetString() ??
|
var name =
|
||||||
json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(User.Parse).Select(u => u.Name).JoinToString(", ") ??
|
json.GetPropertyOrNull("name")?.GetString() ??
|
||||||
id.ToString();
|
id.ToString();
|
||||||
|
|
||||||
return new ChannelCategory(
|
return new ChannelCategory(
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
position
|
position ?? json.GetPropertyOrNull("position")?.GetInt32()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
15
DiscordChatExporter.Core/Discord/Data/ChannelType.cs
Normal file
15
DiscordChatExporter.Core/Discord/Data/ChannelType.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
|
{
|
||||||
|
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||||
|
// Order of enum fields needs to match the order in the docs.
|
||||||
|
public enum ChannelType
|
||||||
|
{
|
||||||
|
GuildTextChat = 0,
|
||||||
|
DirectTextChat,
|
||||||
|
GuildVoiceChat,
|
||||||
|
DirectGroupTextChat,
|
||||||
|
GuildCategory,
|
||||||
|
GuildNews,
|
||||||
|
GuildStore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
namespace DiscordChatExporter.Core.Discord.Data.Common
|
|
||||||
{
|
|
||||||
public interface IHasPosition
|
|
||||||
{
|
|
||||||
int? Position { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -63,10 +63,10 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
public static Embed Parse(JsonElement json)
|
public static Embed Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
var title = json.GetPropertyOrNull("title")?.GetString();
|
var title = json.GetPropertyOrNull("title")?.GetString();
|
||||||
var description = json.GetPropertyOrNull("description")?.GetString();
|
|
||||||
var url = json.GetPropertyOrNull("url")?.GetString();
|
var url = json.GetPropertyOrNull("url")?.GetString();
|
||||||
var timestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset();
|
var timestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset();
|
||||||
var color = json.GetPropertyOrNull("color")?.GetInt32().Pipe(System.Drawing.Color.FromArgb).ResetAlpha();
|
var color = json.GetPropertyOrNull("color")?.GetInt32().Pipe(System.Drawing.Color.FromArgb).ResetAlpha();
|
||||||
|
var description = json.GetPropertyOrNull("description")?.GetString();
|
||||||
|
|
||||||
var author = json.GetPropertyOrNull("author")?.Pipe(EmbedAuthor.Parse);
|
var author = json.GetPropertyOrNull("author")?.Pipe(EmbedAuthor.Parse);
|
||||||
var thumbnail = json.GetPropertyOrNull("thumbnail")?.Pipe(EmbedImage.Parse);
|
var thumbnail = json.GetPropertyOrNull("thumbnail")?.Pipe(EmbedImage.Parse);
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,12 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
|
|
||||||
public string ImageUrl { get; }
|
public string ImageUrl { get; }
|
||||||
|
|
||||||
public Emoji(string? id, string name, bool isAnimated)
|
public Emoji(string? id, string name, bool isAnimated, string imageUrl)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
IsAnimated = isAnimated;
|
IsAnimated = isAnimated;
|
||||||
|
ImageUrl = imageUrl;
|
||||||
ImageUrl = GetImageUrl(id, name, isAnimated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public override string ToString() => Name;
|
||||||
|
|
@ -53,12 +52,9 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
// Custom emoji
|
// Custom emoji
|
||||||
if (!string.IsNullOrWhiteSpace(id))
|
if (!string.IsNullOrWhiteSpace(id))
|
||||||
{
|
{
|
||||||
// Animated
|
return isAnimated
|
||||||
if (isAnimated)
|
? $"https://cdn.discordapp.com/emojis/{id}.gif"
|
||||||
return $"https://cdn.discordapp.com/emojis/{id}.gif";
|
: $"https://cdn.discordapp.com/emojis/{id}.png";
|
||||||
|
|
||||||
// Non-animated
|
|
||||||
return $"https://cdn.discordapp.com/emojis/{id}.png";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard emoji
|
// Standard emoji
|
||||||
|
|
@ -73,7 +69,9 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
var name = json.GetProperty("name").GetString();
|
var name = json.GetProperty("name").GetString();
|
||||||
var isAnimated = json.GetPropertyOrNull("animated")?.GetBoolean() ?? false;
|
var isAnimated = json.GetPropertyOrNull("animated")?.GetBoolean() ?? false;
|
||||||
|
|
||||||
return new Emoji(id, name, isAnimated);
|
var imageUrl = GetImageUrl(id, name, isAnimated);
|
||||||
|
|
||||||
|
return new Emoji(id, name, isAnimated, imageUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -19,10 +19,10 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
|
|
||||||
public IReadOnlyList<Snowflake> RoleIds { get; }
|
public IReadOnlyList<Snowflake> RoleIds { get; }
|
||||||
|
|
||||||
public Member(User user, string? nick, IReadOnlyList<Snowflake> roleIds)
|
public Member(User user, string nick, IReadOnlyList<Snowflake> roleIds)
|
||||||
{
|
{
|
||||||
User = user;
|
User = user;
|
||||||
Nick = nick ?? user.Name;
|
Nick = nick;
|
||||||
RoleIds = roleIds;
|
RoleIds = roleIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
public static Member CreateForUser(User user) => new(
|
public static Member CreateForUser(User user) => new(
|
||||||
user,
|
user,
|
||||||
null,
|
user.Name,
|
||||||
Array.Empty<Snowflake>()
|
Array.Empty<Snowflake>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -43,12 +43,12 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
var nick = json.GetPropertyOrNull("nick")?.GetString();
|
var nick = json.GetPropertyOrNull("nick")?.GetString();
|
||||||
|
|
||||||
var roleIds =
|
var roleIds =
|
||||||
json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString().Pipe(Snowflake.Parse)).ToArray() ??
|
json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString()).Select(Snowflake.Parse).ToArray() ??
|
||||||
Array.Empty<Snowflake>();
|
Array.Empty<Snowflake>();
|
||||||
|
|
||||||
return new Member(
|
return new Member(
|
||||||
user,
|
user,
|
||||||
nick,
|
nick ?? user.Name,
|
||||||
roleIds
|
roleIds
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
var type = (MessageType) json.GetProperty("type").GetInt32();
|
var type = (MessageType) json.GetProperty("type").GetInt32();
|
||||||
var isPinned = json.GetPropertyOrNull("pinned")?.GetBoolean() ?? false;
|
var isPinned = json.GetPropertyOrNull("pinned")?.GetBoolean() ?? false;
|
||||||
var messageReference = json.GetPropertyOrNull("message_reference")?.Pipe(MessageReference.Parse);
|
var messageReference = json.GetPropertyOrNull("message_reference")?.Pipe(MessageReference.Parse);
|
||||||
var referencedMessage = json.GetPropertyOrNull("referenced_message")?.Pipe(Message.Parse);
|
var referencedMessage = json.GetPropertyOrNull("referenced_message")?.Pipe(Parse);
|
||||||
|
|
||||||
var content = type switch
|
var content = type switch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
public static Reaction Parse(JsonElement json)
|
public static Reaction Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
var count = json.GetProperty("count").GetInt32();
|
|
||||||
var emoji = json.GetProperty("emoji").Pipe(Emoji.Parse);
|
var emoji = json.GetProperty("emoji").Pipe(Emoji.Parse);
|
||||||
|
var count = json.GetProperty("count").GetInt32();
|
||||||
|
|
||||||
return new Reaction(emoji, count);
|
return new Reaction(emoji, count);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
var name = json.GetProperty("name").GetString();
|
var name = json.GetProperty("name").GetString();
|
||||||
var position = json.GetProperty("position").GetInt32();
|
var position = json.GetProperty("position").GetInt32();
|
||||||
|
|
||||||
var color = json.GetPropertyOrNull("color")?
|
var color = json
|
||||||
|
.GetPropertyOrNull("color")?
|
||||||
.GetInt32()
|
.GetInt32()
|
||||||
.Pipe(System.Drawing.Color.FromArgb)
|
.Pipe(System.Drawing.Color.FromArgb)
|
||||||
.ResetAlpha()
|
.ResetAlpha()
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,10 @@ namespace DiscordChatExporter.Core.Discord.Data
|
||||||
public static User Parse(JsonElement json)
|
public static User Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
||||||
|
var isBot = json.GetPropertyOrNull("bot")?.GetBoolean() ?? false;
|
||||||
var discriminator = json.GetProperty("discriminator").GetString().Pipe(int.Parse);
|
var discriminator = json.GetProperty("discriminator").GetString().Pipe(int.Parse);
|
||||||
var name = json.GetProperty("username").GetString();
|
var name = json.GetProperty("username").GetString();
|
||||||
var avatarHash = json.GetProperty("avatar").GetString();
|
var avatarHash = json.GetProperty("avatar").GetString();
|
||||||
var isBot = json.GetPropertyOrNull("bot")?.GetBoolean() ?? false;
|
|
||||||
|
|
||||||
var avatarUrl = !string.IsNullOrWhiteSpace(avatarHash)
|
var avatarUrl = !string.IsNullOrWhiteSpace(avatarHash)
|
||||||
? GetAvatarUrl(id, avatarHash)
|
? GetAvatarUrl(id, avatarHash)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ using DiscordChatExporter.Core.Utils;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Http;
|
using JsonExtensions.Http;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
using Tyrrrz.Extensions;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord
|
namespace DiscordChatExporter.Core.Discord
|
||||||
{
|
{
|
||||||
|
|
@ -29,7 +28,9 @@ namespace DiscordChatExporter.Core.Discord
|
||||||
}
|
}
|
||||||
|
|
||||||
public DiscordClient(AuthToken token)
|
public DiscordClient(AuthToken token)
|
||||||
: this(Http.Client, token) {}
|
: this(Http.Client, token)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
private async ValueTask<HttpResponseMessage> GetResponseAsync(string url) =>
|
private async ValueTask<HttpResponseMessage> GetResponseAsync(string url) =>
|
||||||
await Http.ResponsePolicy.ExecuteAsync(async () =>
|
await Http.ResponsePolicy.ExecuteAsync(async () =>
|
||||||
|
|
@ -64,7 +65,7 @@ namespace DiscordChatExporter.Core.Discord
|
||||||
|
|
||||||
return response.IsSuccessStatusCode
|
return response.IsSuccessStatusCode
|
||||||
? await response.Content.ReadAsJsonAsync()
|
? await response.Content.ReadAsJsonAsync()
|
||||||
: (JsonElement?) null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<Guild> GetUserGuildsAsync()
|
public async IAsyncEnumerable<Guild> GetUserGuildsAsync()
|
||||||
|
|
@ -118,29 +119,30 @@ namespace DiscordChatExporter.Core.Discord
|
||||||
{
|
{
|
||||||
var response = await GetJsonResponseAsync($"guilds/{guildId}/channels");
|
var response = await GetJsonResponseAsync($"guilds/{guildId}/channels");
|
||||||
|
|
||||||
var orderedResponse = response
|
var responseOrdered = response
|
||||||
.EnumerateArray()
|
.EnumerateArray()
|
||||||
.OrderBy(j => j.GetProperty("position").GetInt32())
|
.OrderBy(j => j.GetProperty("position").GetInt32())
|
||||||
.ThenBy(j => ulong.Parse(j.GetProperty("id").GetString()))
|
.ThenBy(j => Snowflake.Parse(j.GetProperty("id").GetString()))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var categories = orderedResponse
|
var categories = responseOrdered
|
||||||
.Where(j => j.GetProperty("type").GetInt32() == (int) ChannelType.GuildCategory)
|
.Where(j => j.GetProperty("type").GetInt32() == (int) ChannelType.GuildCategory)
|
||||||
.Select((j, index) => ChannelCategory.Parse(j, index + 1))
|
.Select((j, index) => ChannelCategory.Parse(j, index + 1))
|
||||||
.ToDictionary(j => j.Id.ToString());
|
.ToDictionary(j => j.Id.ToString(), StringComparer.Ordinal);
|
||||||
|
|
||||||
var position = 0;
|
var position = 0;
|
||||||
|
|
||||||
foreach (var channelJson in orderedResponse)
|
foreach (var channelJson in responseOrdered)
|
||||||
{
|
{
|
||||||
var parentId = channelJson.GetPropertyOrNull("parent_id")?.GetString();
|
var parentId = channelJson.GetPropertyOrNull("parent_id")?.GetString();
|
||||||
|
|
||||||
var category = !string.IsNullOrWhiteSpace(parentId)
|
var category = !string.IsNullOrWhiteSpace(parentId)
|
||||||
? categories.GetValueOrDefault(parentId)
|
? categories.GetValueOrDefault(parentId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var channel = Channel.Parse(channelJson, category, position);
|
var channel = Channel.Parse(channelJson, category, position);
|
||||||
|
|
||||||
// Skip non-text channels
|
// We are only interested in text channels
|
||||||
if (!channel.IsTextChannel)
|
if (!channel.IsTextChannel)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
@ -178,15 +180,12 @@ namespace DiscordChatExporter.Core.Discord
|
||||||
var response = await GetJsonResponseAsync($"channels/{channelId}");
|
var response = await GetJsonResponseAsync($"channels/{channelId}");
|
||||||
return ChannelCategory.Parse(response);
|
return ChannelCategory.Parse(response);
|
||||||
}
|
}
|
||||||
/***
|
// In some cases, the Discord API returns an empty body when requesting channel category.
|
||||||
* In some cases, the Discord API returns an empty body when requesting some channel category info.
|
// Instead, we use an empty channel category as a fallback.
|
||||||
* Instead, we use an empty channel category as a fallback.
|
|
||||||
*/
|
|
||||||
catch (DiscordChatExporterException)
|
catch (DiscordChatExporterException)
|
||||||
{
|
{
|
||||||
return ChannelCategory.Empty;
|
return ChannelCategory.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Channel> GetChannelAsync(Snowflake channelId)
|
public async ValueTask<Channel> GetChannelAsync(Snowflake channelId)
|
||||||
|
|
@ -221,7 +220,7 @@ namespace DiscordChatExporter.Core.Discord
|
||||||
IProgress<double>? progress = null)
|
IProgress<double>? progress = null)
|
||||||
{
|
{
|
||||||
// Get the last message in the specified range.
|
// Get the last message in the specified range.
|
||||||
// This snapshots the boundaries, which means that messages posted after the exported started
|
// This snapshots the boundaries, which means that messages posted after the export started
|
||||||
// will not appear in the output.
|
// will not appear in the output.
|
||||||
// Additionally, it provides the date of the last message, which is used to calculate progress.
|
// Additionally, it provides the date of the last message, which is used to calculate progress.
|
||||||
var lastMessage = await TryGetLastMessageAsync(channelId, before);
|
var lastMessage = await TryGetLastMessageAsync(channelId, before);
|
||||||
|
|
@ -271,7 +270,7 @@ namespace DiscordChatExporter.Core.Discord
|
||||||
progress.Report(exportedDuration / totalDuration);
|
progress.Report(exportedDuration / totalDuration);
|
||||||
}
|
}
|
||||||
// Avoid division by zero if all messages have the exact same timestamp
|
// Avoid division by zero if all messages have the exact same timestamp
|
||||||
// (which can happen easily if there's only one message in the channel)
|
// (which may be the case if there's only one message in the channel)
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
progress.Report(1);
|
progress.Report(1);
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ namespace DiscordChatExporter.Core.Discord
|
||||||
|
|
||||||
public Snowflake(ulong value) => Value = value;
|
public Snowflake(ulong value) => Value = value;
|
||||||
|
|
||||||
public DateTimeOffset ToDate() =>
|
public DateTimeOffset ToDate() => DateTimeOffset.FromUnixTimeMilliseconds(
|
||||||
DateTimeOffset.FromUnixTimeMilliseconds((long) ((Value >> 22) + 1420070400000UL)).ToLocalTime();
|
(long) ((Value >> 22) + 1420070400000UL)
|
||||||
|
).ToLocalTime();
|
||||||
|
|
||||||
public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);
|
public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
@ -53,9 +54,11 @@ namespace DiscordChatExporter.Core.Discord
|
||||||
public static Snowflake Parse(string str) => Parse(str, null);
|
public static Snowflake Parse(string str) => Parse(str, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial struct Snowflake : IEquatable<Snowflake>
|
public partial struct Snowflake : IComparable<Snowflake>, IEquatable<Snowflake>
|
||||||
{
|
{
|
||||||
public bool Equals(Snowflake other) => Value == other.Value;
|
public int CompareTo(Snowflake other) => Value.CompareTo(other.Value);
|
||||||
|
|
||||||
|
public bool Equals(Snowflake other) => CompareTo(other) == 0;
|
||||||
|
|
||||||
public override bool Equals(object? obj) => obj is Snowflake other && Equals(other);
|
public override bool Equals(object? obj) => obj is Snowflake other && Equals(other);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ namespace DiscordChatExporter.Core.Exporting
|
||||||
: filePath;
|
: filePath;
|
||||||
|
|
||||||
// HACK: for HTML, we need to format the URL properly
|
// HACK: for HTML, we need to format the URL properly
|
||||||
if (Request.Format == ExportFormat.HtmlDark || Request.Format == ExportFormat.HtmlLight)
|
if (Request.Format is ExportFormat.HtmlDark or ExportFormat.HtmlLight)
|
||||||
{
|
{
|
||||||
// Need to escape each path segment while keeping the directory separators intact
|
// Need to escape each path segment while keeping the directory separators intact
|
||||||
return relativeFilePath
|
return relativeFilePath
|
||||||
|
|
@ -93,7 +93,7 @@ namespace DiscordChatExporter.Core.Exporting
|
||||||
// Try to catch only exceptions related to failed HTTP requests
|
// Try to catch only exceptions related to failed HTTP requests
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/332
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/332
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/372
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/372
|
||||||
catch (Exception ex) when (ex is HttpRequestException || ex is OperationCanceledException)
|
catch (Exception ex) when (ex is HttpRequestException or OperationCanceledException)
|
||||||
{
|
{
|
||||||
// TODO: add logging so we can be more liberal with catching exceptions
|
// TODO: add logging so we can be more liberal with catching exceptions
|
||||||
// We don't want this to crash the exporting process in case of failure
|
// We don't want this to crash the exporting process in case of failure
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,13 @@ namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||||
internal partial class MessageGroup
|
internal partial class MessageGroup
|
||||||
{
|
{
|
||||||
public static bool CanJoin(Message message1, Message message2) =>
|
public static bool CanJoin(Message message1, Message message2) =>
|
||||||
|
// Must be from the same author
|
||||||
message1.Author.Id == message2.Author.Id &&
|
message1.Author.Id == message2.Author.Id &&
|
||||||
|
// Author's name must not have changed between messages
|
||||||
string.Equals(message1.Author.FullName, message2.Author.FullName, StringComparison.Ordinal) &&
|
string.Equals(message1.Author.FullName, message2.Author.FullName, StringComparison.Ordinal) &&
|
||||||
|
// Duration between messages must be 7 minutes or less
|
||||||
(message2.Timestamp - message1.Timestamp).Duration().TotalMinutes <= 7 &&
|
(message2.Timestamp - message1.Timestamp).Duration().TotalMinutes <= 7 &&
|
||||||
|
// Other message must not be a reply
|
||||||
message2.Reference is null;
|
message2.Reference is null;
|
||||||
|
|
||||||
public static MessageGroup Join(IReadOnlyList<Message> messages)
|
public static MessageGroup Join(IReadOnlyList<Message> messages)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ namespace DiscordChatExporter.Core.Exporting.Writers
|
||||||
{
|
{
|
||||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
Indented = true,
|
Indented = true,
|
||||||
|
// Validation errors may mask actual failures
|
||||||
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/413
|
||||||
SkipValidation = true
|
SkipValidation = true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,6 @@ namespace DiscordChatExporter.Core.Utils.Extensions
|
||||||
public static T? NullIf<T>(this T value, Func<T, bool> predicate) where T : struct =>
|
public static T? NullIf<T>(this T value, Func<T, bool> predicate) where T : struct =>
|
||||||
!predicate(value)
|
!predicate(value)
|
||||||
? value
|
? value
|
||||||
: (T?) null;
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,9 +4,14 @@ namespace DiscordChatExporter.Core.Utils.Extensions
|
||||||
{
|
{
|
||||||
public static class StringExtensions
|
public static class StringExtensions
|
||||||
{
|
{
|
||||||
|
public static string? NullIfWhiteSpace(this string str) =>
|
||||||
|
!string.IsNullOrWhiteSpace(str)
|
||||||
|
? str
|
||||||
|
: null;
|
||||||
|
|
||||||
public static string Truncate(this string str, int charCount) =>
|
public static string Truncate(this string str, int charCount) =>
|
||||||
str.Length > charCount
|
str.Length > charCount
|
||||||
? str.Substring(0, charCount)
|
? str[..charCount]
|
||||||
: str;
|
: str;
|
||||||
|
|
||||||
public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) =>
|
public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) =>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using Polly;
|
using Polly;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Utils
|
namespace DiscordChatExporter.Core.Utils
|
||||||
|
|
@ -41,21 +42,24 @@ namespace DiscordChatExporter.Core.Utils
|
||||||
},
|
},
|
||||||
(_, _, _, _) => Task.CompletedTask);
|
(_, _, _, _) => Task.CompletedTask);
|
||||||
|
|
||||||
private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex)
|
private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex) =>
|
||||||
{
|
|
||||||
// This is extremely frail, but there's no other way
|
// This is extremely frail, but there's no other way
|
||||||
var statusCodeRaw = Regex.Match(ex.Message, @": (\d+) \(").Groups[1].Value;
|
Regex
|
||||||
return !string.IsNullOrWhiteSpace(statusCodeRaw)
|
.Match(ex.Message, @": (\d+) \(")
|
||||||
? (HttpStatusCode) int.Parse(statusCodeRaw, CultureInfo.InvariantCulture)
|
.Groups[1]
|
||||||
: (HttpStatusCode?) null;
|
.Value
|
||||||
}
|
.NullIfWhiteSpace()?
|
||||||
|
.Pipe(s => (HttpStatusCode) int.Parse(s, CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
public static IAsyncPolicy ExceptionPolicy { get; } =
|
public static IAsyncPolicy ExceptionPolicy { get; } =
|
||||||
Policy
|
Policy
|
||||||
.Handle<IOException>() // dangerous
|
.Handle<IOException>() // dangerous
|
||||||
.Or<HttpRequestException>(ex => TryGetStatusCodeFromException(ex) == HttpStatusCode.TooManyRequests)
|
.Or<HttpRequestException>(ex =>
|
||||||
.Or<HttpRequestException>(ex => TryGetStatusCodeFromException(ex) == HttpStatusCode.RequestTimeout)
|
TryGetStatusCodeFromException(ex) is
|
||||||
.Or<HttpRequestException>(ex => TryGetStatusCodeFromException(ex) >= HttpStatusCode.InternalServerError)
|
HttpStatusCode.TooManyRequests or
|
||||||
|
HttpStatusCode.RequestTimeout or
|
||||||
|
HttpStatusCode.InternalServerError
|
||||||
|
)
|
||||||
.WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1));
|
.WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue