mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-23 11:16:59 -04:00
parent
e5e9d4c9ff
commit
d7345e91d3
18 changed files with 157 additions and 133 deletions
|
@ -148,7 +148,6 @@
|
|||
<Compile Include="Models\Guild.cs" />
|
||||
<Compile Include="Models\Message.cs" />
|
||||
<Compile Include="Models\MessageGroup.cs" />
|
||||
<Compile Include="Models\Theme.cs" />
|
||||
<Compile Include="Models\User.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Services\DataService.cs" />
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
{
|
||||
public enum ExportFormat
|
||||
{
|
||||
Text,
|
||||
Html
|
||||
PlainText,
|
||||
HtmlDark,
|
||||
HtmlLight
|
||||
}
|
||||
}
|
|
@ -1,14 +1,31 @@
|
|||
namespace DiscordChatExporter.Models
|
||||
using System;
|
||||
|
||||
namespace DiscordChatExporter.Models
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string GetFileExtension(this ExportFormat format)
|
||||
{
|
||||
if (format == ExportFormat.Text)
|
||||
if (format == ExportFormat.PlainText)
|
||||
return "txt";
|
||||
if (format == ExportFormat.Html)
|
||||
if (format == ExportFormat.HtmlDark)
|
||||
return "html";
|
||||
return null;
|
||||
if (format == ExportFormat.HtmlLight)
|
||||
return "html";
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static string GetDisplayName(this ExportFormat format)
|
||||
{
|
||||
if (format == ExportFormat.PlainText)
|
||||
return "Plain Text";
|
||||
if (format == ExportFormat.HtmlDark)
|
||||
return "HTML (Dark)";
|
||||
if (format == ExportFormat.HtmlLight)
|
||||
return "HTML (Light)";
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace DiscordChatExporter.Models
|
||||
{
|
||||
public enum Theme
|
||||
{
|
||||
Dark,
|
||||
Light
|
||||
}
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using AmmySidekick;
|
||||
|
||||
namespace DiscordChatExporter
|
||||
|
@ -15,5 +18,19 @@ namespace DiscordChatExporter
|
|||
|
||||
app.Run();
|
||||
}
|
||||
|
||||
public static string GetResourceString(string resourcePath)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var stream = assembly.GetManifestResourceStream(resourcePath);
|
||||
if (stream == null)
|
||||
throw new MissingManifestResourceException("Could not find resource");
|
||||
|
||||
using (stream)
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -71,7 +71,8 @@ namespace DiscordChatExporter.Services
|
|||
return channels;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId, DateTime? from, DateTime? to)
|
||||
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
|
||||
DateTime? from, DateTime? to)
|
||||
{
|
||||
var result = new List<Message>();
|
||||
|
||||
|
@ -100,10 +101,12 @@ namespace DiscordChatExporter.Services
|
|||
}
|
||||
|
||||
// If no messages - break
|
||||
if (currentMessageId == null) break;
|
||||
if (currentMessageId == null)
|
||||
break;
|
||||
|
||||
// If last message is older than from date - break
|
||||
if (from != null && result.Last().TimeStamp < from) break;
|
||||
if (from != null && result.Last().TimeStamp < from)
|
||||
break;
|
||||
|
||||
// Otherwise offset the next request
|
||||
beforeId = currentMessageId;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -20,7 +18,7 @@ namespace DiscordChatExporter.Services
|
|||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task ExportAsTextAsync(string filePath, ChannelChatLog log)
|
||||
private async Task ExportAsTextAsync(string filePath, ChannelChatLog log)
|
||||
{
|
||||
var dateFormat = _settingsService.DateFormat;
|
||||
|
||||
|
@ -66,9 +64,8 @@ namespace DiscordChatExporter.Services
|
|||
}
|
||||
}
|
||||
|
||||
public async Task ExportAsHtmlAsync(string filePath, ChannelChatLog log, Theme theme)
|
||||
private async Task ExportAsHtmlAsync(string filePath, ChannelChatLog log, string css)
|
||||
{
|
||||
var themeCss = GetThemeCss(theme);
|
||||
var dateFormat = _settingsService.DateFormat;
|
||||
|
||||
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128 * 1024))
|
||||
|
@ -85,7 +82,7 @@ namespace DiscordChatExporter.Services
|
|||
await writer.WriteLineAsync($"<title>{log.Guild} - {log.Channel}</title>");
|
||||
await writer.WriteLineAsync("<meta charset=\"utf-8\" />");
|
||||
await writer.WriteLineAsync("<meta name=\"viewport\" content=\"width=device-width\" />");
|
||||
await writer.WriteLineAsync($"<style>{themeCss}</style>");
|
||||
await writer.WriteLineAsync($"<style>{css}</style>");
|
||||
await writer.WriteLineAsync("</head>");
|
||||
|
||||
// Body start
|
||||
|
@ -173,26 +170,30 @@ namespace DiscordChatExporter.Services
|
|||
await writer.WriteLineAsync("</html>");
|
||||
}
|
||||
}
|
||||
|
||||
public Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log)
|
||||
{
|
||||
if (format == ExportFormat.PlainText)
|
||||
{
|
||||
return ExportAsTextAsync(filePath, log);
|
||||
}
|
||||
if (format == ExportFormat.HtmlDark)
|
||||
{
|
||||
var css = Program.GetResourceString("DiscordChatExporter.Resources.ExportService.DarkTheme.css");
|
||||
return ExportAsHtmlAsync(filePath, log, css);
|
||||
}
|
||||
if (format == ExportFormat.HtmlLight)
|
||||
{
|
||||
var css = Program.GetResourceString("DiscordChatExporter.Resources.ExportService.LightTheme.css");
|
||||
return ExportAsHtmlAsync(filePath, log, css);
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ExportService
|
||||
{
|
||||
private static string GetThemeCss(Theme theme)
|
||||
{
|
||||
var resourcePath = $"DiscordChatExporter.Resources.ExportService.{theme}Theme.css";
|
||||
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var stream = assembly.GetManifestResourceStream(resourcePath);
|
||||
if (stream == null)
|
||||
throw new MissingManifestResourceException("Could not find style resource");
|
||||
|
||||
using (stream)
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private static string HtmlEncode(string str)
|
||||
{
|
||||
return WebUtility.HtmlEncode(str);
|
||||
|
|
|
@ -5,7 +5,6 @@ namespace DiscordChatExporter.Services
|
|||
{
|
||||
public interface IExportService
|
||||
{
|
||||
Task ExportAsTextAsync(string filePath, ChannelChatLog log);
|
||||
Task ExportAsHtmlAsync(string filePath, ChannelChatLog log, Theme theme);
|
||||
Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ namespace DiscordChatExporter.Services
|
|||
{
|
||||
public interface ISettingsService
|
||||
{
|
||||
Theme Theme { get; set; }
|
||||
string DateFormat { get; set; }
|
||||
int MessageGroupLimit { get; set; }
|
||||
|
||||
|
|
|
@ -5,12 +5,11 @@ namespace DiscordChatExporter.Services
|
|||
{
|
||||
public class SettingsService : SettingsManager, ISettingsService
|
||||
{
|
||||
public Theme Theme { get; set; }
|
||||
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
|
||||
public int MessageGroupLimit { get; set; } = 20;
|
||||
|
||||
public string LastToken { get; set; }
|
||||
public ExportFormat LastExportFormat { get; set; } = ExportFormat.Html;
|
||||
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
|
||||
|
||||
public SettingsService()
|
||||
{
|
||||
|
|
|
@ -39,7 +39,15 @@ namespace DiscordChatExporter.ViewModels
|
|||
public ExportFormat SelectedFormat
|
||||
{
|
||||
get => _format;
|
||||
set => Set(ref _format, value);
|
||||
set
|
||||
{
|
||||
Set(ref _format, value);
|
||||
|
||||
// Replace extension in path
|
||||
var newExt = value.GetFileExtension();
|
||||
if (FilePath != null && !FilePath.EndsWith(newExt))
|
||||
FilePath = FilePath.SubstringUntilLast(".") + "." + newExt;
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? From
|
||||
|
@ -82,7 +90,10 @@ namespace DiscordChatExporter.ViewModels
|
|||
|
||||
private void Export()
|
||||
{
|
||||
// Save format
|
||||
_settingsService.LastExportFormat = SelectedFormat;
|
||||
|
||||
// Start export
|
||||
MessengerInstance.Send(new StartExportMessage(Channel, FilePath, SelectedFormat, From, To));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using DiscordChatExporter.Models;
|
||||
|
||||
namespace DiscordChatExporter.ViewModels
|
||||
namespace DiscordChatExporter.ViewModels
|
||||
{
|
||||
public interface ISettingsViewModel
|
||||
{
|
||||
IReadOnlyList<Theme> AvailableThemes { get; }
|
||||
Theme Theme { get; set; }
|
||||
string DateFormat { get; set; }
|
||||
int MessageGroupLimit { get; set; }
|
||||
}
|
||||
|
|
|
@ -23,10 +23,10 @@ namespace DiscordChatExporter.ViewModels
|
|||
private readonly Dictionary<Guild, IReadOnlyList<Channel>> _guildChannelsMap;
|
||||
|
||||
private bool _isBusy;
|
||||
private string _token;
|
||||
private IReadOnlyList<Guild> _availableGuilds;
|
||||
private Guild _selectedGuild;
|
||||
private IReadOnlyList<Channel> _availableChannels;
|
||||
private string _cachedToken;
|
||||
|
||||
public bool IsBusy
|
||||
{
|
||||
|
@ -43,13 +43,13 @@ namespace DiscordChatExporter.ViewModels
|
|||
|
||||
public string Token
|
||||
{
|
||||
get => _settingsService.LastToken;
|
||||
get => _token;
|
||||
set
|
||||
{
|
||||
// Remove invalid chars
|
||||
value = value?.Trim('"');
|
||||
|
||||
_settingsService.LastToken = value;
|
||||
Set(ref _token, value);
|
||||
PullDataCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
@ -107,12 +107,20 @@ namespace DiscordChatExporter.ViewModels
|
|||
{
|
||||
Export(m.Channel, m.FilePath, m.Format, m.From, m.To);
|
||||
});
|
||||
|
||||
// Defaults
|
||||
_token = _settingsService.LastToken;
|
||||
}
|
||||
|
||||
private async void PullData()
|
||||
{
|
||||
IsBusy = true;
|
||||
_cachedToken = Token;
|
||||
|
||||
// Copy token so it doesn't get mutated
|
||||
var token = Token;
|
||||
|
||||
// Save token
|
||||
_settingsService.LastToken = token;
|
||||
|
||||
// Clear existing
|
||||
_guildChannelsMap.Clear();
|
||||
|
@ -121,17 +129,17 @@ namespace DiscordChatExporter.ViewModels
|
|||
{
|
||||
// Get DM channels
|
||||
{
|
||||
var channels = await _dataService.GetDirectMessageChannelsAsync(_cachedToken);
|
||||
var channels = await _dataService.GetDirectMessageChannelsAsync(token);
|
||||
var guild = new Guild("@me", "Direct Messages", null);
|
||||
_guildChannelsMap[guild] = channels.ToArray();
|
||||
}
|
||||
|
||||
// Get guild channels
|
||||
{
|
||||
var guilds = await _dataService.GetGuildsAsync(_cachedToken);
|
||||
var guilds = await _dataService.GetGuildsAsync(token);
|
||||
foreach (var guild in guilds)
|
||||
{
|
||||
var channels = await _dataService.GetGuildChannelsAsync(_cachedToken, guild.Id);
|
||||
var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id);
|
||||
_guildChannelsMap[guild] = channels.Where(c => c.Type == ChannelType.GuildTextChat).ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -171,22 +179,22 @@ namespace DiscordChatExporter.ViewModels
|
|||
{
|
||||
IsBusy = true;
|
||||
|
||||
// Get last used token
|
||||
var token = _settingsService.LastToken;
|
||||
|
||||
try
|
||||
{
|
||||
// Get messages
|
||||
var messages = await _dataService.GetChannelMessagesAsync(_cachedToken, channel.Id, from, to);
|
||||
var messages = await _dataService.GetChannelMessagesAsync(token, channel.Id, from, to);
|
||||
|
||||
// Group them
|
||||
var messageGroups = _messageGroupService.GroupMessages(messages);
|
||||
|
||||
// Create log
|
||||
var chatLog = new ChannelChatLog(SelectedGuild, channel, messageGroups, messages.Count);
|
||||
var log = new ChannelChatLog(SelectedGuild, channel, messageGroups, messages.Count);
|
||||
|
||||
// Export
|
||||
if (format == ExportFormat.Text)
|
||||
await _exportService.ExportAsTextAsync(filePath, chatLog);
|
||||
else if (format == ExportFormat.Html)
|
||||
await _exportService.ExportAsHtmlAsync(filePath, chatLog, _settingsService.Theme);
|
||||
await _exportService.ExportAsync(format, filePath, log);
|
||||
|
||||
// Notify completion
|
||||
MessengerInstance.Send(new ShowExportDoneMessage(filePath));
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DiscordChatExporter.Models;
|
||||
using DiscordChatExporter.Services;
|
||||
using DiscordChatExporter.Services;
|
||||
using GalaSoft.MvvmLight;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
|
@ -12,14 +8,6 @@ namespace DiscordChatExporter.ViewModels
|
|||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public IReadOnlyList<Theme> AvailableThemes { get; }
|
||||
|
||||
public Theme Theme
|
||||
{
|
||||
get => _settingsService.Theme;
|
||||
set => _settingsService.Theme = value;
|
||||
}
|
||||
|
||||
public string DateFormat
|
||||
{
|
||||
get => _settingsService.DateFormat;
|
||||
|
@ -35,9 +23,6 @@ namespace DiscordChatExporter.ViewModels
|
|||
public SettingsViewModel(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
|
||||
// Defaults
|
||||
AvailableThemes = Enum.GetValues(typeof(Theme)).Cast<Theme>().ToArray();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
using MaterialDesignThemes.Wpf
|
||||
using DiscordChatExporter.Models
|
||||
using MaterialDesignThemes.Wpf
|
||||
|
||||
UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
||||
DataContext: bind ExportSetupViewModel from $resource Container
|
||||
Width: 350
|
||||
Width: 325
|
||||
|
||||
StackPanel {
|
||||
// File path
|
||||
|
@ -15,13 +16,29 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
|||
set [ UpdateSourceTrigger: PropertyChanged ]
|
||||
}
|
||||
|
||||
// Format
|
||||
ComboBox {
|
||||
Margin: "16 8 16 8"
|
||||
HintAssist.Hint: "Export format"
|
||||
HintAssist.IsFloating: true
|
||||
IsReadOnly: true
|
||||
ItemsSource: bind AvailableFormats
|
||||
ItemTemplate: DataTemplate {
|
||||
TextBlock {
|
||||
Text: bind
|
||||
convert (ExportFormat f) => Extensions.GetDisplayName(f)
|
||||
}
|
||||
}
|
||||
SelectedItem: bind SelectedFormat
|
||||
}
|
||||
|
||||
// Date range
|
||||
Grid {
|
||||
#TwoColumns("*", "*")
|
||||
|
||||
DatePicker {
|
||||
Grid.Column: 0
|
||||
Margin: "16 16 8 8"
|
||||
Margin: "16 20 8 8"
|
||||
HintAssist.Hint: "From"
|
||||
HintAssist.IsFloating: true
|
||||
SelectedDate: bind From
|
||||
|
@ -29,7 +46,7 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
|||
|
||||
DatePicker {
|
||||
Grid.Column: 1
|
||||
Margin: "8 16 16 8"
|
||||
Margin: "8 20 16 8"
|
||||
HintAssist.Hint: "To"
|
||||
HintAssist.IsFloating: true
|
||||
SelectedDate: bind To
|
||||
|
@ -40,6 +57,14 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
|||
@StackPanelHorizontal {
|
||||
HorizontalAlignment: Right
|
||||
|
||||
// Browse
|
||||
Button "BrowseButton" {
|
||||
Click: BrowseButton_Click
|
||||
Content: "BROWSE"
|
||||
Margin: 8
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
|
||||
// Export
|
||||
Button "ExportButton" {
|
||||
Click: ExportButton_Click
|
||||
|
@ -49,14 +74,6 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
|||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
|
||||
// Browse
|
||||
Button "BrowseButton" {
|
||||
Click: BrowseButton_Click
|
||||
Content: "BROWSE"
|
||||
Margin: 8
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
|
||||
// Cancel
|
||||
Button {
|
||||
Command: DialogHost.CloseDialogCommand
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
using DiscordChatExporter.Models;
|
||||
using DiscordChatExporter.ViewModels;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Microsoft.Win32;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Views
|
||||
{
|
||||
|
@ -17,39 +15,30 @@ namespace DiscordChatExporter.Views
|
|||
InitializeComponent();
|
||||
}
|
||||
|
||||
private string GetFilter()
|
||||
public void BrowseButton_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
var filters = new List<string>();
|
||||
foreach (var format in ViewModel.AvailableFormats)
|
||||
{
|
||||
var ext = format.GetFileExtension();
|
||||
filters.Add($"{format} (*.{ext})|*.{ext}");
|
||||
}
|
||||
// Get file extension of the selected format
|
||||
var ext = ViewModel.SelectedFormat.GetFileExtension();
|
||||
|
||||
return filters.JoinToString("|");
|
||||
// Open dialog
|
||||
var sfd = new SaveFileDialog
|
||||
{
|
||||
FileName = ViewModel.FilePath,
|
||||
Filter = $"{ext.ToUpperInvariant()} Files|*.{ext}|All Files|*.*",
|
||||
AddExtension = true,
|
||||
Title = "Select output file"
|
||||
};
|
||||
|
||||
// Assign new file path if dialog was successful
|
||||
if (sfd.ShowDialog() == true)
|
||||
{
|
||||
ViewModel.FilePath = sfd.FileName;
|
||||
}
|
||||
}
|
||||
|
||||
public void ExportButton_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
DialogHost.CloseDialogCommand.Execute(null, null);
|
||||
}
|
||||
|
||||
public void BrowseButton_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
var sfd = new SaveFileDialog
|
||||
{
|
||||
FileName = ViewModel.FilePath,
|
||||
Filter = GetFilter(),
|
||||
FilterIndex = ViewModel.AvailableFormats.IndexOf(ViewModel.SelectedFormat) + 1,
|
||||
AddExtension = true,
|
||||
Title = "Select output file"
|
||||
};
|
||||
|
||||
if (sfd.ShowDialog() == true)
|
||||
{
|
||||
ViewModel.FilePath = sfd.FileName;
|
||||
ViewModel.SelectedFormat = ViewModel.AvailableFormats[sfd.FilterIndex - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -46,6 +46,8 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
|||
FontSize: 16
|
||||
Text: bind Token
|
||||
set [ UpdateSourceTrigger: PropertyChanged ]
|
||||
TextFieldAssist.DecorationVisibility: Hidden
|
||||
TextFieldAssist.TextBoxViewMargin: "0 0 2 0"
|
||||
}
|
||||
|
||||
// Submit
|
||||
|
|
|
@ -5,19 +5,9 @@ UserControl "DiscordChatExporter.Views.SettingsDialog" {
|
|||
Width: 250
|
||||
|
||||
StackPanel {
|
||||
// Theme
|
||||
ComboBox {
|
||||
Margin: "16 16 16 8"
|
||||
HintAssist.Hint: "Theme"
|
||||
HintAssist.IsFloating: true
|
||||
IsReadOnly: true
|
||||
ItemsSource: bind AvailableThemes
|
||||
SelectedItem: bind Theme
|
||||
}
|
||||
|
||||
// Date format
|
||||
TextBox {
|
||||
Margin: "16 8 16 8"
|
||||
Margin: "16 16 16 8"
|
||||
HintAssist.Hint: "Date format"
|
||||
HintAssist.IsFloating: true
|
||||
Text: bind DateFormat
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue