Compare commits

..

7 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
Alexey Golub b9c449709f Update version 2018-07-01 21:12:06 +03:00
Alexey Golub 25207c5093 Improve the templates a bit 2018-07-01 21:04:03 +03:00
Alexey Golub 4fbec95208 Fix CSV and PlainText outputs not rendering timestamps correctly
Fixes #65
2018-07-01 16:53:33 +03:00
27 changed files with 426 additions and 266 deletions
+11
View File
@@ -1,3 +1,14 @@
### 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.
- Fixed incorrect layout in CSV export.
- Improved readability of PlainText export.
### v2.5 (28-Jun-2018) ### v2.5 (28-Jun-2018)
- Added support for embeds and webhooks (HTML only). - Added support for embeds and webhooks (HTML only).
+1 -1
View File
@@ -8,4 +8,4 @@ $files | Compress-Archive -DestinationPath "$PSScriptRoot\bin\DiscordChatExporte
# CLI # CLI
$files = @() $files = @()
$files += Get-ChildItem -Path "$PSScriptRoot\..\DiscordChatExporter.Cli\bin\Release\net461\*" -Include "*.exe", "*.dll", "*.config" $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 class CliOptions
{ {
public string Token { get; set; } public string TokenValue { get; set; }
public bool IsBotToken { get; set; }
public string ChannelId { get; set; } public string ChannelId { get; set; }
@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net461</TargetFramework> <TargetFramework>net461</TargetFramework>
<Version>2.5</Version> <Version>2.6</Version>
<Company>Tyrrrz</Company> <Company>Tyrrrz</Company>
<Copyright>Copyright (c) 2017-2018 Alexey Golub</Copyright> <Copyright>Copyright (c) 2017-2018 Alexey Golub</Copyright>
<ApplicationIcon>..\favicon.ico</ApplicationIcon> <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($"=== Discord Chat Exporter (Command Line Interface) v{version} ===");
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("[-t] [--token] Discord authorization token."); 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("[-c] [--channel] Discord channel ID.");
Console.WriteLine("[-f] [--format] Export format. Optional."); Console.WriteLine("[-f] [--format] Export format. Optional.");
Console.WriteLine("[-o] [--output] Output file path. Optional."); Console.WriteLine("[-o] [--output] Output file path. Optional.");
@@ -28,20 +29,27 @@ namespace DiscordChatExporter.Cli
Console.WriteLine(); Console.WriteLine();
Console.WriteLine($"Available export formats: {availableFormats.JoinToString(", ")}"); Console.WriteLine($"Available export formats: {availableFormats.JoinToString(", ")}");
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("# To get authorization token:"); Console.WriteLine("# To get user token:");
Console.WriteLine(" - Open Discord app"); Console.WriteLine(" - Open Discord app");
Console.WriteLine(" - Log in if you haven't"); Console.WriteLine(" - Log in if you haven't");
Console.WriteLine(" - Press Ctrl+Shift+I"); Console.WriteLine(" - Press Ctrl+Shift+I to show developer tools");
Console.WriteLine(" - Navigate to Application tab"); Console.WriteLine(" - Navigate to the Application tab");
Console.WriteLine(" - Expand Storage > Local Storage > https://discordapp.com"); 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();
Console.WriteLine("# To get channel ID:"); Console.WriteLine("# To get channel ID:");
Console.WriteLine(" - Open Discord app"); Console.WriteLine(" - Open Discord app");
Console.WriteLine(" - Log in if you haven't"); Console.WriteLine(" - Log in if you haven't");
Console.WriteLine(" - Go to any channel you want to export"); Console.WriteLine(" - Go to any channel you want to export");
Console.WriteLine(" - Press Ctrl+Shift+I"); Console.WriteLine(" - Press Ctrl+Shift+I to show developer tools");
Console.WriteLine(" - Navigate to Console tab"); Console.WriteLine(" - Navigate to the Console tab");
Console.WriteLine(" - Type \"document.URL\" and press Enter"); Console.WriteLine(" - Type \"document.URL\" and press Enter");
Console.WriteLine(" - Copy the long sequence of numbers after last slash"); Console.WriteLine(" - Copy the long sequence of numbers after last slash");
} }
@@ -52,7 +60,8 @@ namespace DiscordChatExporter.Cli
var settings = Container.SettingsService; 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.ChannelId).As('c', "channel").Required();
argsParser.Setup(o => o.ExportFormat).As('f', "format").SetDefault(ExportFormat.HtmlDark); argsParser.Setup(o => o.ExportFormat).As('f', "format").SetDefault(ExportFormat.HtmlDark);
argsParser.Setup(o => o.FilePath).As('o', "output").SetDefault(null); argsParser.Setup(o => o.FilePath).As('o', "output").SetDefault(null);
@@ -92,10 +101,15 @@ namespace DiscordChatExporter.Cli
settings.DateFormat = options.DateFormat; settings.DateFormat = options.DateFormat;
settings.MessageGroupLimit = options.MessageGroupLimit; settings.MessageGroupLimit = options.MessageGroupLimit;
// Create token
var token = new AuthToken(
options.IsBotToken ? AuthTokenType.Bot : AuthTokenType.User,
options.TokenValue);
// Export // Export
var vm = Container.MainViewModel; var vm = Container.MainViewModel;
vm.ExportAsync( vm.ExportAsync(
options.Token, token,
options.ChannelId, options.ChannelId,
options.FilePath, options.FilePath,
options.ExportFormat, options.ExportFormat,
@@ -6,7 +6,7 @@ namespace DiscordChatExporter.Cli.ViewModels
{ {
public interface IMainViewModel 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); DateTime? to);
} }
} }
@@ -21,7 +21,7 @@ namespace DiscordChatExporter.Cli.ViewModels
_exportService = exportService; _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) DateTime? from, DateTime? to)
{ {
// Get channel and guild // Get channel and guild
@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net461</TargetFramework> <TargetFramework>net461</TargetFramework>
<Version>2.5</Version> <Version>2.6</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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 namespace DiscordChatExporter.Core.Models
{ {
@@ -14,13 +15,32 @@ namespace DiscordChatExporter.Core.Models
public string FullName => $"{Name}#{Discriminator:0000}"; public string FullName => $"{Name}#{Discriminator:0000}";
public string AvatarHash { get; }
public string DefaultAvatarHash => $"{Discriminator % 5}"; public string DefaultAvatarHash => $"{Discriminator % 5}";
public string AvatarUrl => AvatarHash.IsNotBlank() public string AvatarHash { get; }
? $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.png"
: $"https://cdn.discordapp.com/embed/avatars/{DefaultAvatarHash}.png"; 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) public User(string id, int discriminator, string name, string avatarHash)
{ {
@@ -1,10 +1,9 @@
Author;Date;Content;Attachments; Author;Date;Content;Attachments;
{{~ for group in Model.MessageGroups -}}
{{- for group in MessageGroups -}}
{{- for message in group.Messages -}} {{- for message in group.Messages -}}
{{- message.Author.FullName }}; {{- message.Author.FullName }};
{{- message.TimeStamp | FormatDate }}; {{- message.Timestamp | FormatDate }};
{{- message.Content | FormatContent }}; {{- message.Content | FormatContent }};
Can't render this file because it contains an unexpected character in line 11 and column 41.
@@ -2,208 +2,214 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>{{ Guild.Name | HtmlEncode }} - {{ Channel.Name | HtmlEncode }}</title> <title>{{ Model.Guild.Name | html.escape }} - {{ Model.Channel.Name | html.escape }}</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<style>{{ StyleSheet }}</style> <style>
{{ StyleSheet }}
</style>
</head> </head>
<body> <body>
{{~ # Info }} {{~ # Info ~}}
<div class="info"> <div class="info">
<div class="info__guild-icon-container"> <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>
<div class="info__metadata"> <div class="info__metadata">
<div class="info__guild-name">{{ Guild.Name | HtmlEncode }}</div> <div class="info__guild-name">{{ Model.Guild.Name | html.escape }}</div>
<div class="info__channel-name">{{ Channel.Name | HtmlEncode }}</div> <div class="info__channel-name">{{ Model.Channel.Name | html.escape }}</div>
{{ if Channel.Topic }} {{~ if Model.Channel.Topic ~}}
<div class="info__channel-topic">{{ Channel.Topic | HtmlEncode }}</div> <div class="info__channel-topic">{{ Model.Channel.Topic | html.escape }}</div>
{{ end }} {{~ 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"> <div class="info__channel-date-range">
{{ if From && To }} {{~ if Model.From && Model.To ~}}
Between {{ From | FormatDate | HtmlEncode }} and {{ To | FormatDate | HtmlEncode }} Between {{ Model.From | FormatDate | html.escape }} and {{ Model.To | FormatDate | html.escape }}
{{ else if From }} {{~ else if Model.From ~}}
After {{ From | FormatDate | HtmlEncode }} After {{ Model.From | FormatDate | html.escape }}
{{ else if To }} {{~ else if Model.To ~}}
Before {{ To | FormatDate | HtmlEncode }} Before {{ Model.To | FormatDate | html.escape }}
{{ end }} {{~ end ~}}
</div> </div>
{{ end }} {{~ end ~}}
</div> </div>
</div> </div>
{{~ # Log }} {{~ # Log ~}}
<div class="chatlog"> <div class="chatlog">
{{ for group in MessageGroups }} {{~ for group in Model.MessageGroups ~}}
<div class="chatlog__message-group"> <div class="chatlog__message-group">
{{~ # Avatar }} {{~ # Avatar ~}}
<div class="chatlog__author-avatar-container"> <div class="chatlog__author-avatar-container">
<img class="chatlog__author-avatar" src="{{ group.Author.AvatarUrl }}" /> <img class="chatlog__author-avatar" src="{{ group.Author.AvatarUrl }}" />
</div> </div>
<div class="chatlog__messages"> <div class="chatlog__messages">
{{~ # Author name and timestamp }} {{~ # Author name and timestamp ~}}
<span class="chatlog__author-name" title="{{ group.Author.FullName | HtmlEncode }}">{{ group.Author.Name | HtmlEncode }}</span> <span class="chatlog__author-name" title="{{ group.Author.FullName | html.escape }}">{{ group.Author.Name | html.escape }}</span>
<span class="chatlog__timestamp">{{ group.Timestamp | FormatDate | HtmlEncode }}</span> <span class="chatlog__timestamp">{{ group.Timestamp | FormatDate | html.escape }}</span>
{{~ # Messages }} {{~ # Messages ~}}
{{ for message in group.Messages }} {{~ for message in group.Messages ~}}
{{~ # Content }} {{~ # Content ~}}
{{ if message.Content }} {{~ if message.Content ~}}
<div class="chatlog__content"> <div class="chatlog__content">
{{ message.Content | FormatContent }} {{ message.Content | FormatContent }}
{{~ # Edited timestamp }} {{~ # Edited timestamp ~}}
{{ if message.EditedTimestamp }} {{~ if message.EditedTimestamp ~}}
<span class="chatlog__edited-timestamp" title="{{ message.EditedTimestamp | FormatDate | HtmlEncode }}">(edited)</span> <span class="chatlog__edited-timestamp" title="{{ message.EditedTimestamp | FormatDate | html.escape }}">(edited)</span>
{{ end }} {{~ end ~}}
</div> </div>
{{ end }} {{~ end ~}}
{{~ # Attachments }} {{~ # Attachments ~}}
{{ for attachment in message.Attachments }} {{~ for attachment in message.Attachments ~}}
<div class="chatlog__attachment"> <div class="chatlog__attachment">
<a href="{{ attachment.Url }}"> <a href="{{ attachment.Url }}">
{{~ # Image }} {{ # Image }}
{{ if attachment.IsImage }} {{~ if attachment.IsImage ~}}
<img class="chatlog__attachment-thumbnail" src="{{ attachment.Url }}" /> <img class="chatlog__attachment-thumbnail" src="{{ attachment.Url }}" />
{{~ # Non-image }} {{~ # Non-image ~}}
{{ else }} {{~ else ~}}
Attachment: {{ attachment.FileName }} ({{ attachment.FileSize | FormatFileSize }}) Attachment: {{ attachment.FileName }} ({{ attachment.FileSize | FormatFileSize }})
{{ end }} {{~ end ~}}
</a> </a>
</div> </div>
{{ end }} {{~ end ~}}
{{~ # Embeds }} {{~ # Embeds ~}}
{{ for embed in message.Embeds }} {{~ for embed in message.Embeds ~}}
<div class="chatlog__embed"> <div class="chatlog__embed">
<div class="chatlog__embed-color-pill" style="background-color: rgba({{ embed.Color | FormatColor }})"></div> <div class="chatlog__embed-color-pill" style="background-color: rgba({{ embed.Color | FormatColor }})"></div>
<div class="chatlog__embed-content-container"> <div class="chatlog__embed-content-container">
<div class="chatlog__embed-content"> <div class="chatlog__embed-content">
<div class="chatlog__embed-text"> <div class="chatlog__embed-text">
{{~ # Author }} {{~ # Author ~}}
{{ if embed.Author }} {{~ if embed.Author ~}}
<div class="chatlog__embed-author"> <div class="chatlog__embed-author">
{{ if embed.Author.IconUrl }} {{~ if embed.Author.IconUrl ~}}
<img class="chatlog__embed-author-icon" src="{{ embed.Author.IconUrl }}" /> <img class="chatlog__embed-author-icon" src="{{ embed.Author.IconUrl }}" />
{{ end }} {{~ end ~}}
{{ if embed.Author.Name }} {{~ if embed.Author.Name ~}}
<span class="chatlog__embed-author-name"> <span class="chatlog__embed-author-name">
{{ if embed.Author.Url }} {{~ if embed.Author.Url ~}}
<a class="chatlog__embed-author-name-link" href="{{ embed.Author.Url }}">{{ embed.Author.Name | HtmlEncode }}</a> <a class="chatlog__embed-author-name-link" href="{{ embed.Author.Url }}">{{ embed.Author.Name | html.escape }}</a>
{{ else }} {{~ else ~}}
{{ embed.Author.Name | HtmlEncode }} {{ embed.Author.Name | html.escape }}
{{ end }} {{~ end ~}}
</span> </span>
{{ end }} {{~ end ~}}
</div> </div>
{{ end }} {{~ end ~}}
{{~ # Title }} {{~ # Title ~}}
{{ if embed.Title }} {{~ if embed.Title ~}}
<div class="chatlog__embed-title"> <div class="chatlog__embed-title">
{{ if embed.Url }} {{~ if embed.Url ~}}
<a class="chatlog__embed-title-link" href="{{ embed.Url }}">{{ embed.Title | FormatContent }}</a> <a class="chatlog__embed-title-link" href="{{ embed.Url }}">{{ embed.Title | FormatContent }}</a>
{{ else }} {{~ else ~}}
{{ embed.Title | FormatContent }} {{ embed.Title | FormatContent }}
{{ end }} {{~ end ~}}
</div> </div>
{{ end }} {{~ end ~}}
{{~ # Description }} {{~ # Description ~}}
{{ if embed.Description }} {{~ if embed.Description ~}}
<div class="chatlog__embed-description">{{ embed.Description | FormatContent true }}</div> <div class="chatlog__embed-description">{{ embed.Description | FormatContent true }}</div>
{{ end }} {{~ end ~}}
{{~ # Fields }} {{~ # Fields ~}}
<div class="chatlog__embed-fields"> {{~ if embed.Fields | array.size > 0 ~}}
{{ for field in embed.Fields }} <div class="chatlog__embed-fields">
<div class="chatlog__embed-field {{ if field.IsInline }} chatlog__embed-field--inline {{ end }}"> {{~ for field in embed.Fields ~}}
{{ if field.Name }} <div class="chatlog__embed-field {{ if field.IsInline }} chatlog__embed-field--inline {{ end }}">
<div class="chatlog__embed-field-name">{{ field.Name | FormatContent }}</div> {{~ if field.Name ~}}
{{ end }} <div class="chatlog__embed-field-name">{{ field.Name | FormatContent }}</div>
{{ if field.Value }} {{~ end ~}}
<div class="chatlog__embed-field-value">{{ field.Value | FormatContent true }}</div> {{~ if field.Value ~}}
{{ end }} <div class="chatlog__embed-field-value">{{ field.Value | FormatContent true }}</div>
</div> {{~ end ~}}
{{ end }} </div>
</div> {{~ end ~}}
</div>
{{~ end ~}}
</div> </div>
{{~ # Thumbnail }} {{~ # Thumbnail ~}}
{{ if embed.Thumbnail }} {{~ if embed.Thumbnail ~}}
<div class="chatlog__embed-thumbnail-container"> <div class="chatlog__embed-thumbnail-container">
<a class="chatlog__embed-thumbnail-link" href="{{ embed.Thumbnail.Url }}"> <a class="chatlog__embed-thumbnail-link" href="{{ embed.Thumbnail.Url }}">
<img class="chatlog__embed-thumbnail" src="{{ embed.Thumbnail.Url }}" /> <img class="chatlog__embed-thumbnail" src="{{ embed.Thumbnail.Url }}" />
</a> </a>
</div> </div>
{{ end }} {{~ end ~}}
</div> </div>
{{~ # Image }} {{~ # Image ~}}
{{ if embed.Image }} {{~ if embed.Image ~}}
<div class="chatlog__embed-image-container"> <div class="chatlog__embed-image-container">
<a class="chatlog__embed-image-link" href="{{ embed.Image.Url }}"> <a class="chatlog__embed-image-link" href="{{ embed.Image.Url }}">
<img class="chatlog__embed-image" src="{{ embed.Image.Url }}" /> <img class="chatlog__embed-image" src="{{ embed.Image.Url }}" />
</a> </a>
</div> </div>
{{ end }} {{~ end ~}}
{{~ # Footer }} {{~ # Footer ~}}
{{ if embed.Footer || embed.Timestamp }} {{~ if embed.Footer || embed.Timestamp ~}}
<div class="chatlog__embed-footer"> <div class="chatlog__embed-footer">
{{ if embed.Footer }} {{~ if embed.Footer ~}}
{{ if embed.Footer.Text && embed.Footer.IconUrl }} {{~ if embed.Footer.Text && embed.Footer.IconUrl ~}}
<img class="chatlog__embed-footer-icon" src="{{ embed.Footer.IconUrl }}" /> <img class="chatlog__embed-footer-icon" src="{{ embed.Footer.IconUrl }}" />
{{ end }} {{~ end ~}}
{{ end }} {{~ end ~}}
<span class="chatlog__embed-footer-text"> <span class="chatlog__embed-footer-text">
{{ if embed.Footer }} {{~ if embed.Footer ~}}
{{ if embed.Footer.Text }} {{~ if embed.Footer.Text ~}}
{{ embed.Footer.Text | HtmlEncode }} {{ embed.Footer.Text | html.escape }}
{{ if embed.Timestamp }} • {{ end }} {{ if embed.Timestamp }} • {{ end }}
{{ end }} {{~ end ~}}
{{ end }} {{~ end ~}}
{{ if embed.Timestamp }} {{~ if embed.Timestamp ~}}
{{ embed.Timestamp | FormatDate | HtmlEncode }} {{ embed.Timestamp | FormatDate | html.escape }}
{{ end }} {{~ end ~}}
</span> </span>
</div> </div>
{{ end }} {{~ end ~}}
</div> </div>
</div> </div>
{{ end }} {{~ end ~}}
{{~ # Reactions }} {{~ # Reactions ~}}
<div class="chatlog__reactions"> {{~ if message.Reactions | array.size > 0 ~}}
{{ for reaction in message.Reactions }} <div class="chatlog__reactions">
<div class="chatlog__reaction"> {{~ for reaction in message.Reactions ~}}
<span class="chatlog__reaction-emoji"> <div class="chatlog__reaction">
{{~ # Custom emoji }} <span class="chatlog__reaction-emoji">
{{ if reaction.EmojiId }} {{~ # Custom emoji ~}}
<img class="emoji emoji--small" title="{{ reaction.EmojiName }}" src="https://cdn.discordapp.com/emojis/{{ reaction.EmojiId }}.png" /> {{~ if reaction.EmojiId ~}}
{{~ # Default emoji }} <img class="emoji emoji--small" title="{{ reaction.EmojiName }}" src="https://cdn.discordapp.com/emojis/{{ reaction.EmojiId }}.png" />
{{ else }} {{~ # Default emoji ~}}
{{ reaction.EmojiName }} {{~ else ~}}
{{ end }} {{ reaction.EmojiName }}
</span> {{~ end ~}}
<span class="chatlog__reaction-count">{{ reaction.Count }}</span> </span>
</div> <span class="chatlog__reaction-count">{{ reaction.Count }}</span>
{{ end }} </div>
</div> {{~ end ~}}
{{ end }} </div>
{{~ end ~}}
{{~ end ~}}
</div> </div>
</div> </div>
{{ end }} {{~ end ~}}
</div> </div>
</body> </body>
@@ -1,18 +1,25 @@
============================================================== {{~ # Info ~}}
Guild: {{ Guild.Name }} ==============================================================
Channel: {{ Channel.Name }} Guild: {{ Model.Guild.Name }}
Topic: {{ Channel.Topic }} Channel: {{ Model.Channel.Name }}
Messages: {{ TotalMessageCount | Format "N0" }} Topic: {{ Model.Channel.Topic }}
Range: {{ if From }}{{ From | FormatDate }} {{ end }}{{ if From || To }}->{{ end }}{{ if To }} {{ To | FormatDate }}{{ end }} 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 }}
============================================================== ==============================================================
{{~ for group in MessageGroups ~}} {{~ # Log ~}}
{{~ group.Author.FullName }} [{{ group.TimeStamp | FormatDate }}] {{~ for group in Model.MessageGroups ~}}
{{~ # Author name and timestamp ~}}
{{~ }}[{{ group.Timestamp | FormatDate }}] {{ group.Author.FullName }}
{{~ # Messages ~}}
{{~ for message in group.Messages ~}} {{~ for message in group.Messages ~}}
{{~ # Content ~}}
{{~ message.Content | FormatContent }} {{~ message.Content | FormatContent }}
{{~ # Attachments ~}}
{{~ for attachment in message.Attachments ~}} {{~ for attachment in message.Attachments ~}}
{{~ attachment.Url }} {{~ attachment.Url }}
{{~ end ~}} {{~ end ~}}
{{~ end ~}} {{~ end ~}}
{{~ end ~}} {{~ end ~}}
@@ -2,12 +2,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks; using System.Threading.Tasks;
using DiscordChatExporter.Core.Exceptions; using DiscordChatExporter.Core.Exceptions;
using DiscordChatExporter.Core.Models; using DiscordChatExporter.Core.Models;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using DiscordChatExporter.Core.Internal; using DiscordChatExporter.Core.Internal;
using Polly; using Polly;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Core.Services namespace DiscordChatExporter.Core.Services
{ {
@@ -15,17 +17,9 @@ namespace DiscordChatExporter.Core.Services
{ {
private readonly HttpClient _httpClient = new HttpClient(); 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) 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 // Create request policy
var policy = Policy var policy = Policy
.Handle<HttpErrorStatusCodeException>(e => (int) e.StatusCode == 429) .Handle<HttpErrorStatusCodeException>(e => (int) e.StatusCode == 429)
@@ -34,23 +28,45 @@ namespace DiscordChatExporter.Core.Services
// Send request // Send request
return await policy.ExecuteAsync(async () => 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 // Add url parameter for the user token
// We throw our own exception here because default one doesn't have status code if (token.Type == AuthTokenType.User)
if (!response.IsSuccessStatusCode) request.RequestUri = request.RequestUri.SetQueryParameter("token", token.Value);
throw new HttpErrorStatusCodeException(response.StatusCode, response.ReasonPhrase);
// Get content // Add header for the bot token
var raw = await response.Content.ReadAsStringAsync(); if (token.Type == AuthTokenType.Bot)
request.Headers.Authorization = new AuthenticationHeaderValue("Bot", token.Value);
// Parse // Add parameters
return JToken.Parse(raw); 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 response = await GetApiResponseAsync(token, "guilds", guildId);
var guild = ParseGuild(response); var guild = ParseGuild(response);
@@ -58,7 +74,7 @@ namespace DiscordChatExporter.Core.Services
return guild; 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 response = await GetApiResponseAsync(token, "channels", channelId);
var channel = ParseChannel(response); var channel = ParseChannel(response);
@@ -66,7 +82,7 @@ namespace DiscordChatExporter.Core.Services
return channel; 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 response = await GetApiResponseAsync(token, "users", "@me/guilds", "limit=100");
var guilds = response.Select(ParseGuild).ToArray(); var guilds = response.Select(ParseGuild).ToArray();
@@ -74,7 +90,7 @@ namespace DiscordChatExporter.Core.Services
return guilds; 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 response = await GetApiResponseAsync(token, "users", "@me/channels");
var channels = response.Select(ParseChannel).ToArray(); var channels = response.Select(ParseChannel).ToArray();
@@ -82,7 +98,7 @@ namespace DiscordChatExporter.Core.Services
return channels; 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 response = await GetApiResponseAsync(token, "guilds", $"{guildId}/channels");
var channels = response.Select(ParseChannel).ToArray(); var channels = response.Select(ParseChannel).ToArray();
@@ -90,7 +106,7 @@ namespace DiscordChatExporter.Core.Services
return channels; 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 response = await GetApiResponseAsync(token, "guilds", $"{guildId}/roles");
var roles = response.Select(ParseRole).ToArray(); var roles = response.Select(ParseRole).ToArray();
@@ -98,7 +114,7 @@ namespace DiscordChatExporter.Core.Services
return roles; 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) DateTime? from = null, DateTime? to = null, IProgress<double> progress = null)
{ {
var result = new List<Message>(); var result = new List<Message>();
@@ -169,7 +185,7 @@ namespace DiscordChatExporter.Core.Services
return result; return result;
} }
public async Task<Mentionables> GetMentionablesAsync(string token, string guildId, public async Task<Mentionables> GetMentionablesAsync(AuthToken token, string guildId,
IEnumerable<Message> messages) IEnumerable<Message> messages)
{ {
// Get channels and roles // Get channels and roles
@@ -28,8 +28,6 @@ namespace DiscordChatExporter.Core.Services
private string HtmlEncode(string str) => WebUtility.HtmlEncode(str); private string HtmlEncode(string str) => WebUtility.HtmlEncode(str);
private string HtmlDecode(string str) => WebUtility.HtmlDecode(str);
private string Format(IFormattable obj, string format) => private string Format(IFormattable obj, string format) =>
obj.ToString(format, CultureInfo.InvariantCulture); obj.ToString(format, CultureInfo.InvariantCulture);
@@ -309,11 +307,9 @@ namespace DiscordChatExporter.Core.Services
var scriptObject = new ScriptObject(); var scriptObject = new ScriptObject();
// Import chat log // Import chat log
scriptObject.Import(_log, TemplateMemberFilter, TemplateMemberRenamer); scriptObject.SetValue("Model", _log, true);
// Import functions // Import functions
scriptObject.Import(nameof(HtmlEncode), new Func<string, string>(HtmlEncode));
scriptObject.Import(nameof(HtmlDecode), new Func<string, string>(HtmlDecode));
scriptObject.Import(nameof(Format), new Func<IFormattable, string, string>(Format)); scriptObject.Import(nameof(Format), new Func<IFormattable, string, string>(Format));
scriptObject.Import(nameof(FormatDate), new Func<DateTime, string>(FormatDate)); scriptObject.Import(nameof(FormatDate), new Func<DateTime, string>(FormatDate));
scriptObject.Import(nameof(FormatFileSize), new Func<long, string>(FormatFileSize)); scriptObject.Import(nameof(FormatFileSize), new Func<long, string>(FormatFileSize));
@@ -7,9 +7,6 @@ namespace DiscordChatExporter.Core.Services
{ {
public partial class ExportService : IExportService public partial class ExportService : IExportService
{ {
private static readonly MemberRenamerDelegate TemplateMemberRenamer = m => m.Name;
private static readonly MemberFilterDelegate TemplateMemberFilter = m => true;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
public ExportService(ISettingsService settingsService) public ExportService(ISettingsService settingsService)
@@ -30,8 +27,10 @@ namespace DiscordChatExporter.Core.Services
var context = new TemplateContext var context = new TemplateContext
{ {
TemplateLoader = loader, TemplateLoader = loader,
MemberRenamer = TemplateMemberRenamer, MemberRenamer = m => m.Name,
MemberFilter = TemplateMemberFilter MemberFilter = m => true,
LoopLimit = int.MaxValue,
StrictVariables = true
}; };
// Create template model // Create template model
@@ -7,22 +7,22 @@ namespace DiscordChatExporter.Core.Services
{ {
public interface IDataService 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); 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); IEnumerable<Message> messages);
} }
} }
@@ -9,7 +9,7 @@ namespace DiscordChatExporter.Core.Services
string DateFormat { get; set; } string DateFormat { get; set; }
int MessageGroupLimit { get; set; } int MessageGroupLimit { get; set; }
string LastToken { get; set; } AuthToken LastToken { get; set; }
ExportFormat LastExportFormat { get; set; } ExportFormat LastExportFormat { get; set; }
void Load(); void Load();
@@ -10,7 +10,7 @@ namespace DiscordChatExporter.Core.Services
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt"; public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
public int MessageGroupLimit { get; set; } = 20; public int MessageGroupLimit { get; set; } = 20;
public string LastToken { get; set; } public AuthToken LastToken { get; set; }
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark; public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
public SettingsService() public SettingsService()
+15
View File
@@ -96,6 +96,21 @@
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}" /> <Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}" />
</Style> </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 -->
<converters:ExportFormatToStringConverter x:Key="ExportFormatToStringConverter" /> <converters:ExportFormatToStringConverter x:Key="ExportFormatToStringConverter" />
@@ -3,5 +3,5 @@
[assembly: AssemblyTitle("DiscordChatExporter")] [assembly: AssemblyTitle("DiscordChatExporter")]
[assembly: AssemblyCompany("Tyrrrz")] [assembly: AssemblyCompany("Tyrrrz")]
[assembly: AssemblyCopyright("Copyright (c) 2017-2018 Alexey Golub")] [assembly: AssemblyCopyright("Copyright (c) 2017-2018 Alexey Golub")]
[assembly: AssemblyVersion("2.5")] [assembly: AssemblyVersion("2.6")]
[assembly: AssemblyFileVersion("2.5")] [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 public ExportFormat SelectedFormat
{ {
@@ -69,9 +70,6 @@ namespace DiscordChatExporter.Gui.ViewModels
{ {
_settingsService = settingsService; _settingsService = settingsService;
// Defaults
AvailableFormats = Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray();
// Commands // Commands
ExportCommand = new RelayCommand(Export, () => FilePath.IsNotBlank()); ExportCommand = new RelayCommand(Export, () => FilePath.IsNotBlank());
@@ -12,7 +12,8 @@ namespace DiscordChatExporter.Gui.ViewModels
bool IsProgressIndeterminate { get; } bool IsProgressIndeterminate { get; }
double Progress { get; } double Progress { get; }
string Token { get; set; } bool IsBotToken { get; set; }
string TokenValue { get; set; }
IReadOnlyList<Guild> AvailableGuilds { get; } IReadOnlyList<Guild> AvailableGuilds { get; }
Guild SelectedGuild { get; set; } Guild SelectedGuild { get; set; }
@@ -26,7 +26,8 @@ namespace DiscordChatExporter.Gui.ViewModels
private bool _isBusy; private bool _isBusy;
private double _progress; private double _progress;
private string _token; private bool _isBotToken;
private string _tokenValue;
private IReadOnlyList<Guild> _availableGuilds; private IReadOnlyList<Guild> _availableGuilds;
private Guild _selectedGuild; private Guild _selectedGuild;
private IReadOnlyList<Channel> _availableChannels; 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 set
{ {
// Remove invalid chars // Remove invalid chars
value = value?.Trim('"'); value = value?.Trim('"');
Set(ref _token, value); Set(ref _tokenValue, value);
PullDataCommand.RaiseCanExecuteChanged(); PullDataCommand.RaiseCanExecuteChanged();
} }
} }
@@ -117,7 +124,7 @@ namespace DiscordChatExporter.Gui.ViewModels
// Commands // Commands
ViewLoadedCommand = new RelayCommand(ViewLoaded); ViewLoadedCommand = new RelayCommand(ViewLoaded);
ViewClosedCommand = new RelayCommand(ViewClosed); ViewClosedCommand = new RelayCommand(ViewClosed);
PullDataCommand = new RelayCommand(PullData, () => Token.IsNotBlank() && !IsBusy); PullDataCommand = new RelayCommand(PullData, () => TokenValue.IsNotBlank() && !IsBusy);
ShowSettingsCommand = new RelayCommand(ShowSettings); ShowSettingsCommand = new RelayCommand(ShowSettings);
ShowAboutCommand = new RelayCommand(ShowAbout); ShowAboutCommand = new RelayCommand(ShowAbout);
ShowExportSetupCommand = new RelayCommand<Channel>(ShowExportSetup, _ => !IsBusy); ShowExportSetupCommand = new RelayCommand<Channel>(ShowExportSetup, _ => !IsBusy);
@@ -132,8 +139,12 @@ namespace DiscordChatExporter.Gui.ViewModels
// Load settings // Load settings
_settingsService.Load(); _settingsService.Load();
// Set last token // Get last token
Token = _settingsService.LastToken; if (_settingsService.LastToken != null)
{
IsBotToken = _settingsService.LastToken.Type == AuthTokenType.Bot;
TokenValue = _settingsService.LastToken.Value;
}
// Check and prepare update // Check and prepare update
try try
@@ -169,8 +180,10 @@ namespace DiscordChatExporter.Gui.ViewModels
{ {
IsBusy = true; IsBusy = true;
// Copy token so it doesn't get mutated // Create token
var token = Token; var token = new AuthToken(
IsBotToken ? AuthTokenType.Bot : AuthTokenType.User,
TokenValue);
// Save token // Save token
_settingsService.LastToken = token; _settingsService.LastToken = token;
+89 -38
View File
@@ -9,7 +9,7 @@
Height="550" Height="550"
Background="{DynamicResource MaterialDesignPaper}" Background="{DynamicResource MaterialDesignPaper}"
DataContext="{Binding MainViewModel, Source={StaticResource Container}}" DataContext="{Binding MainViewModel, Source={StaticResource Container}}"
FocusManager.FocusedElement="{Binding ElementName=TokenTextBox}" FocusManager.FocusedElement="{Binding ElementName=TokenValueTextBox}"
FontFamily="{DynamicResource MaterialDesignFont}" FontFamily="{DynamicResource MaterialDesignFont}"
Icon="/DiscordChatExporter;component/favicon.ico" Icon="/DiscordChatExporter;component/favicon.ico"
SnapsToDevicePixels="True" SnapsToDevicePixels="True"
@@ -48,27 +48,47 @@
Margin="6,6,0,6"> Margin="6,6,0,6">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Token --> <!-- Token type -->
<TextBox <ToggleButton
x:Name="TokenTextBox"
Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Margin="6" 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:HintAssist.Hint="Token"
materialDesign:TextFieldAssist.DecorationVisibility="Hidden" materialDesign:TextFieldAssist.DecorationVisibility="Hidden"
materialDesign:TextFieldAssist.TextBoxViewMargin="0,0,2,0" materialDesign:TextFieldAssist.TextBoxViewMargin="0,0,2,0"
BorderThickness="0" BorderThickness="0"
FontSize="16" FontSize="16"
Text="{Binding Token, UpdateSourceTrigger=PropertyChanged}" /> Text="{Binding TokenValue, UpdateSourceTrigger=PropertyChanged}" />
<!-- Pull data button --> <!-- Pull data button -->
<Button <Button
Grid.Row="0" Grid.Column="2"
Grid.Column="1"
Margin="0,6,6,6" Margin="0,6,6,6"
Padding="4" Padding="4"
Command="{Binding PullDataCommand}" Command="{Binding PullDataCommand}"
@@ -99,8 +119,8 @@
<ProgressBar <ProgressBar
Background="Transparent" Background="Transparent"
IsIndeterminate="{Binding IsProgressIndeterminate}" 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> </StackPanel>
</Border> </Border>
@@ -196,34 +216,65 @@
</DockPanel> </DockPanel>
<!-- Usage instructions --> <!-- Usage instructions -->
<StackPanel Margin="32,32,8,8" Visibility="{Binding IsDataAvailable, Converter={StaticResource InvertBoolToVisibilityConverter}}"> <Grid Margin="32,32,8,8" Visibility="{Binding IsDataAvailable, Converter={StaticResource InvertBoolToVisibilityConverter}}">
<TextBlock FontSize="18" Text="DiscordChatExporter needs your authorization token to work." /> <!-- User token -->
<TextBlock <StackPanel Visibility="{Binding IsBotToken, Converter={StaticResource InvertBoolToVisibilityConverter}}">
Margin="0,8,0,0" <TextBlock FontSize="18" Text="DiscordChatExporter needs your user token to work." />
FontSize="16" <TextBlock
Text="To obtain it, follow these steps:" /> Margin="0,8,0,0"
<TextBlock Margin="8,0,0,0" FontSize="14"> FontSize="16"
<Run Text="1. Open the Discord app" /> Text="To obtain it, follow these steps:" />
<LineBreak /> <TextBlock Margin="8,0,0,0" FontSize="14">
<Run Text="2. Log in if you haven't" /> <Run Text="1. Open the Discord app" />
<LineBreak /> <LineBreak />
<Run Text="3. Press" /> <Run Text="2. Log in if you haven't" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Ctrl+Shift+I" /> <LineBreak />
<LineBreak /> <Run Text="3. Press" />
<Run Text="4. Navigate to" /> <Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Ctrl+Shift+I" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Application" /> <Run Text="to show developer tools" />
<Run Text="tab" /> <LineBreak />
<LineBreak /> <Run Text="4. Navigate to the" />
<Run Text="5. Expand" /> <Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Application" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Storage &gt; Local Storage &gt; https://discordapp.com" /> <Run Text="tab" />
<LineBreak /> <LineBreak />
<Run Text="6. Find" /> <Run Text="5. Expand" />
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="&quot;token&quot;" /> <Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Storage &gt; Local Storage &gt; https://discordapp.com" />
<Run Text="under key and copy the value" /> <LineBreak />
<LineBreak /> <Run Text="6. Find the" />
<Run Text="7. Paste the value in the textbox above" /> <Run Foreground="{DynamicResource PrimaryTextBrush}" Text="&quot;token&quot;" />
</TextBlock> <Run Text="key and copy its value" />
</StackPanel> <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" /> <materialDesign:Snackbar x:Name="Snackbar" />
</Grid> </Grid>
</DockPanel> </DockPanel>
+5 -16
View File
@@ -18,22 +18,11 @@ DiscordChatExporter can be used to export message history from a [Discord](https
## Features ## Features
- Intuitive GUI that displays available guilds and channels - Graphical and command line interfaces
- CLI as additional alternative to GUI - Supports both user tokens and bot tokens
- Date ranges to limit messages - Allows retrieving messages in specified date range
- Groups messages by author and time - Multiple export formats: HTML (dark/light), TXT and CSV
- Exports to a plain text file - Renders all message features including: markdown, attachments, embeds, emojis, mentions, etc
- 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
## Libraries used ## Libraries used