mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-07 06:39:33 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c484d1f88 | |||
| c36b8df8e7 | |||
| 0408096746 | |||
| 40d833cd57 | |||
| 0de1f47310 | |||
| e31ff55e33 | |||
| 81525a1076 | |||
| 65ab003ff5 | |||
| 2156c6cd0c | |||
| e97151cd19 | |||
| 271a478a0e | |||
| b40a88c4d3 |
+10
-1
@@ -1,6 +1,15 @@
|
||||
### v2.32 (15-Dec-2021)
|
||||
|
||||
- Token kind (user or bot) is now detected automatically. Removed the button to switch token kind in GUI. Option `-b|--bot` in CLI is now deprecated and does nothing.
|
||||
- Updated user token extraction guide to reflect the fact that devtools are no longer accessible in the desktop version of Discord client. The recommended workaround is to open Discord in browser. (Thanks [@Dhananjay-JSR](https://github.com/Dhananjay-JSR))
|
||||
- Fixed an issue which caused an error when parsing invalid date timestamps. Such timestamps are now rendered as "Invalid date", similarly to how the Discord client does it.
|
||||
- Fixed an issue which caused an error when parsing certain embed fields.
|
||||
- Fixed an issue which caused an error when parsing a mention to a user that has no username.
|
||||
- [GUI] Minor visual fixes.
|
||||
|
||||
### v2.31.1 (15-Dec-2021)
|
||||
|
||||
- Fixed an issue which caused an exception when parsing certain custom emoji reactions.
|
||||
- Fixed an issue which caused an error when parsing certain custom emoji reactions.
|
||||
- [GUI] Improved user interface.
|
||||
|
||||
### v2.31 (06-Dec-2021)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Version>2.31.1</Version>
|
||||
<Version>2.32</Version>
|
||||
<Company>Tyrrrz</Company>
|
||||
<Copyright>Copyright (c) Alexey Golub</Copyright>
|
||||
<Copyright>Copyright (c) Oleksii Holub</Copyright>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="0.16.1" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.2.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.4.0" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
|
||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
|
||||
@@ -36,8 +36,7 @@ public class ExportWrapperFixture : IDisposable
|
||||
{
|
||||
await new ExportChannelsCommand
|
||||
{
|
||||
TokenValue = Secrets.DiscordToken,
|
||||
IsBotToken = Secrets.IsDiscordTokenBot,
|
||||
Token = Secrets.DiscordToken,
|
||||
ChannelIds = new[] { channelId },
|
||||
ExportFormat = format,
|
||||
OutputPath = filePath
|
||||
|
||||
@@ -22,24 +22,5 @@ internal static class Secrets
|
||||
throw new InvalidOperationException("Discord token not provided for tests.");
|
||||
});
|
||||
|
||||
private static readonly Lazy<bool> IsDiscordTokenBotLazy = new(() =>
|
||||
{
|
||||
var fromEnvironment = Environment.GetEnvironmentVariable("DISCORD_TOKEN_BOT");
|
||||
if (!string.IsNullOrWhiteSpace(fromEnvironment))
|
||||
return string.Equals(fromEnvironment, "true", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var secretFilePath = Path.Combine(
|
||||
Path.GetDirectoryName(typeof(Secrets).Assembly.Location) ?? Directory.GetCurrentDirectory(),
|
||||
"DiscordTokenBot.secret"
|
||||
);
|
||||
|
||||
if (File.Exists(secretFilePath))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
public static string DiscordToken => DiscordTokenLazy.Value;
|
||||
|
||||
public static bool IsDiscordTokenBot => IsDiscordTokenBotLazy.Value;
|
||||
}
|
||||
@@ -27,8 +27,7 @@ public record DateRangeSpecs(TempOutputFixture TempOutput) : IClassFixture<TempO
|
||||
// Act
|
||||
await new ExportChannelsCommand
|
||||
{
|
||||
TokenValue = Secrets.DiscordToken,
|
||||
IsBotToken = Secrets.IsDiscordTokenBot,
|
||||
Token = Secrets.DiscordToken,
|
||||
ChannelIds = new[] { ChannelIds.DateRangeTestCases },
|
||||
ExportFormat = ExportFormat.Json,
|
||||
OutputPath = filePath,
|
||||
@@ -74,8 +73,7 @@ public record DateRangeSpecs(TempOutputFixture TempOutput) : IClassFixture<TempO
|
||||
// Act
|
||||
await new ExportChannelsCommand
|
||||
{
|
||||
TokenValue = Secrets.DiscordToken,
|
||||
IsBotToken = Secrets.IsDiscordTokenBot,
|
||||
Token = Secrets.DiscordToken,
|
||||
ChannelIds = new[] { ChannelIds.DateRangeTestCases },
|
||||
ExportFormat = ExportFormat.Json,
|
||||
OutputPath = filePath,
|
||||
@@ -120,8 +118,7 @@ public record DateRangeSpecs(TempOutputFixture TempOutput) : IClassFixture<TempO
|
||||
// Act
|
||||
await new ExportChannelsCommand
|
||||
{
|
||||
TokenValue = Secrets.DiscordToken,
|
||||
IsBotToken = Secrets.IsDiscordTokenBot,
|
||||
Token = Secrets.DiscordToken,
|
||||
ChannelIds = new[] { ChannelIds.DateRangeTestCases },
|
||||
ExportFormat = ExportFormat.Json,
|
||||
OutputPath = filePath,
|
||||
|
||||
@@ -25,8 +25,7 @@ public record FilterSpecs(TempOutputFixture TempOutput) : IClassFixture<TempOutp
|
||||
// Act
|
||||
await new ExportChannelsCommand
|
||||
{
|
||||
TokenValue = Secrets.DiscordToken,
|
||||
IsBotToken = Secrets.IsDiscordTokenBot,
|
||||
Token = Secrets.DiscordToken,
|
||||
ChannelIds = new[] { ChannelIds.FilterTestCases },
|
||||
ExportFormat = ExportFormat.Json,
|
||||
OutputPath = filePath,
|
||||
@@ -54,8 +53,7 @@ public record FilterSpecs(TempOutputFixture TempOutput) : IClassFixture<TempOutp
|
||||
// Act
|
||||
await new ExportChannelsCommand
|
||||
{
|
||||
TokenValue = Secrets.DiscordToken,
|
||||
IsBotToken = Secrets.IsDiscordTokenBot,
|
||||
Token = Secrets.DiscordToken,
|
||||
ChannelIds = new[] { ChannelIds.FilterTestCases },
|
||||
ExportFormat = ExportFormat.Json,
|
||||
OutputPath = filePath,
|
||||
@@ -83,8 +81,7 @@ public record FilterSpecs(TempOutputFixture TempOutput) : IClassFixture<TempOutp
|
||||
// Act
|
||||
await new ExportChannelsCommand
|
||||
{
|
||||
TokenValue = Secrets.DiscordToken,
|
||||
IsBotToken = Secrets.IsDiscordTokenBot,
|
||||
Token = Secrets.DiscordToken,
|
||||
ChannelIds = new[] { ChannelIds.FilterTestCases },
|
||||
ExportFormat = ExportFormat.Json,
|
||||
OutputPath = filePath,
|
||||
@@ -112,8 +109,7 @@ public record FilterSpecs(TempOutputFixture TempOutput) : IClassFixture<TempOutp
|
||||
// Act
|
||||
await new ExportChannelsCommand
|
||||
{
|
||||
TokenValue = Secrets.DiscordToken,
|
||||
IsBotToken = Secrets.IsDiscordTokenBot,
|
||||
Token = Secrets.DiscordToken,
|
||||
ChannelIds = new[] { ChannelIds.FilterTestCases },
|
||||
ExportFormat = ExportFormat.Json,
|
||||
OutputPath = filePath,
|
||||
|
||||
@@ -25,8 +25,7 @@ public record PartitioningSpecs(TempOutputFixture TempOutput) : IClassFixture<Te
|
||||
// Act
|
||||
await new ExportChannelsCommand
|
||||
{
|
||||
TokenValue = Secrets.DiscordToken,
|
||||
IsBotToken = Secrets.IsDiscordTokenBot,
|
||||
Token = Secrets.DiscordToken,
|
||||
ChannelIds = new[] { ChannelIds.DateRangeTestCases },
|
||||
ExportFormat = ExportFormat.HtmlDark,
|
||||
OutputPath = filePath,
|
||||
@@ -50,8 +49,7 @@ public record PartitioningSpecs(TempOutputFixture TempOutput) : IClassFixture<Te
|
||||
// Act
|
||||
await new ExportChannelsCommand
|
||||
{
|
||||
TokenValue = Secrets.DiscordToken,
|
||||
IsBotToken = Secrets.IsDiscordTokenBot,
|
||||
Token = Secrets.DiscordToken,
|
||||
ChannelIds = new[] { ChannelIds.DateRangeTestCases },
|
||||
ExportFormat = ExportFormat.HtmlDark,
|
||||
OutputPath = filePath,
|
||||
|
||||
@@ -25,8 +25,7 @@ public record SelfContainedSpecs(TempOutputFixture TempOutput) : IClassFixture<T
|
||||
// Act
|
||||
await new ExportChannelsCommand
|
||||
{
|
||||
TokenValue = Secrets.DiscordToken,
|
||||
IsBotToken = Secrets.IsDiscordTokenBot,
|
||||
Token = Secrets.DiscordToken,
|
||||
ChannelIds = new[] { ChannelIds.SelfContainedTestCases },
|
||||
ExportFormat = ExportFormat.HtmlDark,
|
||||
OutputPath = filePath,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
@@ -9,21 +10,14 @@ namespace DiscordChatExporter.Cli.Commands.Base;
|
||||
public abstract class TokenCommandBase : ICommand
|
||||
{
|
||||
[CommandOption("token", 't', IsRequired = true, EnvironmentVariable = "DISCORD_TOKEN", Description = "Authentication token.")]
|
||||
public string TokenValue { get; init; } = "";
|
||||
public string Token { get; init; } = "";
|
||||
|
||||
[CommandOption("bot", 'b', EnvironmentVariable = "DISCORD_TOKEN_BOT", Description = "Authenticate as a bot.")]
|
||||
[CommandOption("bot", 'b', EnvironmentVariable = "DISCORD_TOKEN_BOT", Description = "This option doesn't do anything. Kept for backwards compatibility.")]
|
||||
[Obsolete("This option doesn't do anything. Kept for backwards compatibility.")]
|
||||
public bool IsBotToken { get; init; }
|
||||
|
||||
private AuthToken? _authToken;
|
||||
private AuthToken AuthToken => _authToken ??= new AuthToken(
|
||||
IsBotToken
|
||||
? AuthTokenKind.Bot
|
||||
: AuthTokenKind.User,
|
||||
TokenValue
|
||||
);
|
||||
|
||||
private DiscordClient? _discordClient;
|
||||
protected DiscordClient Discord => _discordClient ??= new DiscordClient(AuthToken);
|
||||
protected DiscordClient Discord => _discordClient ??= new DiscordClient(Token);
|
||||
|
||||
public abstract ValueTask ExecuteAsync(IConsole console);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public class GuideCommand : ICommand
|
||||
using (console.WithForegroundColor(ConsoleColor.White))
|
||||
console.Output.WriteLine("To get user token:");
|
||||
|
||||
console.Output.WriteLine(" 1. Open Discord");
|
||||
console.Output.WriteLine(" 1. Open Discord in your web browser and login");
|
||||
console.Output.WriteLine(" 2. Press Ctrl+Shift+I to show developer tools");
|
||||
console.Output.WriteLine(" 3. Press Ctrl+Shift+M to toggle device toolbar");
|
||||
console.Output.WriteLine(" 4. Navigate to the Application tab");
|
||||
@@ -61,7 +61,7 @@ public class GuideCommand : ICommand
|
||||
|
||||
// Wiki link
|
||||
using (console.WithForegroundColor(ConsoleColor.White))
|
||||
console.Output.WriteLine("For more information, check out the wiki:");
|
||||
console.Output.WriteLine("If you have questions or issues, please refer to the wiki:");
|
||||
using (console.WithForegroundColor(ConsoleColor.DarkCyan))
|
||||
console.Output.WriteLine("https://github.com/Tyrrrz/DiscordChatExporter/wiki");
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliFx" Version="2.0.6" />
|
||||
<PackageReference Include="CliFx" Version="2.2.1" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.43.0" />
|
||||
<PackageReference Include="Gress" Version="1.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord;
|
||||
|
||||
public record AuthToken(AuthTokenKind Kind, string Value)
|
||||
{
|
||||
public AuthenticationHeaderValue GetAuthenticationHeader() => Kind switch
|
||||
{
|
||||
AuthTokenKind.Bot => new AuthenticationHeaderValue("Bot", Value),
|
||||
_ => new AuthenticationHeaderValue(Value)
|
||||
};
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public partial record Attachment
|
||||
var url = json.GetProperty("url").GetNonWhiteSpaceString();
|
||||
var width = json.GetPropertyOrNull("width")?.GetInt32OrNull();
|
||||
var height = json.GetPropertyOrNull("height")?.GetInt32OrNull();
|
||||
var fileName = json.GetProperty("filename").GetNonWhiteSpaceString();
|
||||
var fileName = json.GetProperty("filename").GetNonNullString();
|
||||
var fileSize = json.GetProperty("size").GetInt64().Pipe(FileSize.FromBytes);
|
||||
|
||||
return new Attachment(id, url, fileName, width, height, fileSize);
|
||||
|
||||
@@ -11,8 +11,8 @@ public record EmbedField(
|
||||
{
|
||||
public static EmbedField Parse(JsonElement json)
|
||||
{
|
||||
var name = json.GetProperty("name").GetNonWhiteSpaceString();
|
||||
var value = json.GetProperty("value").GetNonWhiteSpaceString();
|
||||
var name = json.GetProperty("name").GetNonNullString();
|
||||
var value = json.GetProperty("value").GetNonNullString();
|
||||
var isInline = json.GetPropertyOrNull("inline")?.GetBooleanOrNull() ?? false;
|
||||
|
||||
return new EmbedField(name, value, isInline);
|
||||
|
||||
@@ -23,7 +23,7 @@ public record Guild(Snowflake Id, string Name, string IconUrl) : IHasId
|
||||
public static Guild Parse(JsonElement json)
|
||||
{
|
||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var name = json.GetProperty("name").GetNonWhiteSpaceString();
|
||||
var name = json.GetProperty("name").GetNonNullString();
|
||||
var iconHash = json.GetPropertyOrNull("icon")?.GetNonWhiteSpaceStringOrNull();
|
||||
|
||||
var iconUrl = !string.IsNullOrWhiteSpace(iconHash)
|
||||
|
||||
@@ -12,7 +12,7 @@ public record Role(Snowflake Id, string Name, int Position, Color? Color) : IHas
|
||||
public static Role Parse(JsonElement json)
|
||||
{
|
||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var name = json.GetProperty("name").GetNonWhiteSpaceString();
|
||||
var name = json.GetProperty("name").GetNonNullString();
|
||||
var position = json.GetProperty("position").GetInt32();
|
||||
|
||||
var color = json
|
||||
|
||||
@@ -38,7 +38,7 @@ public partial record User
|
||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var isBot = json.GetPropertyOrNull("bot")?.GetBooleanOrNull() ?? false;
|
||||
var discriminator = json.GetProperty("discriminator").GetNonWhiteSpaceString().Pipe(int.Parse);
|
||||
var name = json.GetProperty("username").GetNonWhiteSpaceString();
|
||||
var name = json.GetProperty("username").GetNonNullString();
|
||||
var avatarHash = json.GetPropertyOrNull("avatar")?.GetNonWhiteSpaceStringOrNull();
|
||||
|
||||
var avatarUrl = !string.IsNullOrWhiteSpace(avatarHash)
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@@ -18,10 +19,30 @@ namespace DiscordChatExporter.Core.Discord;
|
||||
|
||||
public class DiscordClient
|
||||
{
|
||||
private readonly AuthToken _token;
|
||||
private readonly string _token;
|
||||
private readonly Uri _baseUri = new("https://discord.com/api/v8/", UriKind.Absolute);
|
||||
|
||||
public DiscordClient(AuthToken token) => _token = token;
|
||||
private TokenKind _tokenKind = TokenKind.Unknown;
|
||||
|
||||
public DiscordClient(string token) => _token = token;
|
||||
|
||||
private async ValueTask<HttpResponseMessage> GetResponseAsync(
|
||||
string url,
|
||||
bool isBot,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
|
||||
|
||||
request.Headers.Authorization = isBot
|
||||
? new AuthenticationHeaderValue("Bot", _token)
|
||||
: new AuthenticationHeaderValue(_token);
|
||||
|
||||
return await Http.Client.SendAsync(
|
||||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
private async ValueTask<HttpResponseMessage> GetResponseAsync(
|
||||
string url,
|
||||
@@ -29,14 +50,33 @@ public class DiscordClient
|
||||
{
|
||||
return await Http.ResponsePolicy.ExecuteAsync(async innerCancellationToken =>
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
|
||||
request.Headers.Authorization = _token.GetAuthenticationHeader();
|
||||
if (_tokenKind == TokenKind.User)
|
||||
return await GetResponseAsync(url, false, innerCancellationToken);
|
||||
|
||||
return await Http.Client.SendAsync(
|
||||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
innerCancellationToken
|
||||
);
|
||||
if (_tokenKind == TokenKind.Bot)
|
||||
return await GetResponseAsync(url, true, innerCancellationToken);
|
||||
|
||||
// Try to authenticate as user
|
||||
var userResponse = await GetResponseAsync(url, false, innerCancellationToken);
|
||||
if (userResponse.StatusCode != HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_tokenKind = TokenKind.User;
|
||||
return userResponse;
|
||||
}
|
||||
|
||||
userResponse.Dispose();
|
||||
|
||||
// Otherwise, try to authenticate as bot
|
||||
var botResponse = await GetResponseAsync(url, true, innerCancellationToken);
|
||||
if (botResponse.StatusCode != HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_tokenKind = TokenKind.Bot;
|
||||
return botResponse;
|
||||
}
|
||||
|
||||
// The token is probably invalid altogether.
|
||||
// Return the last response anyway, upstream should handle the error.
|
||||
return botResponse;
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
namespace DiscordChatExporter.Core.Discord;
|
||||
|
||||
public enum AuthTokenKind
|
||||
public enum TokenKind
|
||||
{
|
||||
Unknown,
|
||||
User,
|
||||
Bot
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||
<PackageReference Include="MiniRazor.CodeGen" Version="2.2.0" />
|
||||
<PackageReference Include="Polly" Version="7.2.2" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
<PackageReference Include="Superpower" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ public class ChannelExporter
|
||||
|
||||
public ChannelExporter(DiscordClient discord) => _discord = discord;
|
||||
|
||||
public ChannelExporter(AuthToken token) : this(new DiscordClient(token)) {}
|
||||
|
||||
public async ValueTask ExportChannelAsync(
|
||||
ExportRequest request,
|
||||
IProgress<double>? progress = null,
|
||||
|
||||
@@ -159,12 +159,18 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||
|
||||
protected override MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp)
|
||||
{
|
||||
var dateString = timestamp.Date is not null
|
||||
? _context.FormatDate(timestamp.Date.Value)
|
||||
: "Invalid date";
|
||||
|
||||
// Timestamp tooltips always use full date regardless of the configured format
|
||||
var longDateString = timestamp.Value.ToLocalString("dddd, MMMM d, yyyy h:mm tt");
|
||||
var longDateString = timestamp.Date is not null
|
||||
? timestamp.Date.Value.ToLocalString("dddd, MMMM d, yyyy h:mm tt")
|
||||
: "Invalid date";
|
||||
|
||||
_buffer
|
||||
.Append($"<span class=\"timestamp\" title=\"{HtmlEncode(longDateString)}\">")
|
||||
.Append(HtmlEncode(_context.FormatDate(timestamp.Value)))
|
||||
.Append(HtmlEncode(dateString))
|
||||
.Append("</span>");
|
||||
|
||||
return base.VisitUnixTimestamp(timestamp);
|
||||
|
||||
+3
-1
@@ -73,7 +73,9 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
||||
protected override MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp)
|
||||
{
|
||||
_buffer.Append(
|
||||
_context.FormatDate(timestamp.Value)
|
||||
timestamp.Date is not null
|
||||
? _context.FormatDate(timestamp.Date.Value)
|
||||
: "Invalid date"
|
||||
);
|
||||
|
||||
return base.VisitUnixTimestamp(timestamp);
|
||||
|
||||
@@ -235,7 +235,7 @@ internal static partial class MarkdownParser
|
||||
|
||||
// Capture <t:12345678> or <t:12345678:R>
|
||||
private static readonly IMatcher<MarkdownNode> UnixTimestampNodeMatcher = new RegexMatcher<MarkdownNode>(
|
||||
new Regex("<t:(\\d+)(?::\\w)?>", DefaultRegexOptions),
|
||||
new Regex("<t:(-?\\d+)(?::\\w)?>", DefaultRegexOptions),
|
||||
(_, m) =>
|
||||
{
|
||||
// TODO: support formatting parameters
|
||||
@@ -244,17 +244,19 @@ internal static partial class MarkdownParser
|
||||
if (!long.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture,
|
||||
out var offset))
|
||||
{
|
||||
return null;
|
||||
return new UnixTimestampNode(null);
|
||||
}
|
||||
|
||||
// Bound check
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/681
|
||||
if (offset < TimeSpan.MinValue.TotalSeconds || offset > TimeSpan.MaxValue.TotalSeconds)
|
||||
try
|
||||
{
|
||||
return null;
|
||||
return new UnixTimestampNode(DateTimeOffset.UnixEpoch + TimeSpan.FromSeconds(offset));
|
||||
}
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/681
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/766
|
||||
catch (Exception ex) when (ex is ArgumentOutOfRangeException or OverflowException)
|
||||
{
|
||||
return new UnixTimestampNode(null);
|
||||
}
|
||||
|
||||
return new UnixTimestampNode(DateTimeOffset.UnixEpoch + TimeSpan.FromSeconds(offset));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
namespace DiscordChatExporter.Core.Markdown;
|
||||
|
||||
internal record UnixTimestampNode(DateTimeOffset Value) : MarkdownNode;
|
||||
// Null means invalid date
|
||||
internal record UnixTimestampNode(DateTimeOffset? Date) : MarkdownNode;
|
||||
@@ -17,12 +17,12 @@
|
||||
<PackageReference Include="MaterialDesignColors" Version="2.0.4" />
|
||||
<PackageReference Include="MaterialDesignThemes" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.0" />
|
||||
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
|
||||
<PackageReference Include="Onova" Version="2.6.2" />
|
||||
<PackageReference Include="Stylet" Version="1.3.6" />
|
||||
<PackageReference Include="Tyrrrz.Settings" Version="1.3.4" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" PrivateAssets="all" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.0.2" PrivateAssets="all" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.0.3" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Exporting;
|
||||
using DiscordChatExporter.Core.Exporting;
|
||||
using Tyrrrz.Settings;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Services;
|
||||
@@ -18,7 +17,7 @@ public class SettingsService : SettingsManager
|
||||
|
||||
public bool ShouldReuseMedia { get; set; }
|
||||
|
||||
public AuthToken? LastToken { get; set; }
|
||||
public string? LastToken { get; set; }
|
||||
|
||||
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ public class RootViewModel : Screen
|
||||
private readonly SettingsService _settingsService;
|
||||
private readonly UpdateService _updateService;
|
||||
|
||||
private DiscordClient? _discord;
|
||||
|
||||
public ISnackbarMessageQueue Notifications { get; } = new SnackbarMessageQueue(TimeSpan.FromSeconds(5));
|
||||
|
||||
public IProgressManager ProgressManager { get; } = new ProgressManager();
|
||||
@@ -33,9 +35,7 @@ public class RootViewModel : Screen
|
||||
|
||||
public bool IsProgressIndeterminate { get; private set; }
|
||||
|
||||
public bool IsBotToken { get; set; }
|
||||
|
||||
public string? TokenValue { get; set; }
|
||||
public string? Token { get; set; }
|
||||
|
||||
private IReadOnlyDictionary<Guild, IReadOnlyList<Channel>>? GuildChannelMap { get; set; }
|
||||
|
||||
@@ -111,8 +111,7 @@ public class RootViewModel : Screen
|
||||
|
||||
if (_settingsService.LastToken is not null)
|
||||
{
|
||||
IsBotToken = _settingsService.LastToken.Kind == AuthTokenKind.Bot;
|
||||
TokenValue = _settingsService.LastToken.Value;
|
||||
Token = _settingsService.LastToken;
|
||||
}
|
||||
|
||||
if (_settingsService.IsDarkModeEnabled)
|
||||
@@ -144,7 +143,7 @@ public class RootViewModel : Screen
|
||||
public void ShowHelp() => ProcessEx.StartShellExecute(App.GitHubProjectWikiUrl);
|
||||
|
||||
public bool CanPopulateGuildsAndChannels =>
|
||||
!IsBusy && !string.IsNullOrWhiteSpace(TokenValue);
|
||||
!IsBusy && !string.IsNullOrWhiteSpace(Token);
|
||||
|
||||
public async void PopulateGuildsAndChannels()
|
||||
{
|
||||
@@ -152,15 +151,10 @@ public class RootViewModel : Screen
|
||||
|
||||
try
|
||||
{
|
||||
var tokenValue = TokenValue?.Trim('"', ' ');
|
||||
if (string.IsNullOrWhiteSpace(tokenValue))
|
||||
var token = Token?.Trim('"', ' ');
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
return;
|
||||
|
||||
var token = new AuthToken(
|
||||
IsBotToken ? AuthTokenKind.Bot : AuthTokenKind.User,
|
||||
tokenValue
|
||||
);
|
||||
|
||||
_settingsService.LastToken = token;
|
||||
|
||||
var discord = new DiscordClient(token);
|
||||
@@ -172,6 +166,7 @@ public class RootViewModel : Screen
|
||||
guildChannelMap[guild] = channels.Where(c => c.IsTextChannel).ToArray();
|
||||
}
|
||||
|
||||
_discord = discord;
|
||||
GuildChannelMap = guildChannelMap;
|
||||
SelectedGuild = guildChannelMap.Keys.FirstOrDefault();
|
||||
}
|
||||
@@ -191,21 +186,24 @@ public class RootViewModel : Screen
|
||||
}
|
||||
|
||||
public bool CanExportChannels =>
|
||||
!IsBusy && SelectedGuild is not null && SelectedChannels is not null && SelectedChannels.Any();
|
||||
!IsBusy &&
|
||||
_discord is not null &&
|
||||
SelectedGuild is not null &&
|
||||
SelectedChannels is not null &&
|
||||
SelectedChannels.Any();
|
||||
|
||||
public async void ExportChannels()
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = _settingsService.LastToken;
|
||||
if (token is null || SelectedGuild is null || SelectedChannels is null || !SelectedChannels.Any())
|
||||
if (_discord is null || SelectedGuild is null || SelectedChannels is null || !SelectedChannels.Any())
|
||||
return;
|
||||
|
||||
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels);
|
||||
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
||||
return;
|
||||
|
||||
var exporter = new ChannelExporter(token);
|
||||
var exporter = new ChannelExporter(_discord);
|
||||
|
||||
var operations = ProgressManager.CreateOperations(dialog.Channels!.Count);
|
||||
var successfulExportCount = 0;
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
BorderThickness="0,1">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock
|
||||
Margin="16,0,16,16"
|
||||
Margin="16,8"
|
||||
Text="{Binding Message}"
|
||||
TextWrapping="Wrap" />
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -24,126 +24,129 @@
|
||||
FontWeight="Light"
|
||||
Text="Settings" />
|
||||
|
||||
<ScrollViewer
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- Auto-updates -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Perform automatic updates on every launch">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Auto-updates" />
|
||||
<ToggleButton
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding IsAutoUpdateEnabled}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Dark mode -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Use darker colors in the UI">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Dark mode" />
|
||||
<ToggleButton
|
||||
x:Name="DarkModeToggleButton"
|
||||
VerticalAlignment="Center"
|
||||
Checked="DarkModeToggleButton_Checked"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding IsDarkModeEnabled}"
|
||||
Unchecked="DarkModeToggleButton_Unchecked" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Persist token -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Save last used token in a file so that it can be persisted between sessions">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Save token" />
|
||||
<ToggleButton
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding IsTokenPersisted}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Reuse media -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Reuse already existing media content to skip redundant downloads">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Reuse downloaded media" />
|
||||
<ToggleButton
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding ShouldReuseMedia}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Date format -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Format used when writing dates (uses .NET date formatting rules)">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Date format" />
|
||||
<TextBox
|
||||
Width="150"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
Text="{Binding DateFormat}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Parallel limit -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="How many channels can be exported at the same time">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Parallel limit"
|
||||
TextAlignment="Right" />
|
||||
<StackPanel
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
Orientation="Horizontal">
|
||||
Padding="0,8"
|
||||
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
||||
BorderThickness="0,1">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<!-- Auto-updates -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Perform automatic updates on every launch">
|
||||
<TextBlock
|
||||
Margin="10,0"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding ParallelLimit}" />
|
||||
<Slider
|
||||
DockPanel.Dock="Left"
|
||||
Text="Auto-updates" />
|
||||
<ToggleButton
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding IsAutoUpdateEnabled}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Dark mode -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Use darker colors in the UI">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Dark mode" />
|
||||
<ToggleButton
|
||||
x:Name="DarkModeToggleButton"
|
||||
VerticalAlignment="Center"
|
||||
Checked="DarkModeToggleButton_Checked"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding IsDarkModeEnabled}"
|
||||
Unchecked="DarkModeToggleButton_Unchecked" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Persist token -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Save last used token in a file so that it can be persisted between sessions">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Save token" />
|
||||
<ToggleButton
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding IsTokenPersisted}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Reuse media -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Reuse already existing media content to skip redundant downloads">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Reuse downloaded media" />
|
||||
<ToggleButton
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
IsChecked="{Binding ShouldReuseMedia}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Date format -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="Format used when writing dates (uses .NET date formatting rules)">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Date format" />
|
||||
<TextBox
|
||||
Width="150"
|
||||
VerticalAlignment="Center"
|
||||
Maximum="10"
|
||||
Minimum="1"
|
||||
Style="{DynamicResource MaterialDesignThinSlider}"
|
||||
Value="{Binding ParallelLimit}" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
DockPanel.Dock="Right"
|
||||
Text="{Binding DateFormat}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Parallel limit -->
|
||||
<DockPanel
|
||||
Margin="16,8"
|
||||
Background="Transparent"
|
||||
LastChildFill="False"
|
||||
ToolTip="How many channels can be exported at the same time">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
Text="Parallel limit"
|
||||
TextAlignment="Right" />
|
||||
<StackPanel
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="10,0"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding ParallelLimit}" />
|
||||
<Slider
|
||||
Width="150"
|
||||
VerticalAlignment="Center"
|
||||
Maximum="10"
|
||||
Minimum="1"
|
||||
Style="{DynamicResource MaterialDesignThinSlider}"
|
||||
Value="{Binding ParallelLimit}" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Save button -->
|
||||
<Button
|
||||
|
||||
@@ -64,39 +64,28 @@
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Token type -->
|
||||
<ToggleButton
|
||||
<!-- Token icon -->
|
||||
<materialDesign:PackIcon
|
||||
Grid.Column="0"
|
||||
Margin="6"
|
||||
IsChecked="{Binding IsBotToken}"
|
||||
Style="{DynamicResource MaterialDesignFlatActionToggleButton}"
|
||||
ToolTip="Switch between user token and bot token">
|
||||
<ToggleButton.Content>
|
||||
<materialDesign:PackIcon
|
||||
Width="24"
|
||||
Height="24"
|
||||
Kind="Account" />
|
||||
</ToggleButton.Content>
|
||||
<materialDesign:ToggleButtonAssist.OnContent>
|
||||
<materialDesign:PackIcon
|
||||
Width="24"
|
||||
Height="24"
|
||||
Kind="Robot" />
|
||||
</materialDesign:ToggleButtonAssist.OnContent>
|
||||
</ToggleButton>
|
||||
Width="24"
|
||||
Height="24"
|
||||
Margin="8"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Password" />
|
||||
|
||||
<!-- Token value -->
|
||||
<TextBox
|
||||
x:Name="TokenValueTextBox"
|
||||
Grid.Column="1"
|
||||
Margin="2,6,6,7"
|
||||
Margin="0,6,6,8"
|
||||
VerticalAlignment="Bottom"
|
||||
materialDesign:HintAssist.Hint="Token"
|
||||
materialDesign:TextFieldAssist.DecorationVisibility="Hidden"
|
||||
materialDesign:TextFieldAssist.TextBoxViewMargin="0,0,2,0"
|
||||
BorderThickness="0"
|
||||
FontSize="16"
|
||||
Text="{Binding TokenValue, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Text="{Binding Token, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<!-- Pull data button -->
|
||||
<Button
|
||||
@@ -152,12 +141,25 @@
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
<!-- Placeholder / usage instructions -->
|
||||
<Grid Margin="32,32,8,8" Visibility="{Binding AvailableGuilds, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}">
|
||||
<!-- For user token -->
|
||||
<StackPanel Visibility="{Binding IsBotToken, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}">
|
||||
<TextBlock FontSize="18" Text="Please provide your user token to authorize" />
|
||||
<TextBlock Margin="8,8,0,0" FontSize="14">
|
||||
<Run Text="1. Open Discord" />
|
||||
<Grid Visibility="{Binding AvailableGuilds, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock Margin="32,16" FontSize="14">
|
||||
<Run FontSize="18" Text="Please provide authentication token to continue" />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
|
||||
<!-- User token -->
|
||||
<InlineUIContainer>
|
||||
<materialDesign:PackIcon
|
||||
Margin="1,0,0,-2"
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Account" />
|
||||
</InlineUIContainer>
|
||||
<Run FontSize="16" Text="Authenticate using your personal account" />
|
||||
<LineBreak />
|
||||
<Run Text="1. Open Discord in your" />
|
||||
<Run FontWeight="SemiBold" Text="web browser" />
|
||||
<Run Text="and login" />
|
||||
<LineBreak />
|
||||
<Run Text="2. Press" />
|
||||
<Run FontWeight="SemiBold" Text="Ctrl+Shift+I" />
|
||||
@@ -189,35 +191,20 @@
|
||||
<Run Text="8. Copy the value of the" />
|
||||
<Run FontWeight="SemiBold" Text="token" />
|
||||
<Run Text="key" />
|
||||
</TextBlock>
|
||||
<TextBlock Margin="0,24,0,0" FontSize="14">
|
||||
<Run Text="Automating user accounts is technically against TOS, use at your own risk." />
|
||||
<LineBreak />
|
||||
<Run Text="To authorize using bot token instead, click" />
|
||||
<Run Text="* Automating user accounts is technically against TOS, use at your own risk!" />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
|
||||
<!-- Bot token -->
|
||||
<InlineUIContainer>
|
||||
<materialDesign:PackIcon
|
||||
Margin="1,0,0,-3"
|
||||
Margin="1,0,0,-2"
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Account" />
|
||||
Kind="Robot" />
|
||||
</InlineUIContainer>
|
||||
<Run Text="in the text box above." />
|
||||
</TextBlock>
|
||||
<TextBlock Margin="0,24,0,0" FontSize="14">
|
||||
<Run Text="For more information, check out the" />
|
||||
<Hyperlink Command="{s:Action ShowHelp}">wiki</Hyperlink><Run Text="." />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<!-- For bot token -->
|
||||
<StackPanel Visibility="{Binding IsBotToken, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
<TextBlock
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Text="Please provide your bot token to authorize" />
|
||||
<TextBlock
|
||||
Margin="8,8,0,0"
|
||||
FontSize="14"
|
||||
FontWeight="Light">
|
||||
<Run FontSize="16" Text="Authenticate as a bot" />
|
||||
<LineBreak />
|
||||
<Run Text="1. Open Discord developer portal" />
|
||||
<LineBreak />
|
||||
<Run Text="2. Open your application's settings" />
|
||||
@@ -230,22 +217,13 @@
|
||||
<Run FontWeight="SemiBold" Text="Token" />
|
||||
<Run Text="click" />
|
||||
<Run FontWeight="SemiBold" Text="Copy" />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
|
||||
<Run FontSize="16" Text="If you have questions or issues, please refer to the" />
|
||||
<Hyperlink Command="{s:Action ShowHelp}" FontSize="16">wiki</Hyperlink><Run FontSize="16" Text="." />
|
||||
</TextBlock>
|
||||
<TextBlock Margin="0,24,0,0" FontSize="14">
|
||||
<Run Text="To authorize using user token instead, click" />
|
||||
<InlineUIContainer>
|
||||
<materialDesign:PackIcon
|
||||
Margin="1,0,0,-1"
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Robot" />
|
||||
</InlineUIContainer>
|
||||
<Run Text="in the text box above." />
|
||||
</TextBlock>
|
||||
<TextBlock Margin="0,24,0,0" FontSize="14">
|
||||
<Run Text="For more information, check out the" />
|
||||
<Hyperlink Command="{s:Action ShowHelp}">wiki</Hyperlink><Run Text="." />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- Guilds and channels -->
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
DiscordChatExporter
|
||||
Copyright (C) 2017-2021 Alexey Golub
|
||||
Copyright (C) 2017-2022 Oleksii Holub
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
||||
Reference in New Issue
Block a user