mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-09 07:30:28 +02:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97a36028b9 | |||
| f0d99676a3 | |||
| 8e38adae7e | |||
| d95bc37592 | |||
| 3f6354053c | |||
| a9bab60ba6 | |||
| 614bd8590d | |||
| bd9dc6455f | |||
| 0faa427970 | |||
| dd9da4831e | |||
| 0d816cee5d | |||
| f63ea41188 | |||
| 3572a21aad | |||
| 37ee0b8be3 | |||
| ed146bac22 |
@@ -1,3 +1,15 @@
|
|||||||
|
### v2.7 (14-Sep-2018)
|
||||||
|
|
||||||
|
- Updated instructions on how to obtain the user token.
|
||||||
|
- Expanded CLI with new commands: `channels` (get a list of channels in a guild), `dms` (get a list of DM channels), `guilds` (get a list of guilds), on top of `export` (export chatlog).
|
||||||
|
- Improved help text and error messages in CLI.
|
||||||
|
- In CLI, if the provided output file path is a directory, a file name will be generated and appended to it automatically.
|
||||||
|
|
||||||
|
### 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)
|
### v2.5.1 (01-Jul-2018)
|
||||||
|
|
||||||
- Fixed a bug that would prevent timestamps from rendering properly in CSV and PlainText exports.
|
- Fixed a bug that would prevent timestamps from rendering properly in CSV and PlainText exports.
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using System;
|
|
||||||
using DiscordChatExporter.Core.Models;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli
|
|
||||||
{
|
|
||||||
public class CliOptions
|
|
||||||
{
|
|
||||||
public string Token { get; set; }
|
|
||||||
|
|
||||||
public string ChannelId { get; set; }
|
|
||||||
|
|
||||||
public ExportFormat ExportFormat { get; set; }
|
|
||||||
|
|
||||||
public string FilePath { get; set; }
|
|
||||||
|
|
||||||
public DateTime? From { get; set; }
|
|
||||||
|
|
||||||
public DateTime? To { get; set; }
|
|
||||||
|
|
||||||
public string DateFormat { get; set; }
|
|
||||||
|
|
||||||
public int MessageGroupLimit { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using CommonServiceLocator;
|
using CommonServiceLocator;
|
||||||
using DiscordChatExporter.Cli.ViewModels;
|
|
||||||
using DiscordChatExporter.Core.Services;
|
using DiscordChatExporter.Core.Services;
|
||||||
using GalaSoft.MvvmLight.Ioc;
|
using GalaSoft.MvvmLight.Ioc;
|
||||||
|
|
||||||
@@ -7,15 +6,7 @@ namespace DiscordChatExporter.Cli
|
|||||||
{
|
{
|
||||||
public class Container
|
public class Container
|
||||||
{
|
{
|
||||||
public IMainViewModel MainViewModel => Resolve<IMainViewModel>();
|
public Container()
|
||||||
public ISettingsService SettingsService => Resolve<ISettingsService>();
|
|
||||||
|
|
||||||
private T Resolve<T>(string key = null)
|
|
||||||
{
|
|
||||||
return ServiceLocator.Current.GetInstance<T>(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Init()
|
|
||||||
{
|
{
|
||||||
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
|
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
|
||||||
SimpleIoc.Default.Reset();
|
SimpleIoc.Default.Reset();
|
||||||
@@ -25,13 +16,12 @@ namespace DiscordChatExporter.Cli
|
|||||||
SimpleIoc.Default.Register<IExportService, ExportService>();
|
SimpleIoc.Default.Register<IExportService, ExportService>();
|
||||||
SimpleIoc.Default.Register<IMessageGroupService, MessageGroupService>();
|
SimpleIoc.Default.Register<IMessageGroupService, MessageGroupService>();
|
||||||
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
||||||
|
SimpleIoc.Default.Register<IUpdateService, UpdateService>();
|
||||||
// View models
|
|
||||||
SimpleIoc.Default.Register<IMainViewModel, MainViewModel>(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Cleanup()
|
public T Resolve<T>(string key = null)
|
||||||
{
|
{
|
||||||
|
return ServiceLocator.Current.GetInstance<T>(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,15 +3,15 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net461</TargetFramework>
|
<TargetFramework>net461</TargetFramework>
|
||||||
<Version>2.5.1</Version>
|
<Version>2.7</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>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommandLineParser" Version="2.2.1" />
|
||||||
<PackageReference Include="CommonServiceLocator" Version="2.0.3" />
|
<PackageReference Include="CommonServiceLocator" Version="2.0.3" />
|
||||||
<PackageReference Include="FluentCommandLineParser" Version="1.4.3" />
|
|
||||||
<PackageReference Include="MvvmLightLibs" Version="5.4.1" />
|
<PackageReference Include="MvvmLightLibs" Version="5.4.1" />
|
||||||
<PackageReference Include="Tyrrrz.Extensions" Version="1.5.1" />
|
<PackageReference Include="Tyrrrz.Extensions" Version="1.5.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,111 +1,73 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Linq;
|
||||||
using DiscordChatExporter.Core.Models;
|
using CommandLine;
|
||||||
using Fclp;
|
using DiscordChatExporter.Cli.Verbs;
|
||||||
using Tyrrrz.Extensions;
|
using DiscordChatExporter.Cli.Verbs.Options;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli
|
namespace DiscordChatExporter.Cli
|
||||||
{
|
{
|
||||||
public static class Program
|
public static class Program
|
||||||
{
|
{
|
||||||
private static readonly Container Container = new Container();
|
private static void PrintTokenHelp()
|
||||||
|
|
||||||
private static void ShowHelp()
|
|
||||||
{
|
{
|
||||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
Console.WriteLine("# To get user token:");
|
||||||
var availableFormats = Enum.GetNames(typeof(ExportFormat));
|
Console.WriteLine(" 1. Open Discord app");
|
||||||
|
Console.WriteLine(" 2. Log in if you haven't");
|
||||||
Console.WriteLine($"=== Discord Chat Exporter (Command Line Interface) v{version} ===");
|
Console.WriteLine(" 3. Press Ctrl+Shift+I to show developer tools");
|
||||||
|
Console.WriteLine(" 4. Press Ctrl+R to trigger reload");
|
||||||
|
Console.WriteLine(" 5. Navigate to the Application tab");
|
||||||
|
Console.WriteLine(" 6. Select \"Local Storage\" > \"https://discordapp.com\" on the left");
|
||||||
|
Console.WriteLine(" 7. Find \"token\" under key and copy the value");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine("[-t] [--token] Discord authorization token.");
|
Console.WriteLine("# To get bot token:");
|
||||||
Console.WriteLine("[-c] [--channel] Discord channel ID.");
|
Console.WriteLine(" 1. Go to Discord developer portal");
|
||||||
Console.WriteLine("[-f] [--format] Export format. Optional.");
|
Console.WriteLine(" 2. Log in if you haven't");
|
||||||
Console.WriteLine("[-o] [--output] Output file path. Optional.");
|
Console.WriteLine(" 3. Open your application's settings");
|
||||||
Console.WriteLine(" [--datefrom] Limit to messages after this date. Optional.");
|
Console.WriteLine(" 4. Navigate to the Bot section on the left");
|
||||||
Console.WriteLine(" [--dateto] Limit to messages before this date. Optional.");
|
Console.WriteLine(" 5. Under Token click Copy");
|
||||||
Console.WriteLine(" [--dateformat] Date format. Optional.");
|
|
||||||
Console.WriteLine(" [--grouplimit] Message group limit. Optional.");
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine($"Available export formats: {availableFormats.JoinToString(", ")}");
|
Console.WriteLine("# To get guild or channel ID:");
|
||||||
Console.WriteLine();
|
Console.WriteLine(" 1. Open Discord app");
|
||||||
Console.WriteLine("# To get authorization token:");
|
Console.WriteLine(" 2. Log in if you haven't");
|
||||||
Console.WriteLine(" - Open Discord app");
|
Console.WriteLine(" 3. Open Settings");
|
||||||
Console.WriteLine(" - Log in if you haven't");
|
Console.WriteLine(" 4. Go to Appearance section");
|
||||||
Console.WriteLine(" - Press Ctrl+Shift+I");
|
Console.WriteLine(" 5. Enable Developer Mode");
|
||||||
Console.WriteLine(" - Navigate to Application tab");
|
Console.WriteLine(" 6. Right click on the desired guild or channel and click Copy ID");
|
||||||
Console.WriteLine(" - Expand Storage > Local Storage > https://discordapp.com");
|
|
||||||
Console.WriteLine(" - Find \"token\" under key and copy the value");
|
|
||||||
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(" - Type \"document.URL\" and press Enter");
|
|
||||||
Console.WriteLine(" - Copy the long sequence of numbers after last slash");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CliOptions ParseOptions(string[] args)
|
|
||||||
{
|
|
||||||
var argsParser = new FluentCommandLineParser<CliOptions>();
|
|
||||||
|
|
||||||
var settings = Container.SettingsService;
|
|
||||||
|
|
||||||
argsParser.Setup(o => o.Token).As('t', "token").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.FilePath).As('o', "output").SetDefault(null);
|
|
||||||
argsParser.Setup(o => o.From).As("datefrom").SetDefault(null);
|
|
||||||
argsParser.Setup(o => o.To).As("dateto").SetDefault(null);
|
|
||||||
argsParser.Setup(o => o.DateFormat).As("dateformat").SetDefault(settings.DateFormat);
|
|
||||||
argsParser.Setup(o => o.MessageGroupLimit).As("grouplimit").SetDefault(settings.MessageGroupLimit);
|
|
||||||
|
|
||||||
var parsed = argsParser.Parse(args);
|
|
||||||
|
|
||||||
// Show help if no arguments
|
|
||||||
if (parsed.EmptyArgs)
|
|
||||||
{
|
|
||||||
ShowHelp();
|
|
||||||
Environment.Exit(0);
|
|
||||||
}
|
|
||||||
// Show error if there are any
|
|
||||||
else if (parsed.HasErrors)
|
|
||||||
{
|
|
||||||
Console.Error.Write(parsed.ErrorText);
|
|
||||||
Environment.Exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return argsParser.Object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
// Init container
|
// Get all verb types
|
||||||
Container.Init();
|
var verbTypes = new[]
|
||||||
|
{
|
||||||
|
typeof(ExportChatOptions),
|
||||||
|
typeof(GetChannelsOptions),
|
||||||
|
typeof(GetDirectMessageChannelsOptions),
|
||||||
|
typeof(GetGuildsOptions),
|
||||||
|
//typeof(UpdateAppOptions)
|
||||||
|
};
|
||||||
|
|
||||||
// Parse options
|
// Parse command line arguments
|
||||||
var options = ParseOptions(args);
|
var parsedArgs = Parser.Default.ParseArguments(args, verbTypes);
|
||||||
|
|
||||||
// Inject some settings
|
// Execute commands
|
||||||
var settings = Container.SettingsService;
|
parsedArgs.WithParsed<ExportChatOptions>(o => new ExportChatVerb(o).Execute());
|
||||||
settings.DateFormat = options.DateFormat;
|
parsedArgs.WithParsed<GetChannelsOptions>(o => new GetChannelsVerb(o).Execute());
|
||||||
settings.MessageGroupLimit = options.MessageGroupLimit;
|
parsedArgs.WithParsed<GetDirectMessageChannelsOptions>(o => new GetDirectMessageChannelsVerb(o).Execute());
|
||||||
|
parsedArgs.WithParsed<GetGuildsOptions>(o => new GetGuildsVerb(o).Execute());
|
||||||
|
parsedArgs.WithParsed<UpdateAppOptions>(o => new UpdateAppVerb(o).Execute());
|
||||||
|
|
||||||
// Export
|
// Show token help if help requested or no verb specified
|
||||||
var vm = Container.MainViewModel;
|
parsedArgs.WithNotParsed(errs =>
|
||||||
vm.ExportAsync(
|
{
|
||||||
options.Token,
|
var err = errs.First();
|
||||||
options.ChannelId,
|
|
||||||
options.FilePath,
|
|
||||||
options.ExportFormat,
|
|
||||||
options.From,
|
|
||||||
options.To).GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
// Cleanup container
|
if (err.Tag == ErrorType.NoVerbSelectedError)
|
||||||
Container.Cleanup();
|
PrintTokenHelp();
|
||||||
|
|
||||||
Console.WriteLine("Export complete.");
|
if (err.Tag == ErrorType.HelpVerbRequestedError && args.Length == 1)
|
||||||
|
PrintTokenHelp();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Cli.Verbs.Options;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
using DiscordChatExporter.Core.Services;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs
|
||||||
|
{
|
||||||
|
public class ExportChatVerb : Verb<ExportChatOptions>
|
||||||
|
{
|
||||||
|
public ExportChatVerb(ExportChatOptions options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ExecuteAsync()
|
||||||
|
{
|
||||||
|
// Get services
|
||||||
|
var container = new Container();
|
||||||
|
var settingsService = container.Resolve<ISettingsService>();
|
||||||
|
var dataService = container.Resolve<IDataService>();
|
||||||
|
var messageGroupService = container.Resolve<IMessageGroupService>();
|
||||||
|
var exportService = container.Resolve<IExportService>();
|
||||||
|
|
||||||
|
// Configure settings
|
||||||
|
if (Options.DateFormat.IsNotBlank())
|
||||||
|
settingsService.DateFormat = Options.DateFormat;
|
||||||
|
if (Options.MessageGroupLimit > 0)
|
||||||
|
settingsService.MessageGroupLimit = Options.MessageGroupLimit;
|
||||||
|
|
||||||
|
// Get channel and guild
|
||||||
|
var channel = await dataService.GetChannelAsync(Options.GetToken(), Options.ChannelId);
|
||||||
|
var guild = channel.GuildId == Guild.DirectMessages.Id
|
||||||
|
? Guild.DirectMessages
|
||||||
|
: await dataService.GetGuildAsync(Options.GetToken(), channel.GuildId);
|
||||||
|
|
||||||
|
// Generate file path if not set
|
||||||
|
var filePath = Options.FilePath;
|
||||||
|
if (filePath == null || filePath.EndsWith("/") || filePath.EndsWith("\\"))
|
||||||
|
{
|
||||||
|
filePath += $"{guild.Name} - {channel.Name}.{Options.ExportFormat.GetFileExtension()}"
|
||||||
|
.Replace(Path.GetInvalidFileNameChars(), '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: extract this to make it reusable across implementations
|
||||||
|
// Get messages
|
||||||
|
var messages =
|
||||||
|
await dataService.GetChannelMessagesAsync(Options.GetToken(), channel.Id,
|
||||||
|
Options.After, Options.Before);
|
||||||
|
|
||||||
|
// Group messages
|
||||||
|
var messageGroups = messageGroupService.GroupMessages(messages);
|
||||||
|
|
||||||
|
// Get mentionables
|
||||||
|
var mentionables = await dataService.GetMentionablesAsync(Options.GetToken(), guild.Id, messages);
|
||||||
|
|
||||||
|
// Create log
|
||||||
|
var log = new ChatLog(guild, channel, Options.After, Options.Before, messageGroups, mentionables);
|
||||||
|
|
||||||
|
// Export
|
||||||
|
exportService.Export(Options.ExportFormat, filePath, log);
|
||||||
|
|
||||||
|
// Print result
|
||||||
|
Console.WriteLine($"Exported chat to [{filePath}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Cli.Verbs.Options;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
using DiscordChatExporter.Core.Services;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs
|
||||||
|
{
|
||||||
|
public class GetChannelsVerb : Verb<GetChannelsOptions>
|
||||||
|
{
|
||||||
|
public GetChannelsVerb(GetChannelsOptions options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ExecuteAsync()
|
||||||
|
{
|
||||||
|
// Get data service
|
||||||
|
var container = new Container();
|
||||||
|
var dataService = container.Resolve<IDataService>();
|
||||||
|
|
||||||
|
// Get channels
|
||||||
|
var channels = await dataService.GetGuildChannelsAsync(Options.GetToken(), Options.GuildId);
|
||||||
|
|
||||||
|
// Print result
|
||||||
|
foreach (var channel in channels.Where(c => c.Type.IsEither(ChannelType.GuildTextChat))
|
||||||
|
.OrderBy(c => c.Name))
|
||||||
|
Console.WriteLine($"{channel.Id} | {channel.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Cli.Verbs.Options;
|
||||||
|
using DiscordChatExporter.Core.Services;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs
|
||||||
|
{
|
||||||
|
public class GetDirectMessageChannelsVerb : Verb<GetDirectMessageChannelsOptions>
|
||||||
|
{
|
||||||
|
public GetDirectMessageChannelsVerb(GetDirectMessageChannelsOptions options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ExecuteAsync()
|
||||||
|
{
|
||||||
|
// Get data service
|
||||||
|
var container = new Container();
|
||||||
|
var dataService = container.Resolve<IDataService>();
|
||||||
|
|
||||||
|
// Get channels
|
||||||
|
var channels = await dataService.GetDirectMessageChannelsAsync(Options.GetToken());
|
||||||
|
|
||||||
|
// Print result
|
||||||
|
foreach (var channel in channels.OrderBy(c => c.Name))
|
||||||
|
Console.WriteLine($"{channel.Id} | {channel.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Cli.Verbs.Options;
|
||||||
|
using DiscordChatExporter.Core.Services;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs
|
||||||
|
{
|
||||||
|
public class GetGuildsVerb : Verb<GetGuildsOptions>
|
||||||
|
{
|
||||||
|
public GetGuildsVerb(GetGuildsOptions options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ExecuteAsync()
|
||||||
|
{
|
||||||
|
// Get data service
|
||||||
|
var container = new Container();
|
||||||
|
var dataService = container.Resolve<IDataService>();
|
||||||
|
|
||||||
|
// Get guilds
|
||||||
|
var guilds = await dataService.GetUserGuildsAsync(Options.GetToken());
|
||||||
|
|
||||||
|
// Print result
|
||||||
|
foreach (var guild in guilds.OrderBy(g => g.Name))
|
||||||
|
Console.WriteLine($"{guild.Id} | {guild.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using CommandLine;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs.Options
|
||||||
|
{
|
||||||
|
[Verb("export", HelpText = "Export channel chat log to a file.")]
|
||||||
|
public class ExportChatOptions : TokenOptions
|
||||||
|
{
|
||||||
|
[Option('c', "channel", Required = true, HelpText = "Channel ID.")]
|
||||||
|
public string ChannelId { get; set; }
|
||||||
|
|
||||||
|
[Option('f', "format", Default = ExportFormat.HtmlDark, HelpText = "Output file format.")]
|
||||||
|
public ExportFormat ExportFormat { get; set; }
|
||||||
|
|
||||||
|
[Option('o', "output", Default = null, HelpText = "Output file path.")]
|
||||||
|
public string FilePath { get; set; }
|
||||||
|
|
||||||
|
[Option("after", Default = null, HelpText = "Limit to messages sent after this date.")]
|
||||||
|
public DateTime? After { get; set; }
|
||||||
|
|
||||||
|
[Option("before", Default = null, HelpText = "Limit to messages sent before this date.")]
|
||||||
|
public DateTime? Before { get; set; }
|
||||||
|
|
||||||
|
[Option("dateformat", Default = null, HelpText = "Date format used in output.")]
|
||||||
|
public string DateFormat { get; set; }
|
||||||
|
|
||||||
|
[Option("grouplimit", Default = 0, HelpText = "Message group limit.")]
|
||||||
|
public int MessageGroupLimit { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using CommandLine;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs.Options
|
||||||
|
{
|
||||||
|
[Verb("channels", HelpText = "Get the list of channels in the given guild.")]
|
||||||
|
public class GetChannelsOptions : TokenOptions
|
||||||
|
{
|
||||||
|
[Option('g', "guild", Required = true, HelpText = "Guild ID.")]
|
||||||
|
public string GuildId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using CommandLine;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs.Options
|
||||||
|
{
|
||||||
|
[Verb("dm", HelpText = "Get the list of direct message channels.")]
|
||||||
|
public class GetDirectMessageChannelsOptions : TokenOptions
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using CommandLine;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs.Options
|
||||||
|
{
|
||||||
|
[Verb("guilds", HelpText = "Get the list of accessible guilds.")]
|
||||||
|
public class GetGuildsOptions : TokenOptions
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs.Options
|
||||||
|
{
|
||||||
|
public class TokenOptions
|
||||||
|
{
|
||||||
|
[Option('t', "token", Required = true, HelpText = "Authorization token.")]
|
||||||
|
public string TokenValue { get; set; }
|
||||||
|
|
||||||
|
[Option('b', "bot", Default = false, HelpText = "Whether this authorization token belongs to a bot.")]
|
||||||
|
public bool IsBotToken { get; set; }
|
||||||
|
|
||||||
|
public AuthToken GetToken() => new AuthToken(IsBotToken ? AuthTokenType.Bot : AuthTokenType.User, TokenValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using CommandLine;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs.Options
|
||||||
|
{
|
||||||
|
[Verb("update", HelpText = "Updates this application to the latest version.")]
|
||||||
|
public class UpdateAppOptions
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Cli.Verbs.Options;
|
||||||
|
using DiscordChatExporter.Core.Services;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs
|
||||||
|
{
|
||||||
|
public class UpdateAppVerb : Verb<UpdateAppOptions>
|
||||||
|
{
|
||||||
|
public UpdateAppVerb(UpdateAppOptions options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ExecuteAsync()
|
||||||
|
{
|
||||||
|
// Get update service
|
||||||
|
var container = new Container();
|
||||||
|
var updateService = container.Resolve<IUpdateService>();
|
||||||
|
|
||||||
|
// TODO: this is configured only for GUI
|
||||||
|
// Get update version
|
||||||
|
var updateVersion = await updateService.CheckPrepareUpdateAsync();
|
||||||
|
|
||||||
|
if (updateVersion != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Updating to version {updateVersion}");
|
||||||
|
|
||||||
|
updateService.NeedRestart = false;
|
||||||
|
updateService.FinalizeUpdate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("There are no application updates available.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Verbs
|
||||||
|
{
|
||||||
|
public abstract class Verb<TOptions>
|
||||||
|
{
|
||||||
|
protected TOptions Options { get; }
|
||||||
|
|
||||||
|
protected Verb(TOptions options)
|
||||||
|
{
|
||||||
|
Options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Task ExecuteAsync();
|
||||||
|
|
||||||
|
public virtual void Execute() => ExecuteAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using DiscordChatExporter.Core.Models;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.ViewModels
|
|
||||||
{
|
|
||||||
public interface IMainViewModel
|
|
||||||
{
|
|
||||||
Task ExportAsync(string token, string channelId, string filePath, ExportFormat format, DateTime? from,
|
|
||||||
DateTime? to);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using DiscordChatExporter.Core.Models;
|
|
||||||
using DiscordChatExporter.Core.Services;
|
|
||||||
using Tyrrrz.Extensions;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.ViewModels
|
|
||||||
{
|
|
||||||
public class MainViewModel : IMainViewModel
|
|
||||||
{
|
|
||||||
private readonly IDataService _dataService;
|
|
||||||
private readonly IMessageGroupService _messageGroupService;
|
|
||||||
private readonly IExportService _exportService;
|
|
||||||
|
|
||||||
public MainViewModel(IDataService dataService, IMessageGroupService messageGroupService,
|
|
||||||
IExportService exportService)
|
|
||||||
{
|
|
||||||
_dataService = dataService;
|
|
||||||
_messageGroupService = messageGroupService;
|
|
||||||
_exportService = exportService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ExportAsync(string token, string channelId, string filePath, ExportFormat format,
|
|
||||||
DateTime? from, DateTime? to)
|
|
||||||
{
|
|
||||||
// Get channel and guild
|
|
||||||
var channel = await _dataService.GetChannelAsync(token, channelId);
|
|
||||||
var guild = channel.GuildId == Guild.DirectMessages.Id
|
|
||||||
? Guild.DirectMessages
|
|
||||||
: await _dataService.GetGuildAsync(token, channel.GuildId);
|
|
||||||
|
|
||||||
// Generate file path if not set
|
|
||||||
if (filePath.IsBlank())
|
|
||||||
{
|
|
||||||
filePath = $"{guild.Name} - {channel.Name}.{format.GetFileExtension()}"
|
|
||||||
.Replace(Path.GetInvalidFileNameChars(), '_');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get messages
|
|
||||||
var messages = await _dataService.GetChannelMessagesAsync(token, channel.Id, from, to);
|
|
||||||
|
|
||||||
// Group messages
|
|
||||||
var messageGroups = _messageGroupService.GroupMessages(messages);
|
|
||||||
|
|
||||||
// Get mentionables
|
|
||||||
var mentionables = await _dataService.GetMentionablesAsync(token, guild.Id, messages);
|
|
||||||
|
|
||||||
// Create log
|
|
||||||
var log = new ChatLog(guild, channel, from, to, messageGroups, mentionables);
|
|
||||||
|
|
||||||
// Export
|
|
||||||
_exportService.Export(format, filePath, log);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net461</TargetFramework>
|
<TargetFramework>net461</TargetFramework>
|
||||||
<Version>2.5.1</Version>
|
<Version>2.7</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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,5 +1,5 @@
|
|||||||
Author;Date;Content;Attachments;
|
Author;Date;Content;Attachments;
|
||||||
{{~ for group in MessageGroups -}}
|
{{~ for group in Model.MessageGroups -}}
|
||||||
{{- for message in group.Messages -}}
|
{{- for message in group.Messages -}}
|
||||||
{{- message.Author.FullName }};
|
{{- 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">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<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 charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<style>
|
<style>
|
||||||
@@ -14,26 +14,26 @@
|
|||||||
{{~ # 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 | html.escape }}</div>
|
<div class="info__guild-name">{{ Model.Guild.Name | html.escape }}</div>
|
||||||
<div class="info__channel-name">{{ Channel.Name | html.escape }}</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 | html.escape }}</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 | html.escape }} and {{ To | FormatDate | html.escape }}
|
Between {{ Model.From | FormatDate | html.escape }} and {{ Model.To | FormatDate | html.escape }}
|
||||||
{{~ else if From ~}}
|
{{~ else if Model.From ~}}
|
||||||
After {{ From | FormatDate | html.escape }}
|
After {{ Model.From | FormatDate | html.escape }}
|
||||||
{{~ else if To ~}}
|
{{~ else if Model.To ~}}
|
||||||
Before {{ To | FormatDate | html.escape }}
|
Before {{ Model.To | FormatDate | html.escape }}
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
</div>
|
</div>
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
{{~ # 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">
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{{~ # Info ~}}
|
{{~ # Info ~}}
|
||||||
==============================================================
|
==============================================================
|
||||||
Guild: {{ Guild.Name }}
|
Guild: {{ Model.Guild.Name }}
|
||||||
Channel: {{ Channel.Name }}
|
Channel: {{ Model.Channel.Name }}
|
||||||
Topic: {{ Channel.Topic }}
|
Topic: {{ Model.Channel.Topic }}
|
||||||
Messages: {{ TotalMessageCount | Format "N0" }}
|
Messages: {{ Model.TotalMessageCount | Format "N0" }}
|
||||||
Range: {{ if From }}{{ From | FormatDate }} {{ end }}{{ if From || To }}->{{ end }}{{ if To }} {{ To | FormatDate }}{{ end }}
|
Range: {{ if Model.From }}{{ Model.From | FormatDate }} {{ end }}{{ if Model.From || Model.To }}->{{ end }}{{ if Model.To }} {{ Model.To | FormatDate }}{{ end }}
|
||||||
==============================================================
|
==============================================================
|
||||||
|
|
||||||
{{~ # Log ~}}
|
{{~ # Log ~}}
|
||||||
{{~ for group in MessageGroups ~}}
|
{{~ for group in Model.MessageGroups ~}}
|
||||||
{{~ # Author name and timestamp ~}}
|
{{~ # Author name and timestamp ~}}
|
||||||
{{~ }}[{{ group.Timestamp | FormatDate }}] {{ group.Author.FullName }}
|
{{~ }}[{{ group.Timestamp | FormatDate }}] {{ group.Author.FullName }}
|
||||||
{{~ # Messages ~}}
|
{{~ # Messages ~}}
|
||||||
|
|||||||
@@ -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,42 @@ 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
|
// Set authorization header
|
||||||
// We throw our own exception here because default one doesn't have status code
|
request.Headers.Authorization = token.Type == AuthTokenType.Bot
|
||||||
if (!response.IsSuccessStatusCode)
|
? new AuthenticationHeaderValue("Bot", token.Value)
|
||||||
throw new HttpErrorStatusCodeException(response.StatusCode, response.ReasonPhrase);
|
: new AuthenticationHeaderValue(token.Value);
|
||||||
|
|
||||||
// Get content
|
// Add parameters
|
||||||
var raw = await response.Content.ReadAsStringAsync();
|
foreach (var parameter in parameters)
|
||||||
|
{
|
||||||
|
var key = parameter.SubstringUntil("=");
|
||||||
|
var value = parameter.SubstringAfter("=");
|
||||||
|
request.RequestUri = request.RequestUri.SetQueryParameter(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
// Parse
|
// Get response
|
||||||
return JToken.Parse(raw);
|
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 +71,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 +79,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 +87,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 +95,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 +103,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 +111,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 +182,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
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ 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(Format), new Func<IFormattable, string, string>(Format));
|
scriptObject.Import(nameof(Format), new Func<IFormattable, string, string>(Format));
|
||||||
|
|||||||
@@ -2,14 +2,12 @@
|
|||||||
using DiscordChatExporter.Core.Models;
|
using DiscordChatExporter.Core.Models;
|
||||||
using Scriban;
|
using Scriban;
|
||||||
using Scriban.Runtime;
|
using Scriban.Runtime;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Services
|
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,14 +28,21 @@ 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
|
||||||
var templateModel = new TemplateModel(format, log, _settingsService.DateFormat);
|
var templateModel = new TemplateModel(format, log, _settingsService.DateFormat);
|
||||||
context.PushGlobal(templateModel.GetScriptObject());
|
context.PushGlobal(templateModel.GetScriptObject());
|
||||||
|
|
||||||
|
// Create directory
|
||||||
|
var dirPath = Path.GetDirectoryName(filePath);
|
||||||
|
if (dirPath.IsNotBlank())
|
||||||
|
Directory.CreateDirectory(dirPath);
|
||||||
|
|
||||||
// Render output
|
// Render output
|
||||||
using (var output = File.CreateText(filePath))
|
using (var output = File.CreateText(filePath))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||||
xmlns:local="clr-namespace:DiscordChatExporter.Gui"
|
xmlns:local="clr-namespace:DiscordChatExporter.Gui"
|
||||||
Exit="App_Exit"
|
|
||||||
Startup="App_Startup"
|
|
||||||
StartupUri="Views/MainWindow.xaml">
|
StartupUri="Views/MainWindow.xaml">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
@@ -96,6 +94,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" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,6 @@
|
|||||||
using System.Windows;
|
namespace DiscordChatExporter.Gui
|
||||||
|
|
||||||
namespace DiscordChatExporter.Gui
|
|
||||||
{
|
{
|
||||||
public partial class App
|
public partial class App
|
||||||
{
|
{
|
||||||
private Container Container => (Container) Resources["Container"];
|
|
||||||
|
|
||||||
private void App_Startup(object sender, StartupEventArgs e)
|
|
||||||
{
|
|
||||||
Container.Init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void App_Exit(object sender, ExitEventArgs e)
|
|
||||||
{
|
|
||||||
Container.Cleanup();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,12 +11,7 @@ namespace DiscordChatExporter.Gui
|
|||||||
public IMainViewModel MainViewModel => Resolve<IMainViewModel>();
|
public IMainViewModel MainViewModel => Resolve<IMainViewModel>();
|
||||||
public ISettingsViewModel SettingsViewModel => Resolve<ISettingsViewModel>();
|
public ISettingsViewModel SettingsViewModel => Resolve<ISettingsViewModel>();
|
||||||
|
|
||||||
private T Resolve<T>(string key = null)
|
public Container()
|
||||||
{
|
|
||||||
return ServiceLocator.Current.GetInstance<T>(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Init()
|
|
||||||
{
|
{
|
||||||
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
|
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
|
||||||
SimpleIoc.Default.Reset();
|
SimpleIoc.Default.Reset();
|
||||||
@@ -34,8 +29,9 @@ namespace DiscordChatExporter.Gui
|
|||||||
SimpleIoc.Default.Register<ISettingsViewModel, SettingsViewModel>(true);
|
SimpleIoc.Default.Register<ISettingsViewModel, SettingsViewModel>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Cleanup()
|
private T Resolve<T>(string key = null)
|
||||||
{
|
{
|
||||||
|
return ServiceLocator.Current.GetInstance<T>(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.1")]
|
[assembly: AssemblyVersion("2.7")]
|
||||||
[assembly: AssemblyFileVersion("2.5.1")]
|
[assembly: AssemblyFileVersion("2.7")]
|
||||||
@@ -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;
|
||||||
@@ -243,6 +256,7 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// TODO: extract this to make it reusable across implementations
|
||||||
// Get messages
|
// Get messages
|
||||||
var messages = await _dataService.GetChannelMessagesAsync(token, channel.Id, from, to, progressHandler);
|
var messages = await _dataService.GetChannelMessagesAsync(token, channel.Id, from, to, progressHandler);
|
||||||
|
|
||||||
|
|||||||
@@ -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,68 @@
|
|||||||
</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. Press" />
|
||||||
<Run Text="5. Expand" />
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Ctrl+R" />
|
||||||
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Storage > Local Storage > https://discordapp.com" />
|
<Run Text="to trigger reload" />
|
||||||
<LineBreak />
|
<LineBreak />
|
||||||
<Run Text="6. Find" />
|
<Run Text="5. Navigate to the" />
|
||||||
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text=""token"" />
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Application" />
|
||||||
<Run Text="under key and copy the value" />
|
<Run Text="tab" />
|
||||||
<LineBreak />
|
<LineBreak />
|
||||||
<Run Text="7. Paste the value in the textbox above" />
|
<Run Text="6. Select" />
|
||||||
</TextBlock>
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Local Storage" />
|
||||||
</StackPanel>
|
<Run Text=">" />
|
||||||
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="https://discordapp.com" />
|
||||||
|
<Run Text="on the left" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="7. Find" />
|
||||||
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="token" />
|
||||||
|
<Run Text="under key and copy the value" />
|
||||||
|
</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" />
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
<materialDesign:Snackbar x:Name="Snackbar" />
|
<materialDesign:Snackbar x:Name="Snackbar" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
FROM mono:5
|
||||||
|
WORKDIR /root/build
|
||||||
|
COPY DiscordChatExporter.sln favicon.ico ./
|
||||||
|
COPY DiscordChatExporter.Core DiscordChatExporter.Core
|
||||||
|
COPY DiscordChatExporter.Cli DiscordChatExporter.Cli
|
||||||
|
RUN msbuild ./DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj /t:Restore
|
||||||
|
RUN msbuild ./DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj /p:Configuration=Release
|
||||||
|
|
||||||
|
FROM mono:5
|
||||||
|
COPY --from=0 /root/build/DiscordChatExporter.Cli/bin/Release/net461 /root/bin
|
||||||
|
WORKDIR /a
|
||||||
|
ENTRYPOINT ["mono", "/root/bin/DiscordChatExporter.Cli.exe"]
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
[](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
[](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
||||||
[](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
[](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
||||||
|
|
||||||
DiscordChatExporter can be used to export message history from a [Discord](https://discordapp.com) channel to a file. It works for both direct message chats and guild chats, supports markdown, message grouping, and attachments. The tool also lets you select from/to dates to limit the exported messages. There are options to configure the output, such as date format, color theme, message grouping limit, etc.
|
DiscordChatExporter can be used to export message history from a [Discord](https://discordapp.com) channel to a file. It works for both direct message chats and guild chats, supports markdown, message grouping, embeds, attachments, mentions, reactions and other features. It works with both user and bot tokens, supports multiple output formats and allows you to trim messages by dates.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -15,25 +15,15 @@ DiscordChatExporter can be used to export message history from a [Discord](https
|
|||||||
|
|
||||||
- [Stable releases](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
- [Stable releases](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
||||||
- [Continuous integration](https://ci.appveyor.com/project/Tyrrrz/DiscordChatExporter)
|
- [Continuous integration](https://ci.appveyor.com/project/Tyrrrz/DiscordChatExporter)
|
||||||
|
- [Docker](https://hub.docker.com/r/tyrrrz/discordchatexporter): `docker pull tyrrrz/discordchatexporter` (only CLI version)
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
@@ -43,7 +33,7 @@ DiscordChatExporter can be used to export message history from a [Discord](https
|
|||||||
- [Scriban](https://github.com/lunet-io/scriban)
|
- [Scriban](https://github.com/lunet-io/scriban)
|
||||||
- [Polly](https://github.com/App-vNext/Polly)
|
- [Polly](https://github.com/App-vNext/Polly)
|
||||||
- [Onova](https://github.com/Tyrrrz/Onova)
|
- [Onova](https://github.com/Tyrrrz/Onova)
|
||||||
- [FluentCommandLineParser](https://github.com/fclp/fluent-command-line-parser)
|
- [CommandLineParser](https://github.com/commandlineparser/commandline)
|
||||||
- [Tyrrrz.Extensions](https://github.com/Tyrrrz/Extensions)
|
- [Tyrrrz.Extensions](https://github.com/Tyrrrz/Extensions)
|
||||||
- [Tyrrrz.WpfExtensions](https://github.com/Tyrrrz/WpfExtensions)
|
- [Tyrrrz.WpfExtensions](https://github.com/Tyrrrz/WpfExtensions)
|
||||||
- [Tyrrrz.Settings](https://github.com/Tyrrrz/Settings)
|
- [Tyrrrz.Settings](https://github.com/Tyrrrz/Settings)
|
||||||
|
|||||||
Reference in New Issue
Block a user