mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-22 10:55:15 -04:00
Embrace Snowflake as first class citizen
This commit is contained in:
parent
4ff7990967
commit
3d9ee3b339
36 changed files with 243 additions and 195 deletions
|
@ -1,49 +1,40 @@
|
||||||
using System;
|
using System.IO;
|
||||||
using System.IO;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Utilities;
|
using CliFx.Utilities;
|
||||||
|
using DiscordChatExporter.Domain.Discord;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
using DiscordChatExporter.Domain.Exporting;
|
using DiscordChatExporter.Domain.Exporting;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands.Base
|
namespace DiscordChatExporter.Cli.Commands.Base
|
||||||
{
|
{
|
||||||
public abstract partial class ExportCommandBase : TokenCommandBase
|
public abstract class ExportCommandBase : TokenCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("output", 'o',
|
[CommandOption("output", 'o', Description = "Output file or directory path.")]
|
||||||
Description = "Output file or directory path.")]
|
public string OutputPath { get; init; } = Directory.GetCurrentDirectory();
|
||||||
public string OutputPath { get; set; } = Directory.GetCurrentDirectory();
|
|
||||||
|
|
||||||
[CommandOption("format", 'f',
|
[CommandOption("format", 'f', Description = "Export format.")]
|
||||||
Description = "Export format.")]
|
public ExportFormat ExportFormat { get; init; } = ExportFormat.HtmlDark;
|
||||||
public ExportFormat ExportFormat { get; set; } = ExportFormat.HtmlDark;
|
|
||||||
|
|
||||||
[CommandOption("after",
|
[CommandOption("after", Description = "Only include messages sent after this date or message ID.")]
|
||||||
Description = "Only include messages sent after this date. Alternatively, provide the ID of a message.")]
|
public Snowflake? After { get; init; }
|
||||||
public string? After { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("before",
|
[CommandOption("before", Description = "Only include messages sent before this date or message ID.")]
|
||||||
Description = "Only include messages sent before this date. Alternatively, provide the ID of a message.")]
|
public Snowflake? Before { get; init; }
|
||||||
public string? Before { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("partition", 'p',
|
[CommandOption("partition", 'p', Description = "Split output into partitions limited to this number of messages.")]
|
||||||
Description = "Split output into partitions limited to this number of messages.")]
|
public int? PartitionLimit { get; init; }
|
||||||
public int? PartitionLimit { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("media",
|
[CommandOption("media", Description = "Download referenced media content.")]
|
||||||
Description = "Download referenced media content.")]
|
public bool ShouldDownloadMedia { get; init; }
|
||||||
public bool ShouldDownloadMedia { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("reuse-media",
|
[CommandOption("reuse-media", Description = "Reuse already existing media content to skip redundant downloads.")]
|
||||||
Description = "Reuse already existing media content to skip redundant downloads.")]
|
public bool ShouldReuseMedia { get; init; }
|
||||||
public bool ShouldReuseMedia { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("dateformat",
|
[CommandOption("dateformat", Description = "Format used when writing dates.")]
|
||||||
Description = "Format used when writing dates.")]
|
public string DateFormat { get; init; } = "dd-MMM-yy hh:mm tt";
|
||||||
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
|
|
||||||
|
|
||||||
protected ChannelExporter GetChannelExporter() => new(GetDiscordClient());
|
protected ChannelExporter GetChannelExporter() => new(GetDiscordClient());
|
||||||
|
|
||||||
|
@ -57,8 +48,8 @@ namespace DiscordChatExporter.Cli.Commands.Base
|
||||||
channel,
|
channel,
|
||||||
OutputPath,
|
OutputPath,
|
||||||
ExportFormat,
|
ExportFormat,
|
||||||
ParseRangeOption(After, "--after"),
|
After,
|
||||||
ParseRangeOption(Before, "--before"),
|
Before,
|
||||||
PartitionLimit,
|
PartitionLimit,
|
||||||
ShouldDownloadMedia,
|
ShouldDownloadMedia,
|
||||||
ShouldReuseMedia,
|
ShouldReuseMedia,
|
||||||
|
@ -77,7 +68,7 @@ namespace DiscordChatExporter.Cli.Commands.Base
|
||||||
await ExportAsync(console, guild, channel);
|
await ExportAsync(console, guild, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async ValueTask ExportAsync(IConsole console, string channelId)
|
protected async ValueTask ExportAsync(IConsole console, Snowflake channelId)
|
||||||
{
|
{
|
||||||
var channel = await GetDiscordClient().GetChannelAsync(channelId);
|
var channel = await GetDiscordClient().GetChannelAsync(channelId);
|
||||||
await ExportAsync(console, channel);
|
await ExportAsync(console, channel);
|
||||||
|
@ -93,29 +84,4 @@ namespace DiscordChatExporter.Cli.Commands.Base
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract partial class ExportCommandBase : TokenCommandBase
|
|
||||||
{
|
|
||||||
protected static DateTimeOffset? ParseRangeOption(string? value, string optionName)
|
|
||||||
{
|
|
||||||
if (value == null) return null;
|
|
||||||
|
|
||||||
var isSnowflake = Regex.IsMatch(value, @"^\d{18}$");
|
|
||||||
var isDate = DateTimeOffset.TryParse(value, out var datetime);
|
|
||||||
|
|
||||||
if (!isSnowflake && !isDate)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Value for ${optionName} must be either a date or a message ID.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return isSnowflake ? ExtractDateTimeFromSnowflake() : datetime;
|
|
||||||
|
|
||||||
DateTimeOffset ExtractDateTimeFromSnowflake()
|
|
||||||
{
|
|
||||||
var unixTimestampMs = (long.Parse(value) / 4194304 + 1420070400000);
|
|
||||||
return DateTimeOffset.FromUnixTimeMilliseconds(unixTimestampMs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -17,9 +17,8 @@ namespace DiscordChatExporter.Cli.Commands.Base
|
||||||
{
|
{
|
||||||
public abstract class ExportMultipleCommandBase : ExportCommandBase
|
public abstract class ExportMultipleCommandBase : ExportCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("parallel",
|
[CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")]
|
||||||
Description = "Limits how many channels can be exported in parallel.")]
|
public int ParallelLimit { get; init; } = 1;
|
||||||
public int ParallelLimit { get; set; } = 1;
|
|
||||||
|
|
||||||
protected async ValueTask ExportMultipleAsync(IConsole console, IReadOnlyList<Channel> channels)
|
protected async ValueTask ExportMultipleAsync(IConsole console, IReadOnlyList<Channel> channels)
|
||||||
{
|
{
|
||||||
|
@ -47,8 +46,8 @@ namespace DiscordChatExporter.Cli.Commands.Base
|
||||||
channel,
|
channel,
|
||||||
OutputPath,
|
OutputPath,
|
||||||
ExportFormat,
|
ExportFormat,
|
||||||
ParseRangeOption(After, "--after"),
|
After,
|
||||||
ParseRangeOption(Before, "--before"),
|
Before,
|
||||||
PartitionLimit,
|
PartitionLimit,
|
||||||
ShouldDownloadMedia,
|
ShouldDownloadMedia,
|
||||||
ShouldReuseMedia,
|
ShouldReuseMedia,
|
||||||
|
|
|
@ -7,15 +7,11 @@ namespace DiscordChatExporter.Cli.Commands.Base
|
||||||
{
|
{
|
||||||
public abstract class TokenCommandBase : ICommand
|
public abstract class TokenCommandBase : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption("token", 't', IsRequired = true,
|
[CommandOption("token", 't', IsRequired = true, EnvironmentVariableName = "DISCORD_TOKEN", Description = "Authorization token.")]
|
||||||
EnvironmentVariableName = "DISCORD_TOKEN",
|
public string TokenValue { get; init; } = "";
|
||||||
Description = "Authorization token.")]
|
|
||||||
public string TokenValue { get; set; } = "";
|
|
||||||
|
|
||||||
[CommandOption("bot", 'b',
|
[CommandOption("bot", 'b', EnvironmentVariableName = "DISCORD_TOKEN_BOT", Description = "Authorize as a bot.")]
|
||||||
EnvironmentVariableName = "DISCORD_TOKEN_BOT",
|
public bool IsBotToken { get; init; }
|
||||||
Description = "Authorize as a bot.")]
|
|
||||||
public bool IsBotToken { get; set; }
|
|
||||||
|
|
||||||
protected AuthToken GetAuthToken() => new(
|
protected AuthToken GetAuthToken() => new(
|
||||||
IsBotToken
|
IsBotToken
|
||||||
|
|
|
@ -10,9 +10,8 @@ namespace DiscordChatExporter.Cli.Commands
|
||||||
[Command("exportall", Description = "Export all accessible channels.")]
|
[Command("exportall", Description = "Export all accessible channels.")]
|
||||||
public class ExportAllCommand : ExportMultipleCommandBase
|
public class ExportAllCommand : ExportMultipleCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("include-dm",
|
[CommandOption("include-dm", Description = "Include direct message channels.")]
|
||||||
Description = "Include direct message channels.")]
|
public bool IncludeDirectMessages { get; init; } = true;
|
||||||
public bool IncludeDirectMessages { get; set; } = true;
|
|
||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,15 +2,15 @@
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
|
using DiscordChatExporter.Domain.Discord;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
[Command("export", Description = "Export a channel.")]
|
[Command("export", Description = "Export a channel.")]
|
||||||
public class ExportChannelCommand : ExportCommandBase
|
public class ExportChannelCommand : ExportCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("channel", 'c', IsRequired = true,
|
[CommandOption("channel", 'c', IsRequired = true, Description = "Channel ID.")]
|
||||||
Description = "Channel ID.")]
|
public Snowflake ChannelId { get; init; }
|
||||||
public string ChannelId { get; set; } = "";
|
|
||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
|
using DiscordChatExporter.Domain.Discord;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
|
@ -9,9 +10,8 @@ namespace DiscordChatExporter.Cli.Commands
|
||||||
[Command("exportguild", Description = "Export all channels within specified guild.")]
|
[Command("exportguild", Description = "Export all channels within specified guild.")]
|
||||||
public class ExportGuildCommand : ExportMultipleCommandBase
|
public class ExportGuildCommand : ExportMultipleCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("guild", 'g', IsRequired = true,
|
[CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")]
|
||||||
Description = "Guild ID.")]
|
public Snowflake GuildId { get; init; }
|
||||||
public string GuildId { get; set; } = "";
|
|
||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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.Discord;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
|
@ -10,9 +11,8 @@ namespace DiscordChatExporter.Cli.Commands
|
||||||
[Command("channels", Description = "Get the list of channels in a guild.")]
|
[Command("channels", Description = "Get the list of channels in a guild.")]
|
||||||
public class GetChannelsCommand : TokenCommandBase
|
public class GetChannelsCommand : TokenCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("guild", 'g', IsRequired = true,
|
[CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")]
|
||||||
Description = "Guild ID.")]
|
public Snowflake GuildId { get; init; }
|
||||||
public string GuildId { get; set; } = "";
|
|
||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|
9
DiscordChatExporter.Cli/Internal/Pollyfills.cs
Normal file
9
DiscordChatExporter.Cli/Internal/Pollyfills.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// ReSharper disable CheckNamespace
|
||||||
|
// TODO: remove after moving to .NET 5
|
||||||
|
|
||||||
|
namespace System.Runtime.CompilerServices
|
||||||
|
{
|
||||||
|
internal static class IsExternalInit
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
using DiscordChatExporter.Domain.Exceptions;
|
using DiscordChatExporter.Domain.Exceptions;
|
||||||
using DiscordChatExporter.Domain.Internal;
|
using DiscordChatExporter.Domain.Internal;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
using JsonExtensions.Http;
|
using JsonExtensions.Http;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
|
@ -70,13 +70,14 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
{
|
{
|
||||||
yield return Guild.DirectMessages;
|
yield return Guild.DirectMessages;
|
||||||
|
|
||||||
var afterId = "";
|
var currentAfter = Snowflake.Zero;
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var url = new UrlBuilder()
|
var url = new UrlBuilder()
|
||||||
.SetPath("users/@me/guilds")
|
.SetPath("users/@me/guilds")
|
||||||
.SetQueryParameter("limit", "100")
|
.SetQueryParameter("limit", "100")
|
||||||
.SetQueryParameter("after", afterId)
|
.SetQueryParameter("after", currentAfter.ToString())
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var response = await GetJsonResponseAsync(url);
|
var response = await GetJsonResponseAsync(url);
|
||||||
|
@ -86,7 +87,7 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
{
|
{
|
||||||
yield return guild;
|
yield return guild;
|
||||||
|
|
||||||
afterId = guild.Id;
|
currentAfter = guild.Id;
|
||||||
isEmpty = false;
|
isEmpty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +96,7 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Guild> GetGuildAsync(string guildId)
|
public async ValueTask<Guild> GetGuildAsync(Snowflake guildId)
|
||||||
{
|
{
|
||||||
if (guildId == Guild.DirectMessages.Id)
|
if (guildId == Guild.DirectMessages.Id)
|
||||||
return Guild.DirectMessages;
|
return Guild.DirectMessages;
|
||||||
|
@ -104,7 +105,7 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
return Guild.Parse(response);
|
return Guild.Parse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<Channel> GetGuildChannelsAsync(string guildId)
|
public async IAsyncEnumerable<Channel> GetGuildChannelsAsync(Snowflake guildId)
|
||||||
{
|
{
|
||||||
if (guildId == Guild.DirectMessages.Id)
|
if (guildId == Guild.DirectMessages.Id)
|
||||||
{
|
{
|
||||||
|
@ -141,7 +142,7 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<Role> GetGuildRolesAsync(string guildId)
|
public async IAsyncEnumerable<Role> GetGuildRolesAsync(Snowflake guildId)
|
||||||
{
|
{
|
||||||
if (guildId == Guild.DirectMessages.Id)
|
if (guildId == Guild.DirectMessages.Id)
|
||||||
yield break;
|
yield break;
|
||||||
|
@ -152,7 +153,7 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
yield return Role.Parse(roleJson);
|
yield return Role.Parse(roleJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Member?> TryGetGuildMemberAsync(string guildId, User user)
|
public async ValueTask<Member?> TryGetGuildMemberAsync(Snowflake guildId, User user)
|
||||||
{
|
{
|
||||||
if (guildId == Guild.DirectMessages.Id)
|
if (guildId == Guild.DirectMessages.Id)
|
||||||
return Member.CreateForUser(user);
|
return Member.CreateForUser(user);
|
||||||
|
@ -161,30 +162,31 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
return response?.Pipe(Member.Parse);
|
return response?.Pipe(Member.Parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<string> GetChannelCategoryAsync(string channelParentId)
|
private async ValueTask<string> GetChannelCategoryAsync(Snowflake channelParentId)
|
||||||
{
|
{
|
||||||
var response = await GetJsonResponseAsync($"channels/{channelParentId}");
|
var response = await GetJsonResponseAsync($"channels/{channelParentId}");
|
||||||
return response.GetProperty("name").GetString();
|
return response.GetProperty("name").GetString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Channel> GetChannelAsync(string channelId)
|
public async ValueTask<Channel> GetChannelAsync(Snowflake channelId)
|
||||||
{
|
{
|
||||||
var response = await GetJsonResponseAsync($"channels/{channelId}");
|
var response = await GetJsonResponseAsync($"channels/{channelId}");
|
||||||
|
|
||||||
var parentId = response.GetPropertyOrNull("parent_id")?.GetString();
|
var parentId = response.GetPropertyOrNull("parent_id")?.GetString().Pipe(Snowflake.Parse);
|
||||||
var category = !string.IsNullOrWhiteSpace(parentId)
|
|
||||||
? await GetChannelCategoryAsync(parentId)
|
var category = parentId != null
|
||||||
|
? await GetChannelCategoryAsync(parentId.Value)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Channel.Parse(response, category);
|
return Channel.Parse(response, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<Message?> TryGetLastMessageAsync(string channelId, DateTimeOffset? before = null)
|
private async ValueTask<Message?> TryGetLastMessageAsync(Snowflake channelId, Snowflake? before = null)
|
||||||
{
|
{
|
||||||
var url = new UrlBuilder()
|
var url = new UrlBuilder()
|
||||||
.SetPath($"channels/{channelId}/messages")
|
.SetPath($"channels/{channelId}/messages")
|
||||||
.SetQueryParameter("limit", "1")
|
.SetQueryParameter("limit", "1")
|
||||||
.SetQueryParameter("before", before?.ToSnowflake())
|
.SetQueryParameter("before", before?.ToString())
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var response = await GetJsonResponseAsync(url);
|
var response = await GetJsonResponseAsync(url);
|
||||||
|
@ -192,9 +194,9 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<Message> GetMessagesAsync(
|
public async IAsyncEnumerable<Message> GetMessagesAsync(
|
||||||
string channelId,
|
Snowflake channelId,
|
||||||
DateTimeOffset? after = null,
|
Snowflake? after = null,
|
||||||
DateTimeOffset? before = null,
|
Snowflake? before = null,
|
||||||
IProgress<double>? progress = null)
|
IProgress<double>? progress = null)
|
||||||
{
|
{
|
||||||
// Get the last message in the specified range.
|
// Get the last message in the specified range.
|
||||||
|
@ -202,19 +204,19 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
// 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);
|
||||||
if (lastMessage == null || lastMessage.Timestamp < after)
|
if (lastMessage == null || lastMessage.Timestamp < after?.ToDate())
|
||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
// Keep track of first message in range in order to calculate progress
|
// Keep track of first message in range in order to calculate progress
|
||||||
var firstMessage = default(Message);
|
var firstMessage = default(Message);
|
||||||
var afterId = after?.ToSnowflake() ?? "0";
|
var currentAfter = after ?? Snowflake.Zero;
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var url = new UrlBuilder()
|
var url = new UrlBuilder()
|
||||||
.SetPath($"channels/{channelId}/messages")
|
.SetPath($"channels/{channelId}/messages")
|
||||||
.SetQueryParameter("limit", "100")
|
.SetQueryParameter("limit", "100")
|
||||||
.SetQueryParameter("after", afterId)
|
.SetQueryParameter("after", currentAfter.ToString())
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var response = await GetJsonResponseAsync(url);
|
var response = await GetJsonResponseAsync(url);
|
||||||
|
@ -244,7 +246,7 @@ namespace DiscordChatExporter.Domain.Discord
|
||||||
);
|
);
|
||||||
|
|
||||||
yield return message;
|
yield return message;
|
||||||
afterId = message.Id;
|
currentAfter = message.Id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
@ -11,7 +11,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
// https://discord.com/developers/docs/resources/channel#attachment-object
|
// https://discord.com/developers/docs/resources/channel#attachment-object
|
||||||
public partial class Attachment : IHasId
|
public partial class Attachment : IHasId
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public Snowflake Id { get; }
|
||||||
|
|
||||||
public string Url { get; }
|
public string Url { get; }
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public FileSize FileSize { get; }
|
public FileSize FileSize { get; }
|
||||||
|
|
||||||
public Attachment(string id, string url, string fileName, int? width, int? height, FileSize fileSize)
|
public Attachment(Snowflake id, string url, string fileName, int? width, int? height, FileSize fileSize)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Url = url;
|
Url = url;
|
||||||
|
@ -58,7 +58,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public static Attachment Parse(JsonElement json)
|
public static Attachment Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetString();
|
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
||||||
var url = json.GetProperty("url").GetString();
|
var url = json.GetProperty("url").GetString();
|
||||||
var width = json.GetPropertyOrNull("width")?.GetInt32();
|
var width = json.GetPropertyOrNull("width")?.GetInt32();
|
||||||
var height = json.GetPropertyOrNull("height")?.GetInt32();
|
var height = json.GetPropertyOrNull("height")?.GetInt32();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
// https://discord.com/developers/docs/resources/channel#channel-object
|
// https://discord.com/developers/docs/resources/channel#channel-object
|
||||||
public partial class Channel : IHasId
|
public partial class Channel : IHasId
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public Snowflake Id { get; }
|
||||||
|
|
||||||
public ChannelType Type { get; }
|
public ChannelType Type { get; }
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
Type == ChannelType.GuildNews ||
|
Type == ChannelType.GuildNews ||
|
||||||
Type == ChannelType.GuildStore;
|
Type == ChannelType.GuildStore;
|
||||||
|
|
||||||
public string GuildId { get; }
|
public Snowflake GuildId { get; }
|
||||||
|
|
||||||
public string Category { get; }
|
public string Category { get; }
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public string? Topic { get; }
|
public string? Topic { get; }
|
||||||
|
|
||||||
public Channel(string id, ChannelType type, string guildId, string category, string name, string? topic)
|
public Channel(Snowflake id, ChannelType type, Snowflake guildId, string category, string name, string? topic)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Type = type;
|
Type = type;
|
||||||
|
@ -68,8 +69,8 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public static Channel Parse(JsonElement json, string? category = null)
|
public static Channel Parse(JsonElement json, string? category = null)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetString();
|
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
||||||
var guildId = json.GetPropertyOrNull("guild_id")?.GetString();
|
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();
|
||||||
|
@ -77,7 +78,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
var name =
|
var name =
|
||||||
json.GetPropertyOrNull("name")?.GetString() ??
|
json.GetPropertyOrNull("name")?.GetString() ??
|
||||||
json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(User.Parse).Select(u => u.Name).JoinToString(", ") ??
|
json.GetPropertyOrNull("recipients")?.EnumerateArray().Select(User.Parse).Select(u => u.Name).JoinToString(", ") ??
|
||||||
id;
|
id.ToString();
|
||||||
|
|
||||||
return new Channel(
|
return new Channel(
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
{
|
{
|
||||||
public interface IHasId
|
public interface IHasId
|
||||||
{
|
{
|
||||||
string Id { get; }
|
Snowflake Id { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,9 +5,9 @@ namespace DiscordChatExporter.Domain.Discord.Models.Common
|
||||||
{
|
{
|
||||||
public partial class IdBasedEqualityComparer : IEqualityComparer<IHasId>
|
public partial class IdBasedEqualityComparer : IEqualityComparer<IHasId>
|
||||||
{
|
{
|
||||||
public bool Equals(IHasId? x, IHasId? y) => StringComparer.Ordinal.Equals(x?.Id, y?.Id);
|
public bool Equals(IHasId? x, IHasId? y) => x?.Id == y?.Id;
|
||||||
|
|
||||||
public int GetHashCode(IHasId obj) => StringComparer.Ordinal.GetHashCode(obj.Id);
|
public int GetHashCode(IHasId obj) => obj.Id.GetHashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class IdBasedEqualityComparer
|
public partial class IdBasedEqualityComparer
|
||||||
|
|
|
@ -4,6 +4,7 @@ using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Domain.Internal.Extensions;
|
||||||
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/guild#guild-object
|
// https://discord.com/developers/docs/resources/guild#guild-object
|
||||||
public partial class Guild : IHasId
|
public partial class Guild : IHasId
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public Snowflake Id { get; }
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public string IconUrl { get; }
|
public string IconUrl { get; }
|
||||||
|
|
||||||
public Guild(string id, string name, string iconUrl)
|
public Guild(Snowflake id, string name, string iconUrl)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
|
@ -24,17 +25,17 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public partial class Guild
|
public partial class Guild
|
||||||
{
|
{
|
||||||
public static Guild DirectMessages { get; } = new("@me", "Direct Messages", GetDefaultIconUrl());
|
public static Guild DirectMessages { get; } = new(Snowflake.Zero, "Direct Messages", GetDefaultIconUrl());
|
||||||
|
|
||||||
private static string GetDefaultIconUrl() =>
|
private static string GetDefaultIconUrl() =>
|
||||||
"https://cdn.discordapp.com/embed/avatars/0.png";
|
"https://cdn.discordapp.com/embed/avatars/0.png";
|
||||||
|
|
||||||
private static string GetIconUrl(string id, string iconHash) =>
|
private static string GetIconUrl(Snowflake id, string iconHash) =>
|
||||||
$"https://cdn.discordapp.com/icons/{id}/{iconHash}.png";
|
$"https://cdn.discordapp.com/icons/{id}/{iconHash}.png";
|
||||||
|
|
||||||
public static Guild Parse(JsonElement json)
|
public static Guild Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetString();
|
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
||||||
var name = json.GetProperty("name").GetString();
|
var name = json.GetProperty("name").GetString();
|
||||||
var iconHash = json.GetProperty("icon").GetString();
|
var iconHash = json.GetProperty("icon").GetString();
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
@ -11,15 +11,15 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
// https://discord.com/developers/docs/resources/guild#guild-member-object
|
// https://discord.com/developers/docs/resources/guild#guild-member-object
|
||||||
public partial class Member : IHasId
|
public partial class Member : IHasId
|
||||||
{
|
{
|
||||||
public string Id => User.Id;
|
public Snowflake Id => User.Id;
|
||||||
|
|
||||||
public User User { get; }
|
public User User { get; }
|
||||||
|
|
||||||
public string Nick { get; }
|
public string Nick { get; }
|
||||||
|
|
||||||
public IReadOnlyList<string> RoleIds { get; }
|
public IReadOnlyList<Snowflake> RoleIds { get; }
|
||||||
|
|
||||||
public Member(User user, string? nick, IReadOnlyList<string> roleIds)
|
public Member(User user, string? nick, IReadOnlyList<Snowflake> roleIds)
|
||||||
{
|
{
|
||||||
User = user;
|
User = user;
|
||||||
Nick = nick ?? user.Name;
|
Nick = nick ?? user.Name;
|
||||||
|
@ -31,7 +31,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public partial class Member
|
public partial class Member
|
||||||
{
|
{
|
||||||
public static Member CreateForUser(User user) => new(user, null, Array.Empty<string>());
|
public static Member CreateForUser(User user) => new(user, null, Array.Empty<Snowflake>());
|
||||||
|
|
||||||
public static Member Parse(JsonElement json)
|
public static Member Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
|
@ -39,8 +39,8 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
var nick = json.GetPropertyOrNull("nick")?.GetString();
|
var nick = json.GetPropertyOrNull("nick")?.GetString();
|
||||||
|
|
||||||
var roleIds =
|
var roleIds =
|
||||||
json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString()).ToArray() ??
|
json.GetPropertyOrNull("roles")?.EnumerateArray().Select(j => j.GetString().Pipe(Snowflake.Parse)).ToArray() ??
|
||||||
Array.Empty<string>();
|
Array.Empty<Snowflake>();
|
||||||
|
|
||||||
return new Member(
|
return new Member(
|
||||||
user,
|
user,
|
||||||
|
|
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
@ -24,7 +24,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
// https://discord.com/developers/docs/resources/channel#message-object
|
// https://discord.com/developers/docs/resources/channel#message-object
|
||||||
public partial class Message : IHasId
|
public partial class Message : IHasId
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public Snowflake Id { get; }
|
||||||
|
|
||||||
public MessageType Type { get; }
|
public MessageType Type { get; }
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
public IReadOnlyList<User> MentionedUsers { get; }
|
public IReadOnlyList<User> MentionedUsers { get; }
|
||||||
|
|
||||||
public Message(
|
public Message(
|
||||||
string id,
|
Snowflake id,
|
||||||
MessageType type,
|
MessageType type,
|
||||||
User author,
|
User author,
|
||||||
DateTimeOffset timestamp,
|
DateTimeOffset timestamp,
|
||||||
|
@ -83,7 +83,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
public static Message Parse(JsonElement json)
|
public static Message Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetString();
|
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
||||||
var author = json.GetProperty("author").Pipe(User.Parse);
|
var author = json.GetProperty("author").Pipe(User.Parse);
|
||||||
var timestamp = json.GetProperty("timestamp").GetDateTimeOffset();
|
var timestamp = json.GetProperty("timestamp").GetDateTimeOffset();
|
||||||
var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffset();
|
var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffset();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Domain.Internal.Extensions;
|
||||||
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/topics/permissions#role-object
|
// https://discord.com/developers/docs/topics/permissions#role-object
|
||||||
public partial class Role
|
public partial class Role : IHasId
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public Snowflake Id { get; }
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
|
@ -16,7 +18,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public Color? Color { get; }
|
public Color? Color { get; }
|
||||||
|
|
||||||
public Role(string id, string name, int position, Color? color)
|
public Role(Snowflake id, string name, int position, Color? color)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
|
@ -31,7 +33,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
{
|
{
|
||||||
public static Role Parse(JsonElement json)
|
public static Role Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetString();
|
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
||||||
var name = json.GetProperty("name").GetString();
|
var name = json.GetProperty("name").GetString();
|
||||||
var position = json.GetProperty("position").GetInt32();
|
var position = json.GetProperty("position").GetInt32();
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Domain.Discord.Models.Common;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
@ -9,7 +9,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
// https://discord.com/developers/docs/resources/user#user-object
|
// https://discord.com/developers/docs/resources/user#user-object
|
||||||
public partial class User : IHasId
|
public partial class User : IHasId
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public Snowflake Id { get; }
|
||||||
|
|
||||||
public bool IsBot { get; }
|
public bool IsBot { get; }
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public string AvatarUrl { get; }
|
public string AvatarUrl { get; }
|
||||||
|
|
||||||
public User(string id, bool isBot, int discriminator, string name, string avatarUrl)
|
public User(Snowflake id, bool isBot, int discriminator, string name, string avatarUrl)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
IsBot = isBot;
|
IsBot = isBot;
|
||||||
|
@ -38,7 +38,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
private static string GetDefaultAvatarUrl(int discriminator) =>
|
private static string GetDefaultAvatarUrl(int discriminator) =>
|
||||||
$"https://cdn.discordapp.com/embed/avatars/{discriminator % 5}.png";
|
$"https://cdn.discordapp.com/embed/avatars/{discriminator % 5}.png";
|
||||||
|
|
||||||
private static string GetAvatarUrl(string id, string avatarHash)
|
private static string GetAvatarUrl(Snowflake id, string avatarHash)
|
||||||
{
|
{
|
||||||
// Animated
|
// Animated
|
||||||
if (avatarHash.StartsWith("a_", StringComparison.Ordinal))
|
if (avatarHash.StartsWith("a_", StringComparison.Ordinal))
|
||||||
|
@ -50,7 +50,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
||||||
|
|
||||||
public static User Parse(JsonElement json)
|
public static User Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetString();
|
var id = json.GetProperty("id").GetString().Pipe(Snowflake.Parse);
|
||||||
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();
|
||||||
|
|
68
DiscordChatExporter.Domain/Discord/Snowflake.cs
Normal file
68
DiscordChatExporter.Domain/Discord/Snowflake.cs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Domain.Discord
|
||||||
|
{
|
||||||
|
public readonly partial struct Snowflake
|
||||||
|
{
|
||||||
|
public ulong Value { get; }
|
||||||
|
|
||||||
|
public Snowflake(ulong value) => Value = value;
|
||||||
|
|
||||||
|
public DateTimeOffset ToDate() =>
|
||||||
|
DateTimeOffset.FromUnixTimeMilliseconds((long) ((Value >> 22) + 1420070400000UL)).ToLocalTime();
|
||||||
|
|
||||||
|
public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial struct Snowflake
|
||||||
|
{
|
||||||
|
public static Snowflake Zero { get; } = new(0);
|
||||||
|
|
||||||
|
public static Snowflake FromDate(DateTimeOffset date)
|
||||||
|
{
|
||||||
|
var value = ((ulong) date.ToUnixTimeMilliseconds() - 1420070400000UL) << 22;
|
||||||
|
return new Snowflake(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snowflake? TryParse(string? str, IFormatProvider? formatProvider = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// As number
|
||||||
|
if (Regex.IsMatch(str, @"^\d{15,}$") &&
|
||||||
|
ulong.TryParse(str, NumberStyles.Number, formatProvider, out var value))
|
||||||
|
{
|
||||||
|
return new Snowflake(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// As date
|
||||||
|
if (DateTimeOffset.TryParse(str, formatProvider, DateTimeStyles.None, out var date))
|
||||||
|
{
|
||||||
|
return FromDate(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snowflake Parse(string str, IFormatProvider? formatProvider) =>
|
||||||
|
TryParse(str, formatProvider) ?? throw new FormatException($"Invalid snowflake: {str}.");
|
||||||
|
|
||||||
|
public static Snowflake Parse(string str) => Parse(str, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial struct Snowflake : IEquatable<Snowflake>
|
||||||
|
{
|
||||||
|
public bool Equals(Snowflake other) => Value == other.Value;
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) => obj is Snowflake other && Equals(other);
|
||||||
|
|
||||||
|
public override int GetHashCode() => Value.GetHashCode();
|
||||||
|
|
||||||
|
public static bool operator ==(Snowflake left, Snowflake right) => left.Equals(right);
|
||||||
|
|
||||||
|
public static bool operator !=(Snowflake left, Snowflake right) => !(left == right);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Domain.Discord;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Domain.Internal.Extensions;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
@ -44,16 +45,13 @@ namespace DiscordChatExporter.Domain.Exporting
|
||||||
var dateFormat => date.ToLocalString(dateFormat)
|
var dateFormat => date.ToLocalString(dateFormat)
|
||||||
};
|
};
|
||||||
|
|
||||||
public Member? TryGetMember(string id) =>
|
public Member? TryGetMember(Snowflake id) => Members.FirstOrDefault(m => m.Id == id);
|
||||||
Members.FirstOrDefault(m => m.Id == id);
|
|
||||||
|
|
||||||
public Channel? TryGetChannel(string id) =>
|
public Channel? TryGetChannel(Snowflake id) => Channels.FirstOrDefault(c => c.Id == id);
|
||||||
Channels.FirstOrDefault(c => c.Id == id);
|
|
||||||
|
|
||||||
public Role? TryGetRole(string id) =>
|
public Role? TryGetRole(Snowflake id) => Roles.FirstOrDefault(r => r.Id == id);
|
||||||
Roles.FirstOrDefault(r => r.Id == id);
|
|
||||||
|
|
||||||
public Color? TryGetUserColor(string id)
|
public Color? TryGetUserColor(Snowflake id)
|
||||||
{
|
{
|
||||||
var member = TryGetMember(id);
|
var member = TryGetMember(id);
|
||||||
var roles = member?.RoleIds.Join(Roles, i => i, r => r.Id, (_, role) => role);
|
var roles = member?.RoleIds.Join(Roles, i => i, r => r.Id, (_, role) => role);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System.IO;
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using DiscordChatExporter.Domain.Discord;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
using DiscordChatExporter.Domain.Internal;
|
using DiscordChatExporter.Domain.Internal;
|
||||||
|
|
||||||
|
@ -22,9 +22,9 @@ namespace DiscordChatExporter.Domain.Exporting
|
||||||
|
|
||||||
public ExportFormat Format { get; }
|
public ExportFormat Format { get; }
|
||||||
|
|
||||||
public DateTimeOffset? After { get; }
|
public Snowflake? After { get; }
|
||||||
|
|
||||||
public DateTimeOffset? Before { get; }
|
public Snowflake? Before { get; }
|
||||||
|
|
||||||
public int? PartitionLimit { get; }
|
public int? PartitionLimit { get; }
|
||||||
|
|
||||||
|
@ -39,8 +39,8 @@ namespace DiscordChatExporter.Domain.Exporting
|
||||||
Channel channel,
|
Channel channel,
|
||||||
string outputPath,
|
string outputPath,
|
||||||
ExportFormat format,
|
ExportFormat format,
|
||||||
DateTimeOffset? after,
|
Snowflake? after,
|
||||||
DateTimeOffset? before,
|
Snowflake? before,
|
||||||
int? partitionLimit,
|
int? partitionLimit,
|
||||||
bool shouldDownloadMedia,
|
bool shouldDownloadMedia,
|
||||||
bool shouldReuseMedia,
|
bool shouldReuseMedia,
|
||||||
|
@ -78,8 +78,8 @@ namespace DiscordChatExporter.Domain.Exporting
|
||||||
Channel channel,
|
Channel channel,
|
||||||
string outputPath,
|
string outputPath,
|
||||||
ExportFormat format,
|
ExportFormat format,
|
||||||
DateTimeOffset? after = null,
|
Snowflake? after = null,
|
||||||
DateTimeOffset? before = null)
|
Snowflake? before = null)
|
||||||
{
|
{
|
||||||
// Output is a directory
|
// Output is a directory
|
||||||
if (Directory.Exists(outputPath) || string.IsNullOrWhiteSpace(Path.GetExtension(outputPath)))
|
if (Directory.Exists(outputPath) || string.IsNullOrWhiteSpace(Path.GetExtension(outputPath)))
|
||||||
|
@ -96,8 +96,8 @@ namespace DiscordChatExporter.Domain.Exporting
|
||||||
Guild guild,
|
Guild guild,
|
||||||
Channel channel,
|
Channel channel,
|
||||||
ExportFormat format,
|
ExportFormat format,
|
||||||
DateTimeOffset? after = null,
|
Snowflake? after = null,
|
||||||
DateTimeOffset? before = null)
|
Snowflake? before = null)
|
||||||
{
|
{
|
||||||
var buffer = new StringBuilder();
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
|
@ -112,17 +112,17 @@ namespace DiscordChatExporter.Domain.Exporting
|
||||||
// Both 'after' and 'before' are set
|
// Both 'after' and 'before' are set
|
||||||
if (after != null && before != null)
|
if (after != null && before != null)
|
||||||
{
|
{
|
||||||
buffer.Append($"{after:yyyy-MM-dd} to {before:yyyy-MM-dd}");
|
buffer.Append($"{after?.ToDate():yyyy-MM-dd} to {before?.ToDate():yyyy-MM-dd}");
|
||||||
}
|
}
|
||||||
// Only 'after' is set
|
// Only 'after' is set
|
||||||
else if (after != null)
|
else if (after != null)
|
||||||
{
|
{
|
||||||
buffer.Append($"after {after:yyyy-MM-dd}");
|
buffer.Append($"after {after?.ToDate():yyyy-MM-dd}");
|
||||||
}
|
}
|
||||||
// Only 'before' is set
|
// Only 'before' is set
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
buffer.Append($"before {before:yyyy-MM-dd}");
|
buffer.Append($"before {before?.ToDate():yyyy-MM-dd}");
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.Append(")");
|
buffer.Append(")");
|
||||||
|
|
|
@ -9,6 +9,7 @@ using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Internal;
|
using DiscordChatExporter.Domain.Internal;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Domain.Internal.Extensions;
|
||||||
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting
|
namespace DiscordChatExporter.Domain.Exporting
|
||||||
{
|
{
|
||||||
|
|
|
@ -59,7 +59,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
public override async ValueTask WriteMessageAsync(Message message)
|
public override async ValueTask WriteMessageAsync(Message message)
|
||||||
{
|
{
|
||||||
// Author ID
|
// Author ID
|
||||||
await _writer.WriteAsync(CsvEncode(message.Author.Id));
|
await _writer.WriteAsync(CsvEncode(message.Author.Id.ToString()));
|
||||||
await _writer.WriteAsync(',');
|
await _writer.WriteAsync(',');
|
||||||
|
|
||||||
// Author name
|
// Author name
|
||||||
|
|
|
@ -25,7 +25,7 @@ namespace DiscordChatExporter.Domain.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) =>
|
||||||
string.Equals(message1.Author.Id, message2.Author.Id, StringComparison.Ordinal) &&
|
message1.Author.Id == message2.Author.Id &&
|
||||||
string.Equals(message1.Author.FullName, message2.Author.FullName, StringComparison.Ordinal) &&
|
string.Equals(message1.Author.FullName, message2.Author.FullName, StringComparison.Ordinal) &&
|
||||||
(message2.Timestamp - message1.Timestamp).Duration().TotalMinutes <= 7;
|
(message2.Timestamp - message1.Timestamp).Duration().TotalMinutes <= 7;
|
||||||
|
|
||||||
|
|
|
@ -83,15 +83,15 @@
|
||||||
<div class="preamble__entry preamble__entry--small">
|
<div class="preamble__entry preamble__entry--small">
|
||||||
@if (Model.ExportContext.Request.After != null && Model.ExportContext.Request.Before != null)
|
@if (Model.ExportContext.Request.After != null && Model.ExportContext.Request.Before != null)
|
||||||
{
|
{
|
||||||
@($"Between {FormatDate(Model.ExportContext.Request.After.Value)} and {FormatDate(Model.ExportContext.Request.Before.Value)}")
|
@($"Between {FormatDate(Model.ExportContext.Request.After.Value.ToDate())} and {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}")
|
||||||
}
|
}
|
||||||
else if (Model.ExportContext.Request.After != null)
|
else if (Model.ExportContext.Request.After != null)
|
||||||
{
|
{
|
||||||
@($"After {FormatDate(Model.ExportContext.Request.After.Value)}")
|
@($"After {FormatDate(Model.ExportContext.Request.After.Value.ToDate())}")
|
||||||
}
|
}
|
||||||
else if (Model.ExportContext.Request.Before != null)
|
else if (Model.ExportContext.Request.Before != null)
|
||||||
{
|
{
|
||||||
@($"Before {FormatDate(Model.ExportContext.Request.Before.Value)}")
|
@($"Before {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}")
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
{
|
{
|
||||||
_writer.WriteStartObject();
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
_writer.WriteString("id", attachment.Id);
|
_writer.WriteString("id", attachment.Id.ToString());
|
||||||
_writer.WriteString("url", await Context.ResolveMediaUrlAsync(attachment.Url));
|
_writer.WriteString("url", await Context.ResolveMediaUrlAsync(attachment.Url));
|
||||||
_writer.WriteString("fileName", attachment.FileName);
|
_writer.WriteString("fileName", attachment.FileName);
|
||||||
_writer.WriteNumber("fileSizeBytes", attachment.FileSize.TotalBytes);
|
_writer.WriteNumber("fileSizeBytes", attachment.FileSize.TotalBytes);
|
||||||
|
@ -166,7 +166,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
{
|
{
|
||||||
_writer.WriteStartObject();
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
_writer.WriteString("id", mentionedUser.Id);
|
_writer.WriteString("id", mentionedUser.Id.ToString());
|
||||||
_writer.WriteString("name", mentionedUser.Name);
|
_writer.WriteString("name", mentionedUser.Name);
|
||||||
_writer.WriteNumber("discriminator", mentionedUser.Discriminator);
|
_writer.WriteNumber("discriminator", mentionedUser.Discriminator);
|
||||||
_writer.WriteString("nickname", Context.TryGetMember(mentionedUser.Id)?.Nick ?? mentionedUser.Name);
|
_writer.WriteString("nickname", Context.TryGetMember(mentionedUser.Id)?.Nick ?? mentionedUser.Name);
|
||||||
|
@ -183,14 +183,14 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
|
|
||||||
// Guild
|
// Guild
|
||||||
_writer.WriteStartObject("guild");
|
_writer.WriteStartObject("guild");
|
||||||
_writer.WriteString("id", Context.Request.Guild.Id);
|
_writer.WriteString("id", Context.Request.Guild.Id.ToString());
|
||||||
_writer.WriteString("name", Context.Request.Guild.Name);
|
_writer.WriteString("name", Context.Request.Guild.Name);
|
||||||
_writer.WriteString("iconUrl", await Context.ResolveMediaUrlAsync(Context.Request.Guild.IconUrl));
|
_writer.WriteString("iconUrl", await Context.ResolveMediaUrlAsync(Context.Request.Guild.IconUrl));
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
|
|
||||||
// Channel
|
// Channel
|
||||||
_writer.WriteStartObject("channel");
|
_writer.WriteStartObject("channel");
|
||||||
_writer.WriteString("id", Context.Request.Channel.Id);
|
_writer.WriteString("id", Context.Request.Channel.Id.ToString());
|
||||||
_writer.WriteString("type", Context.Request.Channel.Type.ToString());
|
_writer.WriteString("type", Context.Request.Channel.Type.ToString());
|
||||||
_writer.WriteString("category", Context.Request.Channel.Category);
|
_writer.WriteString("category", Context.Request.Channel.Category);
|
||||||
_writer.WriteString("name", Context.Request.Channel.Name);
|
_writer.WriteString("name", Context.Request.Channel.Name);
|
||||||
|
@ -199,8 +199,8 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
|
|
||||||
// Date range
|
// Date range
|
||||||
_writer.WriteStartObject("dateRange");
|
_writer.WriteStartObject("dateRange");
|
||||||
_writer.WriteString("after", Context.Request.After);
|
_writer.WriteString("after", Context.Request.After?.ToDate());
|
||||||
_writer.WriteString("before", Context.Request.Before);
|
_writer.WriteString("before", Context.Request.Before?.ToDate());
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
|
|
||||||
// Message array (start)
|
// Message array (start)
|
||||||
|
@ -213,7 +213,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
_writer.WriteStartObject();
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
// Metadata
|
// Metadata
|
||||||
_writer.WriteString("id", message.Id);
|
_writer.WriteString("id", message.Id.ToString());
|
||||||
_writer.WriteString("type", message.Type.ToString());
|
_writer.WriteString("type", message.Type.ToString());
|
||||||
_writer.WriteString("timestamp", message.Timestamp);
|
_writer.WriteString("timestamp", message.Timestamp);
|
||||||
_writer.WriteString("timestampEdited", message.EditedTimestamp);
|
_writer.WriteString("timestampEdited", message.EditedTimestamp);
|
||||||
|
@ -225,7 +225,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
|
|
||||||
// Author
|
// Author
|
||||||
_writer.WriteStartObject("author");
|
_writer.WriteStartObject("author");
|
||||||
_writer.WriteString("id", message.Author.Id);
|
_writer.WriteString("id", message.Author.Id.ToString());
|
||||||
_writer.WriteString("name", message.Author.Name);
|
_writer.WriteString("name", message.Author.Name);
|
||||||
_writer.WriteString("discriminator", $"{message.Author.Discriminator:0000}");
|
_writer.WriteString("discriminator", $"{message.Author.Discriminator:0000}");
|
||||||
_writer.WriteBoolean("isBot", message.Author.IsBot);
|
_writer.WriteBoolean("isBot", message.Author.IsBot);
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using DiscordChatExporter.Domain.Discord;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
using DiscordChatExporter.Domain.Markdown;
|
using DiscordChatExporter.Domain.Markdown;
|
||||||
using DiscordChatExporter.Domain.Markdown.Ast;
|
using DiscordChatExporter.Domain.Markdown.Ast;
|
||||||
|
@ -84,7 +85,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.User)
|
else if (mention.Type == MentionType.User)
|
||||||
{
|
{
|
||||||
var member = _context.TryGetMember(mention.Id);
|
var member = _context.TryGetMember(Snowflake.Parse(mention.Id));
|
||||||
var fullName = member?.User.FullName ?? "Unknown";
|
var fullName = member?.User.FullName ?? "Unknown";
|
||||||
var nick = member?.Nick ?? "Unknown";
|
var nick = member?.Nick ?? "Unknown";
|
||||||
|
|
||||||
|
@ -95,7 +96,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.Channel)
|
else if (mention.Type == MentionType.Channel)
|
||||||
{
|
{
|
||||||
var channel = _context.TryGetChannel(mention.Id);
|
var channel = _context.TryGetChannel(Snowflake.Parse(mention.Id));
|
||||||
var name = channel?.Name ?? "deleted-channel";
|
var name = channel?.Name ?? "deleted-channel";
|
||||||
|
|
||||||
_buffer
|
_buffer
|
||||||
|
@ -105,7 +106,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.Role)
|
else if (mention.Type == MentionType.Role)
|
||||||
{
|
{
|
||||||
var role = _context.TryGetRole(mention.Id);
|
var role = _context.TryGetRole(Snowflake.Parse(mention.Id));
|
||||||
var name = role?.Name ?? "deleted-role";
|
var name = role?.Name ?? "deleted-role";
|
||||||
var color = role?.Color;
|
var color = role?.Color;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using DiscordChatExporter.Domain.Discord;
|
||||||
using DiscordChatExporter.Domain.Markdown;
|
using DiscordChatExporter.Domain.Markdown;
|
||||||
using DiscordChatExporter.Domain.Markdown.Ast;
|
using DiscordChatExporter.Domain.Markdown.Ast;
|
||||||
|
|
||||||
|
@ -29,21 +30,21 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.User)
|
else if (mention.Type == MentionType.User)
|
||||||
{
|
{
|
||||||
var member = _context.TryGetMember(mention.Id);
|
var member = _context.TryGetMember(Snowflake.Parse(mention.Id));
|
||||||
var name = member?.User.Name ?? "Unknown";
|
var name = member?.User.Name ?? "Unknown";
|
||||||
|
|
||||||
_buffer.Append($"@{name}");
|
_buffer.Append($"@{name}");
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.Channel)
|
else if (mention.Type == MentionType.Channel)
|
||||||
{
|
{
|
||||||
var channel = _context.TryGetChannel(mention.Id);
|
var channel = _context.TryGetChannel(Snowflake.Parse(mention.Id));
|
||||||
var name = channel?.Name ?? "deleted-channel";
|
var name = channel?.Name ?? "deleted-channel";
|
||||||
|
|
||||||
_buffer.Append($"#{name}");
|
_buffer.Append($"#{name}");
|
||||||
}
|
}
|
||||||
else if (mention.Type == MentionType.Role)
|
else if (mention.Type == MentionType.Role)
|
||||||
{
|
{
|
||||||
var role = _context.TryGetRole(mention.Id);
|
var role = _context.TryGetRole(Snowflake.Parse(mention.Id));
|
||||||
var name = role?.Name ?? "deleted-role";
|
var name = role?.Name ?? "deleted-role";
|
||||||
|
|
||||||
_buffer.Append($"@{name}");
|
_buffer.Append($"@{name}");
|
||||||
|
|
|
@ -119,10 +119,10 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
||||||
await _writer.WriteLineAsync($"Topic: {Context.Request.Channel.Topic}");
|
await _writer.WriteLineAsync($"Topic: {Context.Request.Channel.Topic}");
|
||||||
|
|
||||||
if (Context.Request.After != null)
|
if (Context.Request.After != null)
|
||||||
await _writer.WriteLineAsync($"After: {Context.FormatDate(Context.Request.After.Value)}");
|
await _writer.WriteLineAsync($"After: {Context.FormatDate(Context.Request.After.Value.ToDate())}");
|
||||||
|
|
||||||
if (Context.Request.Before != null)
|
if (Context.Request.Before != null)
|
||||||
await _writer.WriteLineAsync($"Before: {Context.FormatDate(Context.Request.Before.Value)}");
|
await _writer.WriteLineAsync($"Before: {Context.FormatDate(Context.Request.Before.Value.ToDate())}");
|
||||||
|
|
||||||
await _writer.WriteLineAsync('='.Repeat(62));
|
await _writer.WriteLineAsync('='.Repeat(62));
|
||||||
await _writer.WriteLineAsync();
|
await _writer.WriteLineAsync();
|
||||||
|
|
|
@ -5,12 +5,6 @@ namespace DiscordChatExporter.Domain.Internal.Extensions
|
||||||
{
|
{
|
||||||
internal static class DateExtensions
|
internal static class DateExtensions
|
||||||
{
|
{
|
||||||
public static string ToSnowflake(this DateTimeOffset dateTime)
|
|
||||||
{
|
|
||||||
var value = ((ulong) dateTime.ToUnixTimeMilliseconds() - 1420070400000UL) << 22;
|
|
||||||
return value.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ToLocalString(this DateTimeOffset dateTime, string format) =>
|
public static string ToLocalString(this DateTimeOffset dateTime, string format) =>
|
||||||
dateTime.ToLocalTime().ToString(format, CultureInfo.InvariantCulture);
|
dateTime.ToLocalTime().ToString(format, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,6 @@ namespace DiscordChatExporter.Domain.Internal.Extensions
|
||||||
{
|
{
|
||||||
internal static class GenericExtensions
|
internal static class GenericExtensions
|
||||||
{
|
{
|
||||||
public static TOut Pipe<TIn, TOut>(this TIn input, Func<TIn, TOut> transform) => transform(input);
|
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Domain.Utilities
|
||||||
|
{
|
||||||
|
public static class GeneralExtensions
|
||||||
|
{
|
||||||
|
public static TOut Pipe<TIn, TOut>(this TIn input, Func<TIn, TOut> transform) => transform(input);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using DiscordChatExporter.Domain.Discord;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Domain.Discord.Models;
|
||||||
using DiscordChatExporter.Domain.Exporting;
|
using DiscordChatExporter.Domain.Exporting;
|
||||||
|
using DiscordChatExporter.Domain.Utilities;
|
||||||
using DiscordChatExporter.Gui.Services;
|
using DiscordChatExporter.Gui.Services;
|
||||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||||
|
|
||||||
|
@ -82,8 +84,8 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||||
Guild!,
|
Guild!,
|
||||||
channel,
|
channel,
|
||||||
SelectedFormat,
|
SelectedFormat,
|
||||||
After,
|
After?.Pipe(Snowflake.FromDate),
|
||||||
Before
|
Before?.Pipe(Snowflake.FromDate)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter
|
// Filter
|
||||||
|
|
|
@ -211,8 +211,8 @@ namespace DiscordChatExporter.Gui.ViewModels
|
||||||
channel!,
|
channel!,
|
||||||
dialog.OutputPath!,
|
dialog.OutputPath!,
|
||||||
dialog.SelectedFormat,
|
dialog.SelectedFormat,
|
||||||
dialog.After,
|
dialog.After?.Pipe(Snowflake.FromDate),
|
||||||
dialog.Before,
|
dialog.Before?.Pipe(Snowflake.FromDate),
|
||||||
dialog.PartitionLimit,
|
dialog.PartitionLimit,
|
||||||
dialog.ShouldDownloadMedia,
|
dialog.ShouldDownloadMedia,
|
||||||
_settingsService.ShouldReuseMedia,
|
_settingsService.ShouldReuseMedia,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue