mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-08 07:04:40 +02:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b518fb2f59 | |||
| 94165dcd8d | |||
| d6507598fa | |||
| a0359b1e43 | |||
| aa53cecd4e | |||
| 4144911208 | |||
| d28e81f8dc | |||
| 761cb032d7 | |||
| 47f0561c71 | |||
| 23116b776b | |||
| ba27a9f909 | |||
| 52684c264f | |||
| bcad5b4ed1 | |||
| 95a4217ab3 | |||
| 4e8fb80ac5 | |||
| 7ae560dde0 | |||
| 97a36028b9 | |||
| f0d99676a3 | |||
| 8e38adae7e | |||
| d95bc37592 | |||
| 3f6354053c | |||
| a9bab60ba6 | |||
| 614bd8590d | |||
| bd9dc6455f | |||
| 0faa427970 | |||
| dd9da4831e | |||
| 0d816cee5d |
@@ -1,3 +1,19 @@
|
|||||||
|
### v2.8 (03-Nov-2018)
|
||||||
|
|
||||||
|
- Added support for partitioning which lets you split the output into multiple files by setting the partition message limit (`-p` parameter for CLI).
|
||||||
|
- Exported file will no longer open automatically on completion.
|
||||||
|
- Reduced amount of memory used during exporting.
|
||||||
|
- Disabled message grouping in PlainText export.
|
||||||
|
- Improved encoding of newlines in CSV export.
|
||||||
|
- Improved error messages in the GUI app during crashes.
|
||||||
|
|
||||||
|
### 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), `dm` (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, the file name will be automatically generated if the provided output file path is a directory.
|
||||||
|
|
||||||
### v2.6 (25-Jul-2018)
|
### 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 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.
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
using System;
|
|
||||||
using DiscordChatExporter.Core.Models;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli
|
|
||||||
{
|
|
||||||
public class CliOptions
|
|
||||||
{
|
|
||||||
public string TokenValue { get; set; }
|
|
||||||
|
|
||||||
public bool IsBotToken { 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();
|
||||||
@@ -23,15 +14,13 @@ namespace DiscordChatExporter.Cli
|
|||||||
// Services
|
// Services
|
||||||
SimpleIoc.Default.Register<IDataService, DataService>();
|
SimpleIoc.Default.Register<IDataService, DataService>();
|
||||||
SimpleIoc.Default.Register<IExportService, ExportService>();
|
SimpleIoc.Default.Register<IExportService, ExportService>();
|
||||||
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.6</Version>
|
<Version>2.8</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,125 +1,71 @@
|
|||||||
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;
|
|
||||||
var availableFormats = Enum.GetNames(typeof(ExportFormat));
|
|
||||||
|
|
||||||
Console.WriteLine($"=== Discord Chat Exporter (Command Line Interface) v{version} ===");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("[-t] [--token] Discord authorization token.");
|
|
||||||
Console.WriteLine("[-b] [--bot] Whether this is a bot token.");
|
|
||||||
Console.WriteLine("[-c] [--channel] Discord channel ID.");
|
|
||||||
Console.WriteLine("[-f] [--format] Export format. Optional.");
|
|
||||||
Console.WriteLine("[-o] [--output] Output file path. Optional.");
|
|
||||||
Console.WriteLine(" [--datefrom] Limit to messages after this date. Optional.");
|
|
||||||
Console.WriteLine(" [--dateto] Limit to messages before this date. Optional.");
|
|
||||||
Console.WriteLine(" [--dateformat] Date format. Optional.");
|
|
||||||
Console.WriteLine(" [--grouplimit] Message group limit. Optional.");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine($"Available export formats: {availableFormats.JoinToString(", ")}");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("# To get user token:");
|
Console.WriteLine("# To get user token:");
|
||||||
Console.WriteLine(" - Open Discord app");
|
Console.WriteLine(" 1. Open Discord app");
|
||||||
Console.WriteLine(" - Log in if you haven't");
|
Console.WriteLine(" 2. Log in if you haven't");
|
||||||
Console.WriteLine(" - Press Ctrl+Shift+I to show developer tools");
|
Console.WriteLine(" 3. Press Ctrl+Shift+I to show developer tools");
|
||||||
Console.WriteLine(" - Navigate to the Application tab");
|
Console.WriteLine(" 4. Press Ctrl+R to trigger reload");
|
||||||
Console.WriteLine(" - Expand Storage > Local Storage > https://discordapp.com");
|
Console.WriteLine(" 5. Navigate to the Application tab");
|
||||||
Console.WriteLine(" - Find the \"token\" key and copy its value");
|
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("# To get bot token:");
|
Console.WriteLine("# To get bot token:");
|
||||||
Console.WriteLine(" - Go to Discord developer portal");
|
Console.WriteLine(" 1. Go to Discord developer portal");
|
||||||
Console.WriteLine(" - Log in if you haven't");
|
Console.WriteLine(" 2. Log in if you haven't");
|
||||||
Console.WriteLine(" - Open your application's settings");
|
Console.WriteLine(" 3. Open your application's settings");
|
||||||
Console.WriteLine(" - Navigate to the Bot section on the left");
|
Console.WriteLine(" 4. Navigate to the Bot section on the left");
|
||||||
Console.WriteLine(" - Under Token click Copy");
|
Console.WriteLine(" 5. Under Token click Copy");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine("# To get channel ID:");
|
Console.WriteLine("# To get guild or channel ID:");
|
||||||
Console.WriteLine(" - Open Discord app");
|
Console.WriteLine(" 1. Open Discord app");
|
||||||
Console.WriteLine(" - Log in if you haven't");
|
Console.WriteLine(" 2. Log in if you haven't");
|
||||||
Console.WriteLine(" - Go to any channel you want to export");
|
Console.WriteLine(" 3. Open Settings");
|
||||||
Console.WriteLine(" - Press Ctrl+Shift+I to show developer tools");
|
Console.WriteLine(" 4. Go to Appearance section");
|
||||||
Console.WriteLine(" - Navigate to the Console tab");
|
Console.WriteLine(" 5. Enable Developer Mode");
|
||||||
Console.WriteLine(" - Type \"document.URL\" and press Enter");
|
Console.WriteLine(" 6. Right click on the desired guild or channel and click Copy ID");
|
||||||
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.TokenValue).As('t', "token").Required();
|
|
||||||
argsParser.Setup(o => o.IsBotToken).As('b', "bot").SetDefault(false);
|
|
||||||
argsParser.Setup(o => o.ChannelId).As('c', "channel").Required();
|
|
||||||
argsParser.Setup(o => o.ExportFormat).As('f', "format").SetDefault(ExportFormat.HtmlDark);
|
|
||||||
argsParser.Setup(o => o.FilePath).As('o', "output").SetDefault(null);
|
|
||||||
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)
|
||||||
|
};
|
||||||
|
|
||||||
// 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());
|
||||||
|
|
||||||
// Create token
|
// Show token help if help requested or no verb specified
|
||||||
var token = new AuthToken(
|
parsedArgs.WithNotParsed(errs =>
|
||||||
options.IsBotToken ? AuthTokenType.Bot : AuthTokenType.User,
|
{
|
||||||
options.TokenValue);
|
var err = errs.First();
|
||||||
|
|
||||||
// Export
|
if (err.Tag == ErrorType.NoVerbSelectedError)
|
||||||
var vm = Container.MainViewModel;
|
PrintTokenHelp();
|
||||||
vm.ExportAsync(
|
|
||||||
token,
|
|
||||||
options.ChannelId,
|
|
||||||
options.FilePath,
|
|
||||||
options.ExportFormat,
|
|
||||||
options.From,
|
|
||||||
options.To).GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
// Cleanup container
|
if (err.Tag == ErrorType.HelpVerbRequestedError && args.Length == 1)
|
||||||
Container.Cleanup();
|
PrintTokenHelp();
|
||||||
|
});
|
||||||
Console.WriteLine("Export complete.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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 exportService = container.Resolve<IExportService>();
|
||||||
|
|
||||||
|
// Configure settings
|
||||||
|
if (Options.DateFormat.IsNotBlank())
|
||||||
|
settingsService.DateFormat = Options.DateFormat;
|
||||||
|
if (Options.MessageGroupLimit > 0)
|
||||||
|
settingsService.MessageGroupLimit = Options.MessageGroupLimit;
|
||||||
|
|
||||||
|
// Get chat log
|
||||||
|
var chatLog = await dataService.GetChatLogAsync(Options.GetToken(), Options.ChannelId,
|
||||||
|
Options.After, Options.Before);
|
||||||
|
|
||||||
|
// Generate file path if not set
|
||||||
|
var filePath = Options.FilePath;
|
||||||
|
if (filePath == null || filePath.EndsWith("/") || filePath.EndsWith("\\"))
|
||||||
|
{
|
||||||
|
filePath += $"{chatLog.Guild.Name} - {chatLog.Channel.Name}.{Options.ExportFormat.GetFileExtension()}"
|
||||||
|
.Replace(Path.GetInvalidFileNameChars(), '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export
|
||||||
|
exportService.ExportChatLog(chatLog, filePath, Options.ExportFormat, Options.PartitionLimit);
|
||||||
|
|
||||||
|
// 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,34 @@
|
|||||||
|
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('p', "partition", Default = null, HelpText = "Split output into partitions limited to this number of messages.")]
|
||||||
|
public int? PartitionLimit { 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,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(AuthToken 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(AuthToken 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.6</Version>
|
<Version>2.8</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
@@ -14,20 +13,18 @@ namespace DiscordChatExporter.Core.Models
|
|||||||
|
|
||||||
public DateTime? To { get; }
|
public DateTime? To { get; }
|
||||||
|
|
||||||
public IReadOnlyList<MessageGroup> MessageGroups { get; }
|
public IReadOnlyList<Message> Messages { get; }
|
||||||
|
|
||||||
public int TotalMessageCount => MessageGroups.Sum(g => g.Messages.Count);
|
|
||||||
|
|
||||||
public Mentionables Mentionables { get; }
|
public Mentionables Mentionables { get; }
|
||||||
|
|
||||||
public ChatLog(Guild guild, Channel channel, DateTime? from, DateTime? to,
|
public ChatLog(Guild guild, Channel channel, DateTime? from, DateTime? to,
|
||||||
IReadOnlyList<MessageGroup> messageGroups, Mentionables mentionables)
|
IReadOnlyList<Message> messages, Mentionables mentionables)
|
||||||
{
|
{
|
||||||
Guild = guild;
|
Guild = guild;
|
||||||
Channel = channel;
|
Channel = channel;
|
||||||
From = from;
|
From = from;
|
||||||
To = to;
|
To = to;
|
||||||
MessageGroups = messageGroups;
|
Messages = messages;
|
||||||
Mentionables = mentionables;
|
Mentionables = mentionables;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Models
|
namespace DiscordChatExporter.Core.Models
|
||||||
{
|
{
|
||||||
@@ -31,5 +33,45 @@ namespace DiscordChatExporter.Core.Models
|
|||||||
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(format));
|
throw new ArgumentOutOfRangeException(nameof(format));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<ChatLog> SplitIntoPartitions(this ChatLog chatLog, int partitionLimit)
|
||||||
|
{
|
||||||
|
// If chat log has fewer messages than the limit - just return chat log in a list
|
||||||
|
if (chatLog.Messages.Count <= partitionLimit)
|
||||||
|
return new[] {chatLog};
|
||||||
|
|
||||||
|
var result = new List<ChatLog>();
|
||||||
|
|
||||||
|
// Loop through messages
|
||||||
|
var buffer = new List<Message>();
|
||||||
|
foreach (var message in chatLog.Messages)
|
||||||
|
{
|
||||||
|
// Add message to buffer
|
||||||
|
buffer.Add(message);
|
||||||
|
|
||||||
|
// If reached the limit - split and reset buffer
|
||||||
|
if (buffer.Count >= partitionLimit)
|
||||||
|
{
|
||||||
|
// Add to result
|
||||||
|
var chatLogPartition = new ChatLog(chatLog.Guild, chatLog.Channel, chatLog.From, chatLog.To, buffer,
|
||||||
|
chatLog.Mentionables);
|
||||||
|
result.Add(chatLogPartition);
|
||||||
|
|
||||||
|
// Reset the buffer instead of clearing to avoid mutations on existing references
|
||||||
|
buffer = new List<Message>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add what's remaining in buffer
|
||||||
|
if (buffer.Any())
|
||||||
|
{
|
||||||
|
// Add to result
|
||||||
|
var chatLogPartition = new ChatLog(chatLog.Guild, chatLog.Channel, chatLog.From, chatLog.To, buffer,
|
||||||
|
chatLog.Mentionables);
|
||||||
|
result.Add(chatLogPartition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Models
|
|
||||||
{
|
|
||||||
public class MessageGroup
|
|
||||||
{
|
|
||||||
public User Author { get; }
|
|
||||||
|
|
||||||
public DateTime Timestamp { get; }
|
|
||||||
|
|
||||||
public IReadOnlyList<Message> Messages { get; }
|
|
||||||
|
|
||||||
public MessageGroup(User author, DateTime timestamp, IReadOnlyList<Message> messages)
|
|
||||||
{
|
|
||||||
Author = author;
|
|
||||||
Timestamp = timestamp;
|
|
||||||
Messages = messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => $"{Author.FullName} | {Timestamp} | {Messages.Count} messages";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
Author;Date;Content;Attachments;
|
Author;Date;Content;Attachments;
|
||||||
{{~ for group in Model.MessageGroups -}}
|
{{~ for message in Model.Messages -}}
|
||||||
{{- for message in group.Messages -}}
|
{{- }}"{{ message.Author.FullName }}";
|
||||||
{{- message.Author.FullName }};
|
|
||||||
|
|
||||||
{{- message.Timestamp | FormatDate }};
|
{{- }}"{{ message.Timestamp | FormatDate }}";
|
||||||
|
|
||||||
{{- message.Content | FormatContent }};
|
{{- }}"{{ message.Content | FormatContent }}";
|
||||||
|
|
||||||
{{- message.Attachments | array.map "Url" | array.join "," }};
|
{{- }}"{{ message.Attachments | array.map "Url" | array.join "," }}";
|
||||||
{{~ end -}}
|
{{~ end -}}
|
||||||
{{- end -}}
|
|
||||||
|
Can't render this file because it contains an unexpected character in line 10 and column 41.
|
@@ -24,7 +24,7 @@
|
|||||||
<div class="info__channel-topic">{{ Model.Channel.Topic | html.escape }}</div>
|
<div class="info__channel-topic">{{ Model.Channel.Topic | html.escape }}</div>
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
|
|
||||||
<div class="info__channel-message-count">{{ Model.TotalMessageCount | Format "N0" }} messages</div>
|
<div class="info__channel-message-count">{{ Model.Messages | array.size | Format "N0" }} messages</div>
|
||||||
|
|
||||||
{{~ if Model.From || Model.To ~}}
|
{{~ if Model.From || Model.To ~}}
|
||||||
<div class="info__channel-date-range">
|
<div class="info__channel-date-range">
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
{{~ # Log ~}}
|
{{~ # Log ~}}
|
||||||
<div class="chatlog">
|
<div class="chatlog">
|
||||||
{{~ for group in Model.MessageGroups ~}}
|
{{~ for group in Model.Messages | GroupMessages ~}}
|
||||||
<div class="chatlog__message-group">
|
<div class="chatlog__message-group">
|
||||||
{{~ # Avatar ~}}
|
{{~ # Avatar ~}}
|
||||||
<div class="chatlog__author-avatar-container">
|
<div class="chatlog__author-avatar-container">
|
||||||
|
|||||||
@@ -3,23 +3,19 @@
|
|||||||
Guild: {{ Model.Guild.Name }}
|
Guild: {{ Model.Guild.Name }}
|
||||||
Channel: {{ Model.Channel.Name }}
|
Channel: {{ Model.Channel.Name }}
|
||||||
Topic: {{ Model.Channel.Topic }}
|
Topic: {{ Model.Channel.Topic }}
|
||||||
Messages: {{ Model.TotalMessageCount | Format "N0" }}
|
Messages: {{ Model.Messages | array.size | Format "N0" }}
|
||||||
Range: {{ if Model.From }}{{ Model.From | FormatDate }} {{ end }}{{ if Model.From || Model.To }}->{{ end }}{{ if Model.To }} {{ Model.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 Model.MessageGroups ~}}
|
{{~ for message in Model.Messages ~}}
|
||||||
{{~ # Author name and timestamp ~}}
|
{{~ # Author name and timestamp ~}}
|
||||||
{{~ }}[{{ group.Timestamp | FormatDate }}] {{ group.Author.FullName }}
|
{{~ }}[{{ message.Timestamp | FormatDate }}] {{ message.Author.FullName }}
|
||||||
{{~ # Messages ~}}
|
{{~ # Content ~}}
|
||||||
{{~ for message in group.Messages ~}}
|
{{~ message.Content | FormatContent }}
|
||||||
{{~ # Content ~}}
|
{{~ # Attachments ~}}
|
||||||
{{~ message.Content | FormatContent }}
|
{{~ for attachment in message.Attachments ~}}
|
||||||
{{~ # Attachments ~}}
|
{{~ attachment.Url }}
|
||||||
{{~ for attachment in message.Attachments ~}}
|
|
||||||
{{~ attachment.Url }}
|
|
||||||
{{~ end ~}}
|
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
|
|
||||||
|
|
||||||
{{~ end ~}}
|
{{~ end ~}}
|
||||||
@@ -32,13 +32,10 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
const string apiRoot = "https://discordapp.com/api/v6";
|
const string apiRoot = "https://discordapp.com/api/v6";
|
||||||
using (var request = new HttpRequestMessage(HttpMethod.Get, $"{apiRoot}/{resource}/{endpoint}"))
|
using (var request = new HttpRequestMessage(HttpMethod.Get, $"{apiRoot}/{resource}/{endpoint}"))
|
||||||
{
|
{
|
||||||
// Add url parameter for the user token
|
// Set authorization header
|
||||||
if (token.Type == AuthTokenType.User)
|
request.Headers.Authorization = token.Type == AuthTokenType.Bot
|
||||||
request.RequestUri = request.RequestUri.SetQueryParameter("token", token.Value);
|
? new AuthenticationHeaderValue("Bot", token.Value)
|
||||||
|
: new AuthenticationHeaderValue(token.Value);
|
||||||
// Add header for the bot token
|
|
||||||
if (token.Type == AuthTokenType.Bot)
|
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bot", token.Value);
|
|
||||||
|
|
||||||
// Add parameters
|
// Add parameters
|
||||||
foreach (var parameter in parameters)
|
foreach (var parameter in parameters)
|
||||||
@@ -68,6 +65,10 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
|
|
||||||
public async Task<Guild> GetGuildAsync(AuthToken token, string guildId)
|
public async Task<Guild> GetGuildAsync(AuthToken token, string guildId)
|
||||||
{
|
{
|
||||||
|
// Special case for direct messages pseudo-guild
|
||||||
|
if (guildId == Guild.DirectMessages.Id)
|
||||||
|
return Guild.DirectMessages;
|
||||||
|
|
||||||
var response = await GetApiResponseAsync(token, "guilds", guildId);
|
var response = await GetApiResponseAsync(token, "guilds", guildId);
|
||||||
var guild = ParseGuild(response);
|
var guild = ParseGuild(response);
|
||||||
|
|
||||||
@@ -213,6 +214,33 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
return new Mentionables(users, channels, roles);
|
return new Mentionables(users, channels, roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ChatLog> GetChatLogAsync(AuthToken token, Guild guild, Channel channel,
|
||||||
|
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null)
|
||||||
|
{
|
||||||
|
// Get messages
|
||||||
|
var messages = await GetChannelMessagesAsync(token, channel.Id, from, to, progress);
|
||||||
|
|
||||||
|
// Get mentionables
|
||||||
|
var mentionables = await GetMentionablesAsync(token, guild.Id, messages);
|
||||||
|
|
||||||
|
return new ChatLog(guild, channel, from, to, messages, mentionables);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChatLog> GetChatLogAsync(AuthToken token, string channelId,
|
||||||
|
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null)
|
||||||
|
{
|
||||||
|
// Get channel
|
||||||
|
var channel = await GetChannelAsync(token, channelId);
|
||||||
|
|
||||||
|
// Get guild
|
||||||
|
var guild = channel.GuildId == Guild.DirectMessages.Id
|
||||||
|
? Guild.DirectMessages
|
||||||
|
: await GetGuildAsync(token, channel.GuildId);
|
||||||
|
|
||||||
|
// Get the chat log
|
||||||
|
return await GetChatLogAsync(token, guild, channel, from, to, progress);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_httpClient.Dispose();
|
_httpClient.Dispose();
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using DiscordChatExporter.Core.Models;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Services
|
||||||
|
{
|
||||||
|
public partial class ExportService
|
||||||
|
{
|
||||||
|
private class MessageGroup
|
||||||
|
{
|
||||||
|
public User Author { get; }
|
||||||
|
|
||||||
|
public DateTime Timestamp { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Message> Messages { get; }
|
||||||
|
|
||||||
|
public MessageGroup(User author, DateTime timestamp, IReadOnlyList<Message> messages)
|
||||||
|
{
|
||||||
|
Author = author;
|
||||||
|
Timestamp = timestamp;
|
||||||
|
Messages = messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -18,12 +19,57 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
private readonly ExportFormat _format;
|
private readonly ExportFormat _format;
|
||||||
private readonly ChatLog _log;
|
private readonly ChatLog _log;
|
||||||
private readonly string _dateFormat;
|
private readonly string _dateFormat;
|
||||||
|
private readonly int _messageGroupLimit;
|
||||||
|
|
||||||
public TemplateModel(ExportFormat format, ChatLog log, string dateFormat)
|
public TemplateModel(ExportFormat format, ChatLog log, string dateFormat, int messageGroupLimit)
|
||||||
{
|
{
|
||||||
_format = format;
|
_format = format;
|
||||||
_log = log;
|
_log = log;
|
||||||
_dateFormat = dateFormat;
|
_dateFormat = dateFormat;
|
||||||
|
_messageGroupLimit = messageGroupLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<MessageGroup> GroupMessages(IEnumerable<Message> messages)
|
||||||
|
{
|
||||||
|
// Group adjacent messages by timestamp and author
|
||||||
|
var buffer = new List<Message>();
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
var groupFirst = buffer.FirstOrDefault();
|
||||||
|
|
||||||
|
// Group break condition
|
||||||
|
var breakCondition =
|
||||||
|
groupFirst != null &&
|
||||||
|
(
|
||||||
|
message.Author.Id != groupFirst.Author.Id ||
|
||||||
|
(message.Timestamp - groupFirst.Timestamp).TotalHours > 1 ||
|
||||||
|
message.Timestamp.Hour != groupFirst.Timestamp.Hour ||
|
||||||
|
buffer.Count >= _messageGroupLimit
|
||||||
|
);
|
||||||
|
|
||||||
|
// If condition is true - flush buffer
|
||||||
|
if (breakCondition)
|
||||||
|
{
|
||||||
|
var group = new MessageGroup(groupFirst.Author, groupFirst.Timestamp, buffer);
|
||||||
|
|
||||||
|
// Reset the buffer instead of clearing to avoid mutations on existing references
|
||||||
|
buffer = new List<Message>();
|
||||||
|
|
||||||
|
yield return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add message to buffer
|
||||||
|
buffer.Add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add what's remaining in buffer
|
||||||
|
if (buffer.Any())
|
||||||
|
{
|
||||||
|
var groupFirst = buffer.First();
|
||||||
|
var group = new MessageGroup(groupFirst.Author, groupFirst.Timestamp, buffer);
|
||||||
|
|
||||||
|
yield return group;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string HtmlEncode(string str) => WebUtility.HtmlEncode(str);
|
private string HtmlEncode(string str) => WebUtility.HtmlEncode(str);
|
||||||
@@ -229,9 +275,6 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
|
|
||||||
private string FormatContentCsv(string content)
|
private string FormatContentCsv(string content)
|
||||||
{
|
{
|
||||||
// New lines
|
|
||||||
content = content.Replace("\n", ", ");
|
|
||||||
|
|
||||||
// Escape quotes
|
// Escape quotes
|
||||||
content = content.Replace("\"", "\"\"");
|
content = content.Replace("\"", "\"\"");
|
||||||
|
|
||||||
@@ -310,6 +353,7 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
scriptObject.SetValue("Model", _log, true);
|
scriptObject.SetValue("Model", _log, true);
|
||||||
|
|
||||||
// Import functions
|
// Import functions
|
||||||
|
scriptObject.Import(nameof(GroupMessages), new Func<IEnumerable<Message>, IEnumerable<MessageGroup>>(GroupMessages));
|
||||||
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));
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using System.IO;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
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
|
||||||
{
|
{
|
||||||
@@ -14,7 +16,7 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Export(ExportFormat format, string filePath, ChatLog log)
|
private void ExportChatLogSingle(ChatLog chatLog, string filePath, ExportFormat format)
|
||||||
{
|
{
|
||||||
// Create template loader
|
// Create template loader
|
||||||
var loader = new TemplateLoader();
|
var loader = new TemplateLoader();
|
||||||
@@ -34,17 +36,66 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create template model
|
// Create template model
|
||||||
var templateModel = new TemplateModel(format, log, _settingsService.DateFormat);
|
var templateModel = new TemplateModel(format, chatLog,
|
||||||
|
_settingsService.DateFormat, _settingsService.MessageGroupLimit);
|
||||||
|
|
||||||
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))
|
||||||
{
|
{
|
||||||
// Configure output
|
// Configure output
|
||||||
context.PushOutput(new TextWriterOutput(output));
|
context.PushOutput(new TextWriterOutput(output));
|
||||||
|
|
||||||
// Render template
|
// Render output
|
||||||
template.Render(context);
|
context.Evaluate(template.Page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExportChatLogPartitions(IReadOnlyList<ChatLog> partitions, string filePath, ExportFormat format)
|
||||||
|
{
|
||||||
|
// Split file path into components
|
||||||
|
var dirPath = Path.GetDirectoryName(filePath);
|
||||||
|
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
var fileExt = Path.GetExtension(filePath);
|
||||||
|
|
||||||
|
// Export each partition separately
|
||||||
|
var partitionNumber = 1;
|
||||||
|
foreach (var partition in partitions)
|
||||||
|
{
|
||||||
|
// Compose new file name
|
||||||
|
var partitionFilePath = $"{fileNameWithoutExt}-{partitionNumber}{fileExt}";
|
||||||
|
|
||||||
|
// Compose full file path
|
||||||
|
if (dirPath.IsNotBlank())
|
||||||
|
partitionFilePath = Path.Combine(dirPath, partitionFilePath);
|
||||||
|
|
||||||
|
// Export
|
||||||
|
ExportChatLogSingle(partition, partitionFilePath, format);
|
||||||
|
|
||||||
|
// Increment partition number
|
||||||
|
partitionNumber++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExportChatLog(ChatLog chatLog, string filePath, ExportFormat format,
|
||||||
|
int? partitionLimit = null)
|
||||||
|
{
|
||||||
|
// If partitioning is disabled or there are fewer messages in chat log than the limit - process it without partitioning
|
||||||
|
if (partitionLimit == null || chatLog.Messages.Count <= partitionLimit)
|
||||||
|
{
|
||||||
|
ExportChatLogSingle(chatLog, filePath, format);
|
||||||
|
}
|
||||||
|
// Otherwise split into partitions and export separately
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var partitions = chatLog.SplitIntoPartitions(partitionLimit.Value);
|
||||||
|
ExportChatLogPartitions(partitions, filePath, format);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,5 +24,11 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
|
|
||||||
Task<Mentionables> GetMentionablesAsync(AuthToken token, string guildId,
|
Task<Mentionables> GetMentionablesAsync(AuthToken token, string guildId,
|
||||||
IEnumerable<Message> messages);
|
IEnumerable<Message> messages);
|
||||||
|
|
||||||
|
Task<ChatLog> GetChatLogAsync(AuthToken token, Guild guild, Channel channel,
|
||||||
|
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null);
|
||||||
|
|
||||||
|
Task<ChatLog> GetChatLogAsync(AuthToken token, string channelId,
|
||||||
|
DateTime? from = null, DateTime? to = null, IProgress<double> progress = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
{
|
{
|
||||||
public interface IExportService
|
public interface IExportService
|
||||||
{
|
{
|
||||||
void Export(ExportFormat format, string filePath, ChatLog log);
|
void ExportChatLog(ChatLog chatLog, string filePath, ExportFormat format,
|
||||||
|
int? partitionLimit = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using DiscordChatExporter.Core.Models;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Services
|
|
||||||
{
|
|
||||||
public interface IMessageGroupService
|
|
||||||
{
|
|
||||||
IReadOnlyList<MessageGroup> GroupMessages(IEnumerable<Message> messages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,7 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
|
|
||||||
AuthToken LastToken { get; set; }
|
AuthToken LastToken { get; set; }
|
||||||
ExportFormat LastExportFormat { get; set; }
|
ExportFormat LastExportFormat { get; set; }
|
||||||
|
int? LastPartitionLimit { get; set; }
|
||||||
|
|
||||||
void Load();
|
void Load();
|
||||||
void Save();
|
void Save();
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using DiscordChatExporter.Core.Models;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Services
|
|
||||||
{
|
|
||||||
public class MessageGroupService : IMessageGroupService
|
|
||||||
{
|
|
||||||
private readonly ISettingsService _settingsService;
|
|
||||||
|
|
||||||
public MessageGroupService(ISettingsService settingsService)
|
|
||||||
{
|
|
||||||
_settingsService = settingsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyList<MessageGroup> GroupMessages(IEnumerable<Message> messages)
|
|
||||||
{
|
|
||||||
var result = new List<MessageGroup>();
|
|
||||||
|
|
||||||
// Group adjacent messages by timestamp and author
|
|
||||||
var groupBuffer = new List<Message>();
|
|
||||||
foreach (var message in messages)
|
|
||||||
{
|
|
||||||
var groupFirst = groupBuffer.FirstOrDefault();
|
|
||||||
|
|
||||||
// Group break condition
|
|
||||||
var breakCondition =
|
|
||||||
groupFirst != null &&
|
|
||||||
(
|
|
||||||
message.Author.Id != groupFirst.Author.Id ||
|
|
||||||
(message.Timestamp - groupFirst.Timestamp).TotalHours > 1 ||
|
|
||||||
message.Timestamp.Hour != groupFirst.Timestamp.Hour ||
|
|
||||||
groupBuffer.Count >= _settingsService.MessageGroupLimit
|
|
||||||
);
|
|
||||||
|
|
||||||
// If condition is true - flush buffer
|
|
||||||
if (breakCondition)
|
|
||||||
{
|
|
||||||
var group = new MessageGroup(groupFirst.Author, groupFirst.Timestamp, groupBuffer.ToArray());
|
|
||||||
result.Add(group);
|
|
||||||
groupBuffer.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add message to buffer
|
|
||||||
groupBuffer.Add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add what's remaining in buffer
|
|
||||||
if (groupBuffer.Any())
|
|
||||||
{
|
|
||||||
var groupFirst = groupBuffer.First();
|
|
||||||
var group = new MessageGroup(groupFirst.Author, groupFirst.Timestamp, groupBuffer.ToArray());
|
|
||||||
result.Add(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ namespace DiscordChatExporter.Core.Services
|
|||||||
|
|
||||||
public AuthToken LastToken { get; set; }
|
public AuthToken LastToken { get; set; }
|
||||||
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
|
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
|
||||||
|
public int? LastPartitionLimit { get; set; }
|
||||||
|
|
||||||
public SettingsService()
|
public SettingsService()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
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"
|
DispatcherUnhandledException="App_OnDispatcherUnhandledException"
|
||||||
Startup="App_Startup"
|
|
||||||
StartupUri="Views/MainWindow.xaml">
|
StartupUri="Views/MainWindow.xaml">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Gui
|
namespace DiscordChatExporter.Gui
|
||||||
{
|
{
|
||||||
public partial class App
|
public partial class App
|
||||||
{
|
{
|
||||||
private Container Container => (Container) Resources["Container"];
|
private void App_OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs args)
|
||||||
|
|
||||||
private void App_Startup(object sender, StartupEventArgs e)
|
|
||||||
{
|
{
|
||||||
Container.Init();
|
MessageBox.Show(args.Exception.ToString(), "Error occured", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
||||||
@@ -24,7 +19,6 @@ namespace DiscordChatExporter.Gui
|
|||||||
// Services
|
// Services
|
||||||
SimpleIoc.Default.Register<IDataService, DataService>();
|
SimpleIoc.Default.Register<IDataService, DataService>();
|
||||||
SimpleIoc.Default.Register<IExportService, ExportService>();
|
SimpleIoc.Default.Register<IExportService, ExportService>();
|
||||||
SimpleIoc.Default.Register<IMessageGroupService, MessageGroupService>();
|
|
||||||
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
||||||
SimpleIoc.Default.Register<IUpdateService, UpdateService>();
|
SimpleIoc.Default.Register<IUpdateService, UpdateService>();
|
||||||
|
|
||||||
@@ -34,8 +28,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,39 +42,12 @@
|
|||||||
<StartupObject />
|
<StartupObject />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="CommonServiceLocator, Version=2.0.3.0, Culture=neutral, PublicKeyToken=489b6accfaf20ef0, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\CommonServiceLocator.2.0.3\lib\net45\CommonServiceLocator.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="GalaSoft.MvvmLight, Version=5.4.1.0, Culture=neutral, PublicKeyToken=e7570ab207bcb616, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\GalaSoft.MvvmLight.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="GalaSoft.MvvmLight.Extras, Version=5.4.1.0, Culture=neutral, PublicKeyToken=669f0b5e8f868abf, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\GalaSoft.MvvmLight.Extras.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="GalaSoft.MvvmLight.Platform, Version=5.4.1.0, Culture=neutral, PublicKeyToken=5f873c45e98af8a1, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\GalaSoft.MvvmLight.Platform.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="MaterialDesignColors, Version=1.1.3.0, Culture=neutral, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MaterialDesignColors.1.1.3\lib\net45\MaterialDesignColors.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="MaterialDesignThemes.Wpf, Version=2.4.0.1044, Culture=neutral, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MaterialDesignThemes.2.4.0.1044\lib\net45\MaterialDesignThemes.Wpf.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
<Reference Include="System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\MvvmLightLibs.5.4.1\lib\net45\System.Windows.Interactivity.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Xaml">
|
<Reference Include="System.Xaml">
|
||||||
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Tyrrrz.Extensions, Version=1.5.1.0, Culture=neutral, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Tyrrrz.Extensions.1.5.1\lib\net45\Tyrrrz.Extensions.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Tyrrrz.Settings, Version=1.3.2.0, Culture=neutral, PublicKeyToken=null" />
|
<Reference Include="Tyrrrz.Settings, Version=1.3.2.0, Culture=neutral, PublicKeyToken=null" />
|
||||||
<Reference Include="Tyrrrz.WpfExtensions, Version=1.0.6269.37170, Culture=neutral, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Tyrrrz.WpfExtensions.1.0.5\lib\net45\Tyrrrz.WpfExtensions.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="WindowsBase" />
|
<Reference Include="WindowsBase" />
|
||||||
<Reference Include="PresentationCore" />
|
<Reference Include="PresentationCore" />
|
||||||
<Reference Include="PresentationFramework" />
|
<Reference Include="PresentationFramework" />
|
||||||
@@ -119,9 +92,6 @@
|
|||||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<None Include="app.config" />
|
<None Include="app.config" />
|
||||||
<None Include="packages.config">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Resource Include="..\favicon.ico" />
|
<Resource Include="..\favicon.ico" />
|
||||||
@@ -150,6 +120,25 @@
|
|||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</Page>
|
</Page>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup />
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommonServiceLocator">
|
||||||
|
<Version>2.0.3</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="MaterialDesignColors">
|
||||||
|
<Version>1.1.3</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="MaterialDesignThemes">
|
||||||
|
<Version>2.4.0.1044</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="MvvmLightLibs">
|
||||||
|
<Version>5.4.1</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Tyrrrz.Extensions">
|
||||||
|
<Version>1.5.1</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Tyrrrz.WpfExtensions">
|
||||||
|
<Version>1.0.5</Version>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
@@ -15,14 +15,17 @@ namespace DiscordChatExporter.Gui.Messages
|
|||||||
|
|
||||||
public DateTime? To { get; }
|
public DateTime? To { get; }
|
||||||
|
|
||||||
|
public int? PartitionLimit { get; }
|
||||||
|
|
||||||
public StartExportMessage(Channel channel, string filePath, ExportFormat format,
|
public StartExportMessage(Channel channel, string filePath, ExportFormat format,
|
||||||
DateTime? from, DateTime? to)
|
DateTime? from, DateTime? to, int? partitionLimit)
|
||||||
{
|
{
|
||||||
Channel = channel;
|
Channel = channel;
|
||||||
FilePath = filePath;
|
FilePath = filePath;
|
||||||
Format = format;
|
Format = format;
|
||||||
From = from;
|
From = from;
|
||||||
To = to;
|
To = to;
|
||||||
|
PartitionLimit = partitionLimit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.6")]
|
[assembly: AssemblyVersion("2.8")]
|
||||||
[assembly: AssemblyFileVersion("2.6")]
|
[assembly: AssemblyFileVersion("2.8")]
|
||||||
@@ -19,6 +19,7 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
private ExportFormat _format;
|
private ExportFormat _format;
|
||||||
private DateTime? _from;
|
private DateTime? _from;
|
||||||
private DateTime? _to;
|
private DateTime? _to;
|
||||||
|
private int? _partitionLimit;
|
||||||
|
|
||||||
public Guild Guild { get; private set; }
|
public Guild Guild { get; private set; }
|
||||||
|
|
||||||
@@ -63,6 +64,12 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
set => Set(ref _to, value);
|
set => Set(ref _to, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int? PartitionLimit
|
||||||
|
{
|
||||||
|
get => _partitionLimit;
|
||||||
|
set => Set(ref _partitionLimit, value);
|
||||||
|
}
|
||||||
|
|
||||||
// Commands
|
// Commands
|
||||||
public RelayCommand ExportCommand { get; }
|
public RelayCommand ExportCommand { get; }
|
||||||
|
|
||||||
@@ -83,13 +90,15 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
.Replace(Path.GetInvalidFileNameChars(), '_');
|
.Replace(Path.GetInvalidFileNameChars(), '_');
|
||||||
From = null;
|
From = null;
|
||||||
To = null;
|
To = null;
|
||||||
|
PartitionLimit = _settingsService.LastPartitionLimit;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Export()
|
private void Export()
|
||||||
{
|
{
|
||||||
// Save format
|
// Persist preferences
|
||||||
_settingsService.LastExportFormat = SelectedFormat;
|
_settingsService.LastExportFormat = SelectedFormat;
|
||||||
|
_settingsService.LastPartitionLimit = PartitionLimit;
|
||||||
|
|
||||||
// Clamp 'from' and 'to' values
|
// Clamp 'from' and 'to' values
|
||||||
if (From > To)
|
if (From > To)
|
||||||
@@ -98,7 +107,7 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
To = From;
|
To = From;
|
||||||
|
|
||||||
// Start export
|
// Start export
|
||||||
MessengerInstance.Send(new StartExportMessage(Channel, FilePath, SelectedFormat, From, To));
|
MessengerInstance.Send(new StartExportMessage(Channel, FilePath, SelectedFormat, From, To, PartitionLimit));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
ExportFormat SelectedFormat { get; set; }
|
ExportFormat SelectedFormat { get; set; }
|
||||||
DateTime? From { get; set; }
|
DateTime? From { get; set; }
|
||||||
DateTime? To { get; set; }
|
DateTime? To { get; set; }
|
||||||
|
int? PartitionLimit { get; set; }
|
||||||
|
|
||||||
RelayCommand ExportCommand { get; }
|
RelayCommand ExportCommand { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly IUpdateService _updateService;
|
private readonly IUpdateService _updateService;
|
||||||
private readonly IDataService _dataService;
|
private readonly IDataService _dataService;
|
||||||
private readonly IMessageGroupService _messageGroupService;
|
|
||||||
private readonly IExportService _exportService;
|
private readonly IExportService _exportService;
|
||||||
|
|
||||||
private readonly Dictionary<Guild, IReadOnlyList<Channel>> _guildChannelsMap;
|
private readonly Dictionary<Guild, IReadOnlyList<Channel>> _guildChannelsMap;
|
||||||
@@ -111,12 +110,11 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
public RelayCommand<Channel> ShowExportSetupCommand { get; }
|
public RelayCommand<Channel> ShowExportSetupCommand { get; }
|
||||||
|
|
||||||
public MainViewModel(ISettingsService settingsService, IUpdateService updateService, IDataService dataService,
|
public MainViewModel(ISettingsService settingsService, IUpdateService updateService, IDataService dataService,
|
||||||
IMessageGroupService messageGroupService, IExportService exportService)
|
IExportService exportService)
|
||||||
{
|
{
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_updateService = updateService;
|
_updateService = updateService;
|
||||||
_dataService = dataService;
|
_dataService = dataService;
|
||||||
_messageGroupService = messageGroupService;
|
|
||||||
_exportService = exportService;
|
_exportService = exportService;
|
||||||
|
|
||||||
_guildChannelsMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
_guildChannelsMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
||||||
@@ -131,7 +129,7 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
MessengerInstance.Register<StartExportMessage>(this,
|
MessengerInstance.Register<StartExportMessage>(this,
|
||||||
m => Export(m.Channel, m.FilePath, m.Format, m.From, m.To));
|
m => Export(m.Channel, m.FilePath, m.Format, m.From, m.To, m.PartitionLimit));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ViewLoaded()
|
private async void ViewLoaded()
|
||||||
@@ -241,7 +239,8 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
MessengerInstance.Send(new ShowExportSetupMessage(SelectedGuild, channel));
|
MessengerInstance.Send(new ShowExportSetupMessage(SelectedGuild, channel));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Export(Channel channel, string filePath, ExportFormat format, DateTime? from, DateTime? to)
|
private async void Export(Channel channel, string filePath, ExportFormat format,
|
||||||
|
DateTime? from, DateTime? to, int? partitionLimit)
|
||||||
{
|
{
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
|
|
||||||
@@ -256,23 +255,11 @@ namespace DiscordChatExporter.Gui.ViewModels
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get messages
|
// Get chat log
|
||||||
var messages = await _dataService.GetChannelMessagesAsync(token, channel.Id, from, to, progressHandler);
|
var chatLog = await _dataService.GetChatLogAsync(token, guild, channel, from, to, progressHandler);
|
||||||
|
|
||||||
// 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
|
// Export
|
||||||
_exportService.Export(format, filePath, log);
|
_exportService.ExportChatLog(chatLog, filePath, format, partitionLimit);
|
||||||
|
|
||||||
// Open
|
|
||||||
Process.Start(filePath);
|
|
||||||
|
|
||||||
// Notify completion
|
// Notify completion
|
||||||
MessengerInstance.Send(new ShowNotificationMessage("Export complete"));
|
MessengerInstance.Send(new ShowNotificationMessage("Export complete"));
|
||||||
|
|||||||
@@ -55,6 +55,13 @@
|
|||||||
SelectedDate="{Binding To}" />
|
SelectedDate="{Binding To}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Partitioning -->
|
||||||
|
<TextBox
|
||||||
|
Margin="16,8,16,8"
|
||||||
|
materialDesign:HintAssist.Hint="Messages per partition (optional)"
|
||||||
|
materialDesign:HintAssist.IsFloating="True"
|
||||||
|
Text="{Binding PartitionLimit, TargetNullValue=''}" />
|
||||||
|
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -233,18 +233,23 @@
|
|||||||
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Ctrl+Shift+I" />
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Ctrl+Shift+I" />
|
||||||
<Run Text="to show developer tools" />
|
<Run Text="to show developer tools" />
|
||||||
<LineBreak />
|
<LineBreak />
|
||||||
<Run Text="4. Navigate to the" />
|
<Run Text="4. Press" />
|
||||||
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Ctrl+R" />
|
||||||
|
<Run Text="to trigger reload" />
|
||||||
|
<LineBreak />
|
||||||
|
<Run Text="5. Navigate to the" />
|
||||||
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Application" />
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Application" />
|
||||||
<Run Text="tab" />
|
<Run Text="tab" />
|
||||||
<LineBreak />
|
<LineBreak />
|
||||||
<Run Text="5. Expand" />
|
<Run Text="6. Select" />
|
||||||
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Storage > Local Storage > https://discordapp.com" />
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Local Storage" />
|
||||||
|
<Run Text=">" />
|
||||||
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="https://discordapp.com" />
|
||||||
|
<Run Text="on the left" />
|
||||||
<LineBreak />
|
<LineBreak />
|
||||||
<Run Text="6. Find the" />
|
<Run Text="7. Find" />
|
||||||
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text=""token"" />
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="token" />
|
||||||
<Run Text="key and copy its value" />
|
<Run Text="under key and copy the value" />
|
||||||
<LineBreak />
|
|
||||||
<Run Text="7. Paste the value in the textbox above" />
|
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
@@ -270,8 +275,6 @@
|
|||||||
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Token" />
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Token" />
|
||||||
<Run Text="click" />
|
<Run Text="click" />
|
||||||
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Copy" />
|
<Run Foreground="{DynamicResource PrimaryTextBrush}" Text="Copy" />
|
||||||
<LineBreak />
|
|
||||||
<Run Text="6. Paste the value in the textbox above" />
|
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<packages>
|
|
||||||
<package id="CommonServiceLocator" version="2.0.3" targetFramework="net461" />
|
|
||||||
<package id="MaterialDesignColors" version="1.1.3" targetFramework="net461" />
|
|
||||||
<package id="MaterialDesignThemes" version="2.4.0.1044" targetFramework="net461" />
|
|
||||||
<package id="MvvmLightLibs" version="5.4.1" targetFramework="net461" />
|
|
||||||
<package id="Tyrrrz.Extensions" version="1.5.1" targetFramework="net461" />
|
|
||||||
<package id="Tyrrrz.WpfExtensions" version="1.0.5" targetFramework="net461" />
|
|
||||||
</packages>
|
|
||||||
+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,6 +15,7 @@ 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
|
||||||
|
|
||||||
@@ -32,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