mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-02-02 08:09:16 +00:00
Automatically detect token kind (#764)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
namespace DiscordChatExporter.Core.Discord;
|
||||
|
||||
public enum AuthTokenKind
|
||||
public enum TokenKind
|
||||
{
|
||||
Unknown,
|
||||
User,
|
||||
Bot
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,11 +141,22 @@
|
||||
</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">
|
||||
<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" />
|
||||
<LineBreak />
|
||||
<Run Text="2. Press" />
|
||||
@@ -189,35 +189,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 +215,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 -->
|
||||
|
||||
Reference in New Issue
Block a user