From 2156c6cd0ca90524bd6e52418b034334b66b5c8c Mon Sep 17 00:00:00 2001 From: Alexey Golub <1935960+Tyrrrz@users.noreply.github.com> Date: Mon, 3 Jan 2022 14:52:16 -0800 Subject: [PATCH] Automatically detect token kind (#764) --- .../Fixtures/ExportWrapperFixture.cs | 3 +- .../Infra/Secrets.cs | 19 ---- .../Specs/DateRangeSpecs.cs | 9 +- .../Specs/FilterSpecs.cs | 12 +- .../Specs/PartitioningSpecs.cs | 6 +- .../Specs/SelfContainedSpecs.cs | 3 +- .../Commands/Base/TokenCommandBase.cs | 18 +-- .../Commands/GuideCommand.cs | 2 +- DiscordChatExporter.Core/Discord/AuthToken.cs | 12 -- .../Discord/DiscordClient.cs | 58 ++++++++-- .../{AuthTokenKind.cs => TokenKind.cs} | 3 +- .../Exporting/ChannelExporter.cs | 2 - .../Services/SettingsService.cs | 5 +- .../ViewModels/RootViewModel.cs | 32 +++--- DiscordChatExporter.Gui/Views/RootView.xaml | 106 +++++++----------- 15 files changed, 127 insertions(+), 163 deletions(-) delete mode 100644 DiscordChatExporter.Core/Discord/AuthToken.cs rename DiscordChatExporter.Core/Discord/{AuthTokenKind.cs => TokenKind.cs} (66%) diff --git a/DiscordChatExporter.Cli.Tests/Fixtures/ExportWrapperFixture.cs b/DiscordChatExporter.Cli.Tests/Fixtures/ExportWrapperFixture.cs index 90a842be..e8fbaea6 100644 --- a/DiscordChatExporter.Cli.Tests/Fixtures/ExportWrapperFixture.cs +++ b/DiscordChatExporter.Cli.Tests/Fixtures/ExportWrapperFixture.cs @@ -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 diff --git a/DiscordChatExporter.Cli.Tests/Infra/Secrets.cs b/DiscordChatExporter.Cli.Tests/Infra/Secrets.cs index 6e2bd467..5d3180d2 100644 --- a/DiscordChatExporter.Cli.Tests/Infra/Secrets.cs +++ b/DiscordChatExporter.Cli.Tests/Infra/Secrets.cs @@ -22,24 +22,5 @@ internal static class Secrets throw new InvalidOperationException("Discord token not provided for tests."); }); - private static readonly Lazy 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; } \ No newline at end of file diff --git a/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs index 23af76d8..63b98e00 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/DateRangeSpecs.cs @@ -27,8 +27,7 @@ public record DateRangeSpecs(TempOutputFixture TempOutput) : IClassFixture _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); } \ No newline at end of file diff --git a/DiscordChatExporter.Cli/Commands/GuideCommand.cs b/DiscordChatExporter.Cli/Commands/GuideCommand.cs index ead49b58..a234916a 100644 --- a/DiscordChatExporter.Cli/Commands/GuideCommand.cs +++ b/DiscordChatExporter.Cli/Commands/GuideCommand.cs @@ -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"); diff --git a/DiscordChatExporter.Core/Discord/AuthToken.cs b/DiscordChatExporter.Core/Discord/AuthToken.cs deleted file mode 100644 index 6d10c6b9..00000000 --- a/DiscordChatExporter.Core/Discord/AuthToken.cs +++ /dev/null @@ -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) - }; -} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Discord/DiscordClient.cs b/DiscordChatExporter.Core/Discord/DiscordClient.cs index 9319d83e..0ae41ffd 100644 --- a/DiscordChatExporter.Core/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Core/Discord/DiscordClient.cs @@ -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 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 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); } diff --git a/DiscordChatExporter.Core/Discord/AuthTokenKind.cs b/DiscordChatExporter.Core/Discord/TokenKind.cs similarity index 66% rename from DiscordChatExporter.Core/Discord/AuthTokenKind.cs rename to DiscordChatExporter.Core/Discord/TokenKind.cs index 4aa841ae..f22afcce 100644 --- a/DiscordChatExporter.Core/Discord/AuthTokenKind.cs +++ b/DiscordChatExporter.Core/Discord/TokenKind.cs @@ -1,7 +1,8 @@ namespace DiscordChatExporter.Core.Discord; -public enum AuthTokenKind +public enum TokenKind { + Unknown, User, Bot } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs index 165b6107..e6dcb7df 100644 --- a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs +++ b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs @@ -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? progress = null, diff --git a/DiscordChatExporter.Gui/Services/SettingsService.cs b/DiscordChatExporter.Gui/Services/SettingsService.cs index dda9c095..9ee98862 100644 --- a/DiscordChatExporter.Gui/Services/SettingsService.cs +++ b/DiscordChatExporter.Gui/Services/SettingsService.cs @@ -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; diff --git a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs index d6118365..c9cc78a3 100644 --- a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs @@ -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>? 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; diff --git a/DiscordChatExporter.Gui/Views/RootView.xaml b/DiscordChatExporter.Gui/Views/RootView.xaml index 8688aacc..0083d7b6 100644 --- a/DiscordChatExporter.Gui/Views/RootView.xaml +++ b/DiscordChatExporter.Gui/Views/RootView.xaml @@ -64,39 +64,28 @@ - - + - - - - - - - + Width="24" + Height="24" + Margin="8" + VerticalAlignment="Center" + Foreground="{DynamicResource PrimaryHueMidBrush}" + Kind="Password" /> + Text="{Binding Token, UpdateSourceTrigger=PropertyChanged}" />