Compare commits

...

4 Commits

Author SHA1 Message Date
Oleksii Holub f63ea41188 Update version 2018-07-25 11:53:05 +03:00
Alexey Golub 3572a21aad Allow using bot token instead of user token (#70) 2018-07-24 22:47:45 +03:00
Alexey Golub 37ee0b8be3 Cleanup templates and change how model is referenced 2018-07-03 22:58:46 +03:00
trainboy2019 ed146bac22 Add support for animated avatars (#66) 2018-07-02 21:12:32 +03:00
27 changed files with 300 additions and 154 deletions
+5
View File
@@ -1,3 +1,8 @@
### v2.6 (25-Jul-2018)
- Added support for bot tokens as an alternative to user tokens. For GUI, use the button in the top-left to switch between user and bot token. For CLI, pass the `--bot` switch to indicate that the given token is a bot token.
- Added support for animated avatars.
### v2.5.1 (01-Jul-2018)
- Fixed a bug that would prevent timestamps from rendering properly in CSV and PlainText exports.
+1 -1
View File
@@ -8,4 +8,4 @@ $files | Compress-Archive -DestinationPath "$PSScriptRoot\bin\DiscordChatExporte
# CLI
$files = @()
$files += Get-ChildItem -Path "$PSScriptRoot\..\DiscordChatExporter.Cli\bin\Release\net461\*" -Include "*.exe", "*.dll", "*.config"
$files | Compress-Archive -DestinationPath "$PSScriptRoot\bin\DiscordChatExporter.Cli.zip" -Force
$files | Compress-Archive -DestinationPath "$PSScriptRoot\bin\DiscordChatExporter.CLI.zip" -Force
+3 -1
View File
@@ -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; }
@@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net461</TargetFramework>
<Version>2.5.1</Version>
<Version>2.6</Version>
<Company>Tyrrrz</Company>
<Copyright>Copyright (c) 2017-2018 Alexey Golub</Copyright>
<ApplicationIcon>..\favicon.ico</ApplicationIcon>
+22 -8
View File
@@ -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,
@@ -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);
}
}
@@ -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
@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
<Version>2.5.1</Version>
<Version>2.6</Version>
</PropertyGroup>
<ItemGroup>
@@ -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;
}
}
}
@@ -0,0 +1,8 @@
namespace DiscordChatExporter.Core.Models
{
public enum AuthTokenType
{
User,
Bot
}
}
+26 -6
View File
@@ -1,4 +1,5 @@
using Tyrrrz.Extensions;
using System;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Models
{
@@ -14,13 +15,32 @@ namespace DiscordChatExporter.Core.Models
public string FullName => $"{Name}#{Discriminator:0000}";
public string AvatarHash { get; }
public string DefaultAvatarHash => $"{Discriminator % 5}";
public string AvatarUrl => AvatarHash.IsNotBlank()
? $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.png"
: $"https://cdn.discordapp.com/embed/avatars/{DefaultAvatarHash}.png";
public string AvatarHash { get; }
public bool IsAvatarAnimated =>
AvatarHash.IsNotBlank() && AvatarHash.StartsWith("a_", StringComparison.Ordinal);
public string AvatarUrl
{
get
{
// Custom avatar
if (AvatarHash.IsNotBlank())
{
// Animated
if (IsAvatarAnimated)
return $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.gif";
// Non-animated
return $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.png";
}
// Default avatar
return $"https://cdn.discordapp.com/embed/avatars/{DefaultAvatarHash}.png";
}
}
public User(string id, int discriminator, string name, string avatarHash)
{
@@ -1,5 +1,5 @@
Author;Date;Content;Attachments;
{{~ for group in MessageGroups -}}
{{~ for group in Model.MessageGroups -}}
{{- for message in group.Messages -}}
{{- message.Author.FullName }};
Can't render this file because it contains an unexpected character in line 10 and column 41.
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<title>{{ Guild.Name | html.escape }} - {{ Channel.Name | html.escape }}</title>
<title>{{ Model.Guild.Name | html.escape }} - {{ Model.Channel.Name | html.escape }}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<style>
@@ -14,26 +14,26 @@
{{~ # Info ~}}
<div class="info">
<div class="info__guild-icon-container">
<img class="info__guild-icon" src="{{ Guild.IconUrl }}" />
<img class="info__guild-icon" src="{{ Model.Guild.IconUrl }}" />
</div>
<div class="info__metadata">
<div class="info__guild-name">{{ Guild.Name | html.escape }}</div>
<div class="info__channel-name">{{ Channel.Name | html.escape }}</div>
<div class="info__guild-name">{{ Model.Guild.Name | html.escape }}</div>
<div class="info__channel-name">{{ Model.Channel.Name | html.escape }}</div>
{{~ if Channel.Topic ~}}
<div class="info__channel-topic">{{ Channel.Topic | html.escape }}</div>
{{~ if Model.Channel.Topic ~}}
<div class="info__channel-topic">{{ Model.Channel.Topic | html.escape }}</div>
{{~ end ~}}
<div class="info__channel-message-count">{{ TotalMessageCount | Format "N0" }} messages</div>
<div class="info__channel-message-count">{{ Model.TotalMessageCount | Format "N0" }} messages</div>
{{~ if From || To ~}}
{{~ if Model.From || Model.To ~}}
<div class="info__channel-date-range">
{{~ if From && To ~}}
Between {{ From | FormatDate | html.escape }} and {{ To | FormatDate | html.escape }}
{{~ else if From ~}}
After {{ From | FormatDate | html.escape }}
{{~ else if To ~}}
Before {{ To | FormatDate | html.escape }}
{{~ if Model.From && Model.To ~}}
Between {{ Model.From | FormatDate | html.escape }} and {{ Model.To | FormatDate | html.escape }}
{{~ else if Model.From ~}}
After {{ Model.From | FormatDate | html.escape }}
{{~ else if Model.To ~}}
Before {{ Model.To | FormatDate | html.escape }}
{{~ end ~}}
</div>
{{~ end ~}}
@@ -42,7 +42,7 @@
{{~ # Log ~}}
<div class="chatlog">
{{~ for group in MessageGroups ~}}
{{~ for group in Model.MessageGroups ~}}
<div class="chatlog__message-group">
{{~ # Avatar ~}}
<div class="chatlog__author-avatar-container">
@@ -1,14 +1,14 @@
{{~ # Info ~}}
==============================================================
Guild: {{ Guild.Name }}
Channel: {{ Channel.Name }}
Topic: {{ Channel.Topic }}
Messages: {{ TotalMessageCount | Format "N0" }}
Range: {{ if From }}{{ From | FormatDate }} {{ end }}{{ if From || To }}->{{ end }}{{ if To }} {{ To | FormatDate }}{{ end }}
Guild: {{ Model.Guild.Name }}
Channel: {{ Model.Channel.Name }}
Topic: {{ Model.Channel.Topic }}
Messages: {{ Model.TotalMessageCount | Format "N0" }}
Range: {{ if Model.From }}{{ Model.From | FormatDate }} {{ end }}{{ if Model.From || Model.To }}->{{ end }}{{ if Model.To }} {{ Model.To | FormatDate }}{{ end }}
==============================================================
{{~ # Log ~}}
{{~ for group in MessageGroups ~}}
{{~ for group in Model.MessageGroups ~}}
{{~ # Author name and timestamp ~}}
{{~ }}[{{ group.Timestamp | FormatDate }}] {{ group.Author.FullName }}
{{~ # Messages ~}}
@@ -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<JToken> GetApiResponseAsync(string token, string resource, string endpoint,
private async Task<JToken> 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<HttpErrorStatusCodeException>(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);
// Add url parameter for the user token
if (token.Type == AuthTokenType.User)
request.RequestUri = request.RequestUri.SetQueryParameter("token", token.Value);
// Get content
var raw = await response.Content.ReadAsStringAsync();
// Add header for the bot token
if (token.Type == AuthTokenType.Bot)
request.Headers.Authorization = new AuthenticationHeaderValue("Bot", token.Value);
// Parse
return JToken.Parse(raw);
// 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<Guild> GetGuildAsync(string token, string guildId)
public async Task<Guild> 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<Channel> GetChannelAsync(string token, string channelId)
public async Task<Channel> 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<IReadOnlyList<Guild>> GetUserGuildsAsync(string token)
public async Task<IReadOnlyList<Guild>> 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<IReadOnlyList<Channel>> GetDirectMessageChannelsAsync(string token)
public async Task<IReadOnlyList<Channel>> 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<IReadOnlyList<Channel>> GetGuildChannelsAsync(string token, string guildId)
public async Task<IReadOnlyList<Channel>> 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<IReadOnlyList<Role>> GetGuildRolesAsync(string token, string guildId)
public async Task<IReadOnlyList<Role>> 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<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(AuthToken token, string channelId,
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null)
{
var result = new List<Message>();
@@ -169,7 +185,7 @@ namespace DiscordChatExporter.Core.Services
return result;
}
public async Task<Mentionables> GetMentionablesAsync(string token, string guildId,
public async Task<Mentionables> GetMentionablesAsync(AuthToken token, string guildId,
IEnumerable<Message> messages)
{
// Get channels and roles
@@ -307,7 +307,7 @@ namespace DiscordChatExporter.Core.Services
var scriptObject = new ScriptObject();
// Import chat log
scriptObject.Import(_log, TemplateMemberFilter, TemplateMemberRenamer);
scriptObject.SetValue("Model", _log, true);
// Import functions
scriptObject.Import(nameof(Format), new Func<IFormattable, string, string>(Format));
@@ -7,9 +7,6 @@ namespace DiscordChatExporter.Core.Services
{
public partial class ExportService : IExportService
{
private static readonly MemberRenamerDelegate TemplateMemberRenamer = m => m.Name;
private static readonly MemberFilterDelegate TemplateMemberFilter = m => true;
private readonly ISettingsService _settingsService;
public ExportService(ISettingsService settingsService)
@@ -30,8 +27,10 @@ namespace DiscordChatExporter.Core.Services
var context = new TemplateContext
{
TemplateLoader = loader,
MemberRenamer = TemplateMemberRenamer,
MemberFilter = TemplateMemberFilter
MemberRenamer = m => m.Name,
MemberFilter = m => true,
LoopLimit = int.MaxValue,
StrictVariables = true
};
// Create template model
@@ -7,22 +7,22 @@ namespace DiscordChatExporter.Core.Services
{
public interface IDataService
{
Task<Guild> GetGuildAsync(string token, string guildId);
Task<Guild> GetGuildAsync(AuthToken token, string guildId);
Task<Channel> GetChannelAsync(string token, string channelId);
Task<Channel> GetChannelAsync(AuthToken token, string channelId);
Task<IReadOnlyList<Guild>> GetUserGuildsAsync(string token);
Task<IReadOnlyList<Guild>> GetUserGuildsAsync(AuthToken token);
Task<IReadOnlyList<Channel>> GetDirectMessageChannelsAsync(string token);
Task<IReadOnlyList<Channel>> GetDirectMessageChannelsAsync(AuthToken token);
Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string token, string guildId);
Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(AuthToken token, string guildId);
Task<IReadOnlyList<Role>> GetGuildRolesAsync(string token, string guildId);
Task<IReadOnlyList<Role>> GetGuildRolesAsync(AuthToken token, string guildId);
Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
Task<IReadOnlyList<Message>> GetChannelMessagesAsync(AuthToken token, string channelId,
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null);
Task<Mentionables> GetMentionablesAsync(string token, string guildId,
Task<Mentionables> GetMentionablesAsync(AuthToken token, string guildId,
IEnumerable<Message> messages);
}
}
@@ -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();
@@ -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()
+15
View File
@@ -96,6 +96,21 @@
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}" />
</Style>
<Style
x:Key="MaterialDesignFlatActionToggleButton"
BasedOn="{StaticResource MaterialDesignActionToggleButton}"
TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource PrimaryHueMidBrush}" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource MaterialDesignFlatButtonClick}" />
<Setter Property="BorderBrush" Value="{DynamicResource MaterialDesignFlatButtonClick}" />
</Trigger>
</Style.Triggers>
</Style>
<!-- Converters -->
<converters:ExportFormatToStringConverter x:Key="ExportFormatToStringConverter" />
@@ -3,5 +3,5 @@
[assembly: AssemblyTitle("DiscordChatExporter")]
[assembly: AssemblyCompany("Tyrrrz")]
[assembly: AssemblyCopyright("Copyright (c) 2017-2018 Alexey Golub")]
[assembly: AssemblyVersion("2.5.1")]
[assembly: AssemblyFileVersion("2.5.1")]
[assembly: AssemblyVersion("2.6")]
[assembly: AssemblyFileVersion("2.6")]
@@ -34,7 +34,8 @@ namespace DiscordChatExporter.Gui.ViewModels
}
}
public IReadOnlyList<ExportFormat> AvailableFormats { get; }
public IReadOnlyList<ExportFormat> AvailableFormats =>
Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray();
public ExportFormat SelectedFormat
{
@@ -69,9 +70,6 @@ namespace DiscordChatExporter.Gui.ViewModels
{
_settingsService = settingsService;
// Defaults
AvailableFormats = Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray();
// Commands
ExportCommand = new RelayCommand(Export, () => FilePath.IsNotBlank());
@@ -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<Guild> AvailableGuilds { get; }
Guild SelectedGuild { get; set; }
@@ -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<Guild> _availableGuilds;
private Guild _selectedGuild;
private IReadOnlyList<Channel> _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<Channel>(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;
+89 -38
View File
@@ -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">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Token -->
<TextBox
x:Name="TokenTextBox"
Grid.Row="0"
<!-- Token type -->
<ToggleButton
Grid.Column="0"
Margin="6"
IsChecked="{Binding IsBotToken}"
Style="{StaticResource 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>
<!-- Token value -->
<TextBox
x:Name="TokenValueTextBox"
Grid.Column="1"
Margin="2,6,6,7"
materialDesign:HintAssist.Hint="Token"
materialDesign:TextFieldAssist.DecorationVisibility="Hidden"
materialDesign:TextFieldAssist.TextBoxViewMargin="0,0,2,0"
BorderThickness="0"
FontSize="16"
Text="{Binding Token, UpdateSourceTrigger=PropertyChanged}" />
Text="{Binding TokenValue, UpdateSourceTrigger=PropertyChanged}" />
<!-- Pull data button -->
<Button
Grid.Row="0"
Grid.Column="1"
Grid.Column="2"
Margin="0,6,6,6"
Padding="4"
Command="{Binding PullDataCommand}"
@@ -99,8 +119,8 @@
<ProgressBar
Background="Transparent"
IsIndeterminate="{Binding IsProgressIndeterminate}"
Value="{Binding Progress, Mode=OneWay}"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibilityConverter}}" />
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibilityConverter}}"
Value="{Binding Progress, Mode=OneWay}" />
</StackPanel>
</Border>
@@ -196,34 +216,65 @@
</DockPanel>
<!-- Usage instructions -->
<StackPanel Margin="32,32,8,8" Visibility="{Binding IsDataAvailable, Converter={StaticResource InvertBoolToVisibilityConverter}}">
<TextBlock FontSize="18" Text="DiscordChatExporter needs your authorization token to work." />
<TextBlock
Margin="0,8,0,0"
FontSize="16"
Text="To obtain it, follow these steps:" />
<TextBlock Margin="8,0,0,0" FontSize="14">
<Run Text="1. Open the Discord app" />
<LineBreak />
<Run Text="2. Log in if you haven't" />
<LineBreak />
<Run Text="3. Press" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Ctrl+Shift+I" />
<LineBreak />
<Run Text="4. Navigate to" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Application" />
<Run Text="tab" />
<LineBreak />
<Run Text="5. Expand" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Storage &gt; Local Storage &gt; https://discordapp.com" />
<LineBreak />
<Run Text="6. Find" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="&quot;token&quot;" />
<Run Text="under key and copy the value" />
<LineBreak />
<Run Text="7. Paste the value in the textbox above" />
</TextBlock>
</StackPanel>
<Grid Margin="32,32,8,8" Visibility="{Binding IsDataAvailable, Converter={StaticResource InvertBoolToVisibilityConverter}}">
<!-- User token -->
<StackPanel Visibility="{Binding IsBotToken, Converter={StaticResource InvertBoolToVisibilityConverter}}">
<TextBlock FontSize="18" Text="DiscordChatExporter needs your user token to work." />
<TextBlock
Margin="0,8,0,0"
FontSize="16"
Text="To obtain it, follow these steps:" />
<TextBlock Margin="8,0,0,0" FontSize="14">
<Run Text="1. Open the Discord app" />
<LineBreak />
<Run Text="2. Log in if you haven't" />
<LineBreak />
<Run Text="3. Press" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Ctrl+Shift+I" />
<Run Text="to show developer tools" />
<LineBreak />
<Run Text="4. Navigate to the" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Application" />
<Run Text="tab" />
<LineBreak />
<Run Text="5. Expand" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Storage &gt; Local Storage &gt; https://discordapp.com" />
<LineBreak />
<Run Text="6. Find the" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="&quot;token&quot;" />
<Run Text="key and copy its value" />
<LineBreak />
<Run Text="7. Paste the value in the textbox above" />
</TextBlock>
</StackPanel>
<!-- Bot token -->
<StackPanel Visibility="{Binding IsBotToken, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock FontSize="18" Text="DiscordChatExporter needs your bot token to work." />
<TextBlock
Margin="0,8,0,0"
FontSize="16"
Text="To obtain it, follow these steps:" />
<TextBlock Margin="8,0,0,0" FontSize="14">
<Run Text="1. Go to Discord developer portal" />
<LineBreak />
<Run Text="2. Log in if you haven't" />
<LineBreak />
<Run Text="3. Open your application's settings" />
<LineBreak />
<Run Text="4. Navigate to the" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Bot" />
<Run Text="section on the left" />
<LineBreak />
<Run Text="5. Under" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Token" />
<Run Text="click" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Copy" />
<LineBreak />
<Run Text="6. Paste the value in the textbox above" />
</TextBlock>
</StackPanel>
</Grid>
<materialDesign:Snackbar x:Name="Snackbar" />
</Grid>
</DockPanel>
+5 -16
View File
@@ -18,22 +18,11 @@ DiscordChatExporter can be used to export message history from a [Discord](https
## Features
- Intuitive GUI that displays available guilds and channels
- CLI as additional alternative to GUI
- Date ranges to limit messages
- Groups messages by author and time
- Exports to a plain text file
- Exports to an HTML file
- Dark and light themes
- User avatars
- Inline image attachments
- Embeds and webhooks
- Full markdown support
- Automatic links
- Styled similarly to the app
- Exports to a CSV file
- Renders custom emojis
- Resolves user, role and channel mentions
- Graphical and command line interfaces
- Supports both user tokens and bot tokens
- Allows retrieving messages in specified date range
- Multiple export formats: HTML (dark/light), TXT and CSV
- Renders all message features including: markdown, attachments, embeds, emojis, mentions, etc
## Libraries used