From 3572a21aad171198adc63fb66939427649848225 Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Tue, 24 Jul 2018 22:47:45 +0300 Subject: [PATCH] Allow using bot token instead of user token (#70) --- DiscordChatExporter.Cli/CliOptions.cs | 4 +- DiscordChatExporter.Cli/Program.cs | 30 +++-- .../ViewModels/IMainViewModel.cs | 2 +- .../ViewModels/MainViewModel.cs | 2 +- DiscordChatExporter.Core/Models/AuthToken.cs | 15 +++ .../Models/AuthTokenType.cs | 8 ++ .../Services/DataService.cs | 72 ++++++---- .../Services/IDataService.cs | 16 +-- .../Services/ISettingsService.cs | 2 +- .../Services/SettingsService.cs | 2 +- DiscordChatExporter.Gui/App.xaml | 15 +++ .../ViewModels/ExportSetupViewModel.cs | 6 +- .../ViewModels/IMainViewModel.cs | 3 +- .../ViewModels/MainViewModel.cs | 31 +++-- DiscordChatExporter.Gui/Views/MainWindow.xaml | 127 ++++++++++++------ 15 files changed, 234 insertions(+), 101 deletions(-) create mode 100644 DiscordChatExporter.Core/Models/AuthToken.cs create mode 100644 DiscordChatExporter.Core/Models/AuthTokenType.cs diff --git a/DiscordChatExporter.Cli/CliOptions.cs b/DiscordChatExporter.Cli/CliOptions.cs index 0a58b1d..665803a 100644 --- a/DiscordChatExporter.Cli/CliOptions.cs +++ b/DiscordChatExporter.Cli/CliOptions.cs @@ -5,7 +5,9 @@ namespace DiscordChatExporter.Cli { public class CliOptions { - public string Token { get; set; } + public string TokenValue { get; set; } + + public bool IsBotToken { get; set; } public string ChannelId { get; set; } diff --git a/DiscordChatExporter.Cli/Program.cs b/DiscordChatExporter.Cli/Program.cs index ef19767..6b1a5ae 100644 --- a/DiscordChatExporter.Cli/Program.cs +++ b/DiscordChatExporter.Cli/Program.cs @@ -18,6 +18,7 @@ namespace DiscordChatExporter.Cli Console.WriteLine($"=== Discord Chat Exporter (Command Line Interface) v{version} ==="); Console.WriteLine(); Console.WriteLine("[-t] [--token] Discord authorization token."); + Console.WriteLine("[-b] [--bot] Whether this is a bot token."); Console.WriteLine("[-c] [--channel] Discord channel ID."); Console.WriteLine("[-f] [--format] Export format. Optional."); Console.WriteLine("[-o] [--output] Output file path. Optional."); @@ -28,20 +29,27 @@ namespace DiscordChatExporter.Cli Console.WriteLine(); Console.WriteLine($"Available export formats: {availableFormats.JoinToString(", ")}"); Console.WriteLine(); - Console.WriteLine("# To get authorization token:"); + Console.WriteLine("# To get user token:"); Console.WriteLine(" - Open Discord app"); Console.WriteLine(" - Log in if you haven't"); - Console.WriteLine(" - Press Ctrl+Shift+I"); - Console.WriteLine(" - Navigate to Application tab"); + Console.WriteLine(" - Press Ctrl+Shift+I to show developer tools"); + Console.WriteLine(" - Navigate to the Application tab"); Console.WriteLine(" - Expand Storage > Local Storage > https://discordapp.com"); - Console.WriteLine(" - Find \"token\" under key and copy the value"); + Console.WriteLine(" - Find the \"token\" key and copy its value"); + Console.WriteLine(); + Console.WriteLine("# To get bot token:"); + Console.WriteLine(" - Go to Discord developer portal"); + Console.WriteLine(" - Log in if you haven't"); + Console.WriteLine(" - Open your application's settings"); + Console.WriteLine(" - Navigate to the Bot section on the left"); + Console.WriteLine(" - Under Token click Copy"); Console.WriteLine(); Console.WriteLine("# To get channel ID:"); Console.WriteLine(" - Open Discord app"); Console.WriteLine(" - Log in if you haven't"); Console.WriteLine(" - Go to any channel you want to export"); - Console.WriteLine(" - Press Ctrl+Shift+I"); - Console.WriteLine(" - Navigate to Console tab"); + Console.WriteLine(" - Press Ctrl+Shift+I to show developer tools"); + Console.WriteLine(" - Navigate to the Console tab"); Console.WriteLine(" - Type \"document.URL\" and press Enter"); Console.WriteLine(" - Copy the long sequence of numbers after last slash"); } @@ -52,7 +60,8 @@ namespace DiscordChatExporter.Cli var settings = Container.SettingsService; - argsParser.Setup(o => o.Token).As('t', "token").Required(); + argsParser.Setup(o => o.TokenValue).As('t', "token").Required(); + argsParser.Setup(o => o.IsBotToken).As('b', "bot").SetDefault(false); argsParser.Setup(o => o.ChannelId).As('c', "channel").Required(); argsParser.Setup(o => o.ExportFormat).As('f', "format").SetDefault(ExportFormat.HtmlDark); argsParser.Setup(o => o.FilePath).As('o', "output").SetDefault(null); @@ -92,10 +101,15 @@ namespace DiscordChatExporter.Cli settings.DateFormat = options.DateFormat; settings.MessageGroupLimit = options.MessageGroupLimit; + // Create token + var token = new AuthToken( + options.IsBotToken ? AuthTokenType.Bot : AuthTokenType.User, + options.TokenValue); + // Export var vm = Container.MainViewModel; vm.ExportAsync( - options.Token, + token, options.ChannelId, options.FilePath, options.ExportFormat, diff --git a/DiscordChatExporter.Cli/ViewModels/IMainViewModel.cs b/DiscordChatExporter.Cli/ViewModels/IMainViewModel.cs index 55b26d1..63cac0c 100644 --- a/DiscordChatExporter.Cli/ViewModels/IMainViewModel.cs +++ b/DiscordChatExporter.Cli/ViewModels/IMainViewModel.cs @@ -6,7 +6,7 @@ namespace DiscordChatExporter.Cli.ViewModels { public interface IMainViewModel { - Task ExportAsync(string token, string channelId, string filePath, ExportFormat format, DateTime? from, + Task ExportAsync(AuthToken token, string channelId, string filePath, ExportFormat format, DateTime? from, DateTime? to); } } \ No newline at end of file diff --git a/DiscordChatExporter.Cli/ViewModels/MainViewModel.cs b/DiscordChatExporter.Cli/ViewModels/MainViewModel.cs index ef9db3d..f532fb6 100644 --- a/DiscordChatExporter.Cli/ViewModels/MainViewModel.cs +++ b/DiscordChatExporter.Cli/ViewModels/MainViewModel.cs @@ -21,7 +21,7 @@ namespace DiscordChatExporter.Cli.ViewModels _exportService = exportService; } - public async Task ExportAsync(string token, string channelId, string filePath, ExportFormat format, + public async Task ExportAsync(AuthToken token, string channelId, string filePath, ExportFormat format, DateTime? from, DateTime? to) { // Get channel and guild diff --git a/DiscordChatExporter.Core/Models/AuthToken.cs b/DiscordChatExporter.Core/Models/AuthToken.cs new file mode 100644 index 0000000..911d5a9 --- /dev/null +++ b/DiscordChatExporter.Core/Models/AuthToken.cs @@ -0,0 +1,15 @@ +namespace DiscordChatExporter.Core.Models +{ + public class AuthToken + { + public AuthTokenType Type { get; } + + public string Value { get; } + + public AuthToken(AuthTokenType type, string value) + { + Type = type; + Value = value; + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Models/AuthTokenType.cs b/DiscordChatExporter.Core/Models/AuthTokenType.cs new file mode 100644 index 0000000..1d1e445 --- /dev/null +++ b/DiscordChatExporter.Core/Models/AuthTokenType.cs @@ -0,0 +1,8 @@ +namespace DiscordChatExporter.Core.Models +{ + public enum AuthTokenType + { + User, + Bot + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Services/DataService.cs b/DiscordChatExporter.Core/Services/DataService.cs index 3b57eb3..0f52819 100644 --- a/DiscordChatExporter.Core/Services/DataService.cs +++ b/DiscordChatExporter.Core/Services/DataService.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; using DiscordChatExporter.Core.Exceptions; using DiscordChatExporter.Core.Models; using Newtonsoft.Json.Linq; using DiscordChatExporter.Core.Internal; using Polly; +using Tyrrrz.Extensions; namespace DiscordChatExporter.Core.Services { @@ -15,17 +17,9 @@ namespace DiscordChatExporter.Core.Services { private readonly HttpClient _httpClient = new HttpClient(); - private async Task GetApiResponseAsync(string token, string resource, string endpoint, + private async Task GetApiResponseAsync(AuthToken token, string resource, string endpoint, params string[] parameters) { - // Format URL - const string apiRoot = "https://discordapp.com/api/v6"; - var url = $"{apiRoot}/{resource}/{endpoint}?token={token}"; - - // Add parameters - foreach (var parameter in parameters) - url += $"&{parameter}"; - // Create request policy var policy = Policy .Handle(e => (int) e.StatusCode == 429) @@ -34,23 +28,45 @@ namespace DiscordChatExporter.Core.Services // Send request return await policy.ExecuteAsync(async () => { - using (var response = await _httpClient.GetAsync(url)) + // Create request + const string apiRoot = "https://discordapp.com/api/v6"; + using (var request = new HttpRequestMessage(HttpMethod.Get, $"{apiRoot}/{resource}/{endpoint}")) { - // Check status code - // We throw our own exception here because default one doesn't have status code - if (!response.IsSuccessStatusCode) - throw new HttpErrorStatusCodeException(response.StatusCode, response.ReasonPhrase); - - // Get content - var raw = await response.Content.ReadAsStringAsync(); - - // Parse - return JToken.Parse(raw); + // Add url parameter for the user token + if (token.Type == AuthTokenType.User) + request.RequestUri = request.RequestUri.SetQueryParameter("token", token.Value); + + // Add header for the bot token + if (token.Type == AuthTokenType.Bot) + request.Headers.Authorization = new AuthenticationHeaderValue("Bot", token.Value); + + // Add parameters + foreach (var parameter in parameters) + { + var key = parameter.SubstringUntil("="); + var value = parameter.SubstringAfter("="); + request.RequestUri = request.RequestUri.SetQueryParameter(key, value); + } + + // Get response + using (var response = await _httpClient.SendAsync(request)) + { + // Check status code + // We throw our own exception here because default one doesn't have status code + if (!response.IsSuccessStatusCode) + throw new HttpErrorStatusCodeException(response.StatusCode, response.ReasonPhrase); + + // Get content + var raw = await response.Content.ReadAsStringAsync(); + + // Parse + return JToken.Parse(raw); + } } }); } - public async Task GetGuildAsync(string token, string guildId) + public async Task GetGuildAsync(AuthToken token, string guildId) { var response = await GetApiResponseAsync(token, "guilds", guildId); var guild = ParseGuild(response); @@ -58,7 +74,7 @@ namespace DiscordChatExporter.Core.Services return guild; } - public async Task GetChannelAsync(string token, string channelId) + public async Task GetChannelAsync(AuthToken token, string channelId) { var response = await GetApiResponseAsync(token, "channels", channelId); var channel = ParseChannel(response); @@ -66,7 +82,7 @@ namespace DiscordChatExporter.Core.Services return channel; } - public async Task> GetUserGuildsAsync(string token) + public async Task> GetUserGuildsAsync(AuthToken token) { var response = await GetApiResponseAsync(token, "users", "@me/guilds", "limit=100"); var guilds = response.Select(ParseGuild).ToArray(); @@ -74,7 +90,7 @@ namespace DiscordChatExporter.Core.Services return guilds; } - public async Task> GetDirectMessageChannelsAsync(string token) + public async Task> GetDirectMessageChannelsAsync(AuthToken token) { var response = await GetApiResponseAsync(token, "users", "@me/channels"); var channels = response.Select(ParseChannel).ToArray(); @@ -82,7 +98,7 @@ namespace DiscordChatExporter.Core.Services return channels; } - public async Task> GetGuildChannelsAsync(string token, string guildId) + public async Task> GetGuildChannelsAsync(AuthToken token, string guildId) { var response = await GetApiResponseAsync(token, "guilds", $"{guildId}/channels"); var channels = response.Select(ParseChannel).ToArray(); @@ -90,7 +106,7 @@ namespace DiscordChatExporter.Core.Services return channels; } - public async Task> GetGuildRolesAsync(string token, string guildId) + public async Task> GetGuildRolesAsync(AuthToken token, string guildId) { var response = await GetApiResponseAsync(token, "guilds", $"{guildId}/roles"); var roles = response.Select(ParseRole).ToArray(); @@ -98,7 +114,7 @@ namespace DiscordChatExporter.Core.Services return roles; } - public async Task> GetChannelMessagesAsync(string token, string channelId, + public async Task> GetChannelMessagesAsync(AuthToken token, string channelId, DateTime? from = null, DateTime? to = null, IProgress progress = null) { var result = new List(); @@ -169,7 +185,7 @@ namespace DiscordChatExporter.Core.Services return result; } - public async Task GetMentionablesAsync(string token, string guildId, + public async Task GetMentionablesAsync(AuthToken token, string guildId, IEnumerable messages) { // Get channels and roles diff --git a/DiscordChatExporter.Core/Services/IDataService.cs b/DiscordChatExporter.Core/Services/IDataService.cs index eb0bcd2..9c5522c 100644 --- a/DiscordChatExporter.Core/Services/IDataService.cs +++ b/DiscordChatExporter.Core/Services/IDataService.cs @@ -7,22 +7,22 @@ namespace DiscordChatExporter.Core.Services { public interface IDataService { - Task GetGuildAsync(string token, string guildId); + Task GetGuildAsync(AuthToken token, string guildId); - Task GetChannelAsync(string token, string channelId); + Task GetChannelAsync(AuthToken token, string channelId); - Task> GetUserGuildsAsync(string token); + Task> GetUserGuildsAsync(AuthToken token); - Task> GetDirectMessageChannelsAsync(string token); + Task> GetDirectMessageChannelsAsync(AuthToken token); - Task> GetGuildChannelsAsync(string token, string guildId); + Task> GetGuildChannelsAsync(AuthToken token, string guildId); - Task> GetGuildRolesAsync(string token, string guildId); + Task> GetGuildRolesAsync(AuthToken token, string guildId); - Task> GetChannelMessagesAsync(string token, string channelId, + Task> GetChannelMessagesAsync(AuthToken token, string channelId, DateTime? from = null, DateTime? to = null, IProgress progress = null); - Task GetMentionablesAsync(string token, string guildId, + Task GetMentionablesAsync(AuthToken token, string guildId, IEnumerable messages); } } \ No newline at end of file diff --git a/DiscordChatExporter.Core/Services/ISettingsService.cs b/DiscordChatExporter.Core/Services/ISettingsService.cs index 7e52696..0251466 100644 --- a/DiscordChatExporter.Core/Services/ISettingsService.cs +++ b/DiscordChatExporter.Core/Services/ISettingsService.cs @@ -9,7 +9,7 @@ namespace DiscordChatExporter.Core.Services string DateFormat { get; set; } int MessageGroupLimit { get; set; } - string LastToken { get; set; } + AuthToken LastToken { get; set; } ExportFormat LastExportFormat { get; set; } void Load(); diff --git a/DiscordChatExporter.Core/Services/SettingsService.cs b/DiscordChatExporter.Core/Services/SettingsService.cs index 5329a59..12f9e01 100644 --- a/DiscordChatExporter.Core/Services/SettingsService.cs +++ b/DiscordChatExporter.Core/Services/SettingsService.cs @@ -10,7 +10,7 @@ namespace DiscordChatExporter.Core.Services public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt"; public int MessageGroupLimit { get; set; } = 20; - public string LastToken { get; set; } + public AuthToken LastToken { get; set; } public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark; public SettingsService() diff --git a/DiscordChatExporter.Gui/App.xaml b/DiscordChatExporter.Gui/App.xaml index a4aa02d..ea53d08 100644 --- a/DiscordChatExporter.Gui/App.xaml +++ b/DiscordChatExporter.Gui/App.xaml @@ -96,6 +96,21 @@ + + diff --git a/DiscordChatExporter.Gui/ViewModels/ExportSetupViewModel.cs b/DiscordChatExporter.Gui/ViewModels/ExportSetupViewModel.cs index b4b94e2..8469821 100644 --- a/DiscordChatExporter.Gui/ViewModels/ExportSetupViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/ExportSetupViewModel.cs @@ -34,7 +34,8 @@ namespace DiscordChatExporter.Gui.ViewModels } } - public IReadOnlyList AvailableFormats { get; } + public IReadOnlyList AvailableFormats => + Enum.GetValues(typeof(ExportFormat)).Cast().ToArray(); public ExportFormat SelectedFormat { @@ -69,9 +70,6 @@ namespace DiscordChatExporter.Gui.ViewModels { _settingsService = settingsService; - // Defaults - AvailableFormats = Enum.GetValues(typeof(ExportFormat)).Cast().ToArray(); - // Commands ExportCommand = new RelayCommand(Export, () => FilePath.IsNotBlank()); diff --git a/DiscordChatExporter.Gui/ViewModels/IMainViewModel.cs b/DiscordChatExporter.Gui/ViewModels/IMainViewModel.cs index aef393d..37b48ab 100644 --- a/DiscordChatExporter.Gui/ViewModels/IMainViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/IMainViewModel.cs @@ -12,7 +12,8 @@ namespace DiscordChatExporter.Gui.ViewModels bool IsProgressIndeterminate { get; } double Progress { get; } - string Token { get; set; } + bool IsBotToken { get; set; } + string TokenValue { get; set; } IReadOnlyList AvailableGuilds { get; } Guild SelectedGuild { get; set; } diff --git a/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs b/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs index 6510b1e..4f94830 100644 --- a/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs @@ -26,7 +26,8 @@ namespace DiscordChatExporter.Gui.ViewModels private bool _isBusy; private double _progress; - private string _token; + private bool _isBotToken; + private string _tokenValue; private IReadOnlyList _availableGuilds; private Guild _selectedGuild; private IReadOnlyList _availableChannels; @@ -56,15 +57,21 @@ namespace DiscordChatExporter.Gui.ViewModels } } - public string Token + public bool IsBotToken { - get => _token; + get => _isBotToken; + set => Set(ref _isBotToken, value); + } + + public string TokenValue + { + get => _tokenValue; set { // Remove invalid chars value = value?.Trim('"'); - Set(ref _token, value); + Set(ref _tokenValue, value); PullDataCommand.RaiseCanExecuteChanged(); } } @@ -117,7 +124,7 @@ namespace DiscordChatExporter.Gui.ViewModels // Commands ViewLoadedCommand = new RelayCommand(ViewLoaded); ViewClosedCommand = new RelayCommand(ViewClosed); - PullDataCommand = new RelayCommand(PullData, () => Token.IsNotBlank() && !IsBusy); + PullDataCommand = new RelayCommand(PullData, () => TokenValue.IsNotBlank() && !IsBusy); ShowSettingsCommand = new RelayCommand(ShowSettings); ShowAboutCommand = new RelayCommand(ShowAbout); ShowExportSetupCommand = new RelayCommand(ShowExportSetup, _ => !IsBusy); @@ -132,8 +139,12 @@ namespace DiscordChatExporter.Gui.ViewModels // Load settings _settingsService.Load(); - // Set last token - Token = _settingsService.LastToken; + // Get last token + if (_settingsService.LastToken != null) + { + IsBotToken = _settingsService.LastToken.Type == AuthTokenType.Bot; + TokenValue = _settingsService.LastToken.Value; + } // Check and prepare update try @@ -169,8 +180,10 @@ namespace DiscordChatExporter.Gui.ViewModels { IsBusy = true; - // Copy token so it doesn't get mutated - var token = Token; + // Create token + var token = new AuthToken( + IsBotToken ? AuthTokenType.Bot : AuthTokenType.User, + TokenValue); // Save token _settingsService.LastToken = token; diff --git a/DiscordChatExporter.Gui/Views/MainWindow.xaml b/DiscordChatExporter.Gui/Views/MainWindow.xaml index 4eab1c0..83f731e 100644 --- a/DiscordChatExporter.Gui/Views/MainWindow.xaml +++ b/DiscordChatExporter.Gui/Views/MainWindow.xaml @@ -9,7 +9,7 @@ Height="550" Background="{DynamicResource MaterialDesignPaper}" DataContext="{Binding MainViewModel, Source={StaticResource Container}}" - FocusManager.FocusedElement="{Binding ElementName=TokenTextBox}" + FocusManager.FocusedElement="{Binding ElementName=TokenValueTextBox}" FontFamily="{DynamicResource MaterialDesignFont}" Icon="/DiscordChatExporter;component/favicon.ico" SnapsToDevicePixels="True" @@ -48,27 +48,47 @@ Margin="6,6,0,6"> + - - + + + + + + + + + + + + Text="{Binding TokenValue, UpdateSourceTrigger=PropertyChanged}" />