mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-11 08:21:55 +02:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 152e8c8dcb | |||
| 260139ab25 | |||
| 11e9c0f15c | |||
| ac809d1a3f | |||
| 36fa346299 | |||
| 8c3a5bd096 | |||
| 021682899a | |||
| 0ec6027a93 | |||
| eb49d0bd16 | |||
| e7c2564f6f | |||
| d1b70cf5f6 | |||
| b73d6a78a9 | |||
| b9f04bc4d9 | |||
| d01ed13041 | |||
| ed35a36eaa | |||
| 2ddfb46c31 | |||
| 3c5beeba79 | |||
| ed915443e6 | |||
| f8d55f406c | |||
| 15b605f5a7 | |||
| 511af1e35c | |||
| 2fea455c64 | |||
| c64133f717 | |||
| d18d2e0307 | |||
| 86633b858c | |||
| c390c64316 | |||
| af3a59ce0f | |||
| eb89ea5b40 | |||
| ad3655396f | |||
| 1b8af364d3 | |||
| 58464d8708 | |||
| 026927265a | |||
| 390735032e | |||
| 461d474498 | |||
| 017ed5ae6d | |||
| 6f90c367b9 | |||
| f866c16355 | |||
| 072d4c1fe6 | |||
| 6365e0a080 | |||
| 7b140fd450 | |||
| e237e00600 | |||
| 3aef7faa1e | |||
| ebe4d58a42 | |||
| bed0ade732 | |||
| 06e89538cb | |||
| 7903869f9f | |||
| 32443c5a5b | |||
| d477159907 | |||
| 93578abe24 |
@@ -0,0 +1,55 @@
|
|||||||
|
name: "\U0001F41E Bug report"
|
||||||
|
description: Report broken functionality.
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Important:
|
||||||
|
|
||||||
|
- Please check existing issues (both opened and closed) to ensure that this bug hasn't been reported before.
|
||||||
|
- Refer to the [contribution guidelines](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/Contributing.md) for info on how to file a good issue.
|
||||||
|
- If you want to ask a question instead of reporting a bug, please use [discussions](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) instead.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: "Which version(s) of DiscordChatExporter does this bug affect?"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Flavor
|
||||||
|
description: "Which flavor(s) of DiscordChatExporter does this bug affect?"
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- GUI (Graphical User Interface)
|
||||||
|
- CLI (Command Line Interface)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Export format
|
||||||
|
description: "Which export format(s) does this bug affect? If this bug is not related to any specific export format, leave this blank."
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- HTML
|
||||||
|
- TXT
|
||||||
|
- JSON
|
||||||
|
- CSV
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Details
|
||||||
|
description: "Clear and thorough explanation of the bug. If relevant, include screenshots or screen recordings."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: "Minimum steps required to reproduce the bug."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: "\U0001F4D6 Read the wiki"
|
||||||
|
url: https://github.com/Tyrrrz/DiscordChatExporter/wiki
|
||||||
|
about: Find answers to most common questions.
|
||||||
|
- name: "\U0001F5E8 Ask a question"
|
||||||
|
url: https://github.com/Tyrrrz/DiscordChatExporter/discussions/new
|
||||||
|
about: Please ask and answer questions here.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: "\U00002728 Feature request"
|
||||||
|
description: Request a new feature.
|
||||||
|
labels: [enhancement]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Important:
|
||||||
|
|
||||||
|
- Please check existing issues (both opened and closed) to ensure that this feature hasn't been requested before.
|
||||||
|
- Refer to the [contribution guidelines](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/Contributing.md) for info on how to file a good issue.
|
||||||
|
- If you want to ask a question instead of requesting a feature, please use [discussions](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) instead.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Flavor
|
||||||
|
description: "Which flavor(s) of DiscordChatExporter does this feature request apply to? If this feature is not related to any specific flavor, leave this blank."
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- GUI (Graphical User Interface)
|
||||||
|
- CLI (Command Line Interface)
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Export format
|
||||||
|
description: "Which export format(s) does this feature request apply to? If this feature is not related to any specific export format, leave this blank."
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- HTML
|
||||||
|
- TXT
|
||||||
|
- JSON
|
||||||
|
- CSV
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Details
|
||||||
|
description: "Clear and thorough explanation of the feature you have in mind. If relevant, include screenshots or screen recordings."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -1,3 +1,19 @@
|
|||||||
|
### v2.27.1 (08-Jun-2021)
|
||||||
|
|
||||||
|
- [GUI] Application will now detect if the required .NET Runtime or any of its prerequisites are missing and prompt the user to download and install them automatically. **Experimental feature, please test it out and report any issues you may find!**
|
||||||
|
- [CLI] Fixed an issue where the application crashed when exporting a channel that had square brackets as part of its name or category name. (Thanks [@Lucas LaBuff](https://github.com/96-LB))
|
||||||
|
|
||||||
|
### v2.27 (24-Apr-2021)
|
||||||
|
|
||||||
|
- Added partitioning by file size. You can now use values such as `10mb` to indicate a size-based cut off point, in addition to values like `10` to indicate a number of messages. (Thanks [@Andrew Kolos](https://github.com/andrewkolos))
|
||||||
|
- [CLI] Improved formatting and progress reporting.
|
||||||
|
- [CLI] Changed `export` command so that it can accept multiple channel IDs passed to `-c|--channel` option. This makes it possible to export multiple channels at once while benefiting from parallelization.
|
||||||
|
- [JSON] Added `color` field to message author. This is a hex string that represents user's color, inherited from their roles. (Thanks [@rtm516](https://github.com/rtm516))
|
||||||
|
- [JSON] Added `categoryId` field to the root. This represents the ID of the category channel that the currently exported channel belongs to. (Thanks [@rtm516](https://github.com/rtm516))
|
||||||
|
- [JSON] Fixed an issue where user discriminator was written without leading zeroes. (Thanks [@Lucas LaBuff](https://github.com/96-LB))
|
||||||
|
- [JSON] Fixed an issue where the application sometimes crashed with an error saying `']' invalid without a matching open`, which was just a validation message that masked the actual error behind the failure. (Thanks [@Lucas LaBuff](https://github.com/96-LB))
|
||||||
|
- Fixed an issue where progress was not reported correctly if the channel didn't have any messages in the specified time range.
|
||||||
|
|
||||||
### v2.26.1 (06-Feb-2021)
|
### v2.26.1 (06-Feb-2021)
|
||||||
|
|
||||||
- [CLI] Added support for file name templates, which allow you to dynamically generate output file names based on channel and guild metadata. (Thanks [@Lucas LaBuff](https://github.com/96-LB))
|
- [CLI] Added support for file name templates, which allow you to dynamically generate output file names based on channel and guild metadata. (Thanks [@Lucas LaBuff](https://github.com/96-LB))
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
DiscordChatExporter accepts contributions in the form of issues and pull requests.
|
||||||
|
|
||||||
|
## Creating issues
|
||||||
|
|
||||||
|
If you have a feature suggestion or want to report a bug, you are welcome to create an issue.
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
|
||||||
|
- Avoid creating an issue if a similar one already exists. Look through existing open and closed issues first.
|
||||||
|
- Keep your issue focused on one specific problem. If you have multiple suggestions or bug reports, please create separate issues for them.
|
||||||
|
- Provide a descriptive title for your issue. Don't use generic titles like "A couple suggestions" or "Not working".
|
||||||
|
- Provide more context in the body of the issue. If relevant, attach screenshots or screen recordings.
|
||||||
|
- Remain civil and respectful when participating in discussions.
|
||||||
|
|
||||||
|
## Creating pull requests
|
||||||
|
|
||||||
|
If you want to contribute code to the project, you can do so by creating a pull request.
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
|
||||||
|
- Make sure that there is an existing issue that describes the problem solved by your pull request. If there isn't one, please create it first.
|
||||||
|
- Add a comment to the issue indicating that you want to submit a pull request for it. This can be a good opportunity to discuss possible solutions, finalize requirements, or ask questions.
|
||||||
|
- Keep your pull request focused and as small as possible. If you want to contribute multiple unrelated changes, please create separate pull requests for them.
|
||||||
|
- Follow the coding style and conventions already established by the project. When in doubt which style to use, ask in comments to your pull request.
|
||||||
|
- If you want to start a discussion regarding a specific change you made, add a review comment to your own code. This can be used to highlight something important or to seek input from others.
|
||||||
|
|
||||||
|
## Building the project locally
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- [.NET 5.0 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)
|
||||||
|
- [.NET 3.1 SDK](https://dotnet.microsoft.com/download/dotnet/3.1) (temporarily as the app still targets .NET 3.1, but requires .NET 5.0 to build)
|
||||||
|
- _(Optional)_ C#/.NET IDE, such as [JetBrains Rider](https://www.jetbrains.com/rider), [VS Code](https://code.visualstudio.com/docs/languages/csharp), or [Visual Studio](https://visualstudio.microsoft.com).
|
||||||
|
|
||||||
|
To build the entire solution run the following command in the root of the repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
> dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will generate runtime artifacts for each project:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
./DiscordChatExporter.Gui/bin/[Debug|Release]/[runtime]/*
|
||||||
|
./DiscordChatExporter.Cli/bin/[Debug|Release]/[runtime]/*
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also build and run a specific project directly.
|
||||||
|
To do that, navigate to its directory and use `dotnet run`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
> cd DiscordChatExporter.Gui
|
||||||
|
> dotnet run
|
||||||
|
```
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<Version>2.26.1</Version>
|
<Version>2.27.1</Version>
|
||||||
<Company>Tyrrrz</Company>
|
<Company>Tyrrrz</Company>
|
||||||
<Copyright>Copyright (c) Alexey Golub</Copyright>
|
<Copyright>Copyright (c) Alexey Golub</Copyright>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
@@ -10,4 +10,4 @@
|
|||||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
using System.IO;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Utilities;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Domain.Discord;
|
using DiscordChatExporter.Cli.Utils.Extensions;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Domain.Exporting;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
using DiscordChatExporter.Core.Exceptions;
|
||||||
|
using DiscordChatExporter.Core.Exporting;
|
||||||
|
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||||
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands.Base
|
namespace DiscordChatExporter.Cli.Commands.Base
|
||||||
{
|
{
|
||||||
@@ -24,8 +32,11 @@ namespace DiscordChatExporter.Cli.Commands.Base
|
|||||||
[CommandOption("before", Description = "Only include messages sent before this date or message ID.")]
|
[CommandOption("before", Description = "Only include messages sent before this date or message ID.")]
|
||||||
public Snowflake? Before { get; init; }
|
public Snowflake? Before { get; init; }
|
||||||
|
|
||||||
[CommandOption("partition", 'p', Description = "Split output into partitions limited to this number of messages.")]
|
[CommandOption("partition", 'p', Description = "Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb).")]
|
||||||
public int? PartitionLimit { get; init; }
|
public PartitionLimit PartitionLimit { get; init; } = NullPartitionLimit.Instance;
|
||||||
|
|
||||||
|
[CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")]
|
||||||
|
public int ParallelLimit { get; init; } = 1;
|
||||||
|
|
||||||
[CommandOption("media", Description = "Download referenced media content.")]
|
[CommandOption("media", Description = "Download referenced media content.")]
|
||||||
public bool ShouldDownloadMedia { get; init; }
|
public bool ShouldDownloadMedia { get; init; }
|
||||||
@@ -36,49 +47,96 @@ namespace DiscordChatExporter.Cli.Commands.Base
|
|||||||
[CommandOption("dateformat", Description = "Format used when writing dates.")]
|
[CommandOption("dateformat", Description = "Format used when writing dates.")]
|
||||||
public string DateFormat { get; init; } = "dd-MMM-yy hh:mm tt";
|
public string DateFormat { get; init; } = "dd-MMM-yy hh:mm tt";
|
||||||
|
|
||||||
protected ChannelExporter GetChannelExporter() => new(GetDiscordClient());
|
private ChannelExporter? _channelExporter;
|
||||||
|
protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord);
|
||||||
|
|
||||||
protected async ValueTask ExportAsync(IConsole console, Guild guild, Channel channel)
|
protected async ValueTask ExportAsync(IConsole console, IReadOnlyList<Channel> channels)
|
||||||
{
|
{
|
||||||
console.Output.Write($"Exporting channel '{channel.Category} / {channel.Name}'... ");
|
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
|
||||||
var progress = console.CreateProgressTicker();
|
|
||||||
|
|
||||||
var request = new ExportRequest(
|
var errors = new ConcurrentDictionary<Channel, string>();
|
||||||
guild,
|
|
||||||
channel,
|
|
||||||
OutputPath,
|
|
||||||
ExportFormat,
|
|
||||||
After,
|
|
||||||
Before,
|
|
||||||
PartitionLimit,
|
|
||||||
ShouldDownloadMedia,
|
|
||||||
ShouldReuseMedia,
|
|
||||||
DateFormat
|
|
||||||
);
|
|
||||||
|
|
||||||
await GetChannelExporter().ExportChannelAsync(request, progress);
|
// Wrap everything in a progress ticker
|
||||||
|
await console.CreateProgressTicker().StartAsync(async progressContext =>
|
||||||
|
{
|
||||||
|
await channels.ParallelForEachAsync(async channel =>
|
||||||
|
{
|
||||||
|
// Export
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await progressContext.StartTaskAsync($"{channel.Category} / {channel.Name}", async progress =>
|
||||||
|
{
|
||||||
|
var guild = await Discord.GetGuildAsync(channel.GuildId);
|
||||||
|
|
||||||
console.Output.WriteLine();
|
var request = new ExportRequest(
|
||||||
console.Output.WriteLine("Done.");
|
guild,
|
||||||
}
|
channel,
|
||||||
|
OutputPath,
|
||||||
|
ExportFormat,
|
||||||
|
After,
|
||||||
|
Before,
|
||||||
|
PartitionLimit,
|
||||||
|
ShouldDownloadMedia,
|
||||||
|
ShouldReuseMedia,
|
||||||
|
DateFormat
|
||||||
|
);
|
||||||
|
|
||||||
protected async ValueTask ExportAsync(IConsole console, Channel channel)
|
await Exporter.ExportChannelAsync(request, progress);
|
||||||
{
|
});
|
||||||
var guild = await GetDiscordClient().GetGuildAsync(channel.GuildId);
|
}
|
||||||
await ExportAsync(console, guild, channel);
|
catch (DiscordChatExporterException ex) when (!ex.IsCritical)
|
||||||
}
|
{
|
||||||
|
errors[channel] = ex.Message;
|
||||||
|
}
|
||||||
|
}, ParallelLimit.ClampMin(1));
|
||||||
|
});
|
||||||
|
|
||||||
protected async ValueTask ExportAsync(IConsole console, Snowflake channelId)
|
// Print result
|
||||||
{
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
var channel = await GetDiscordClient().GetChannelAsync(channelId);
|
{
|
||||||
await ExportAsync(console, channel);
|
await console.Output.WriteLineAsync(
|
||||||
|
$"Successfully exported {channels.Count - errors.Count} channel(s)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print errors
|
||||||
|
if (errors.Any())
|
||||||
|
{
|
||||||
|
await console.Output.WriteLineAsync();
|
||||||
|
|
||||||
|
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||||
|
{
|
||||||
|
await console.Output.WriteLineAsync(
|
||||||
|
$"Failed to export {errors.Count} channel(s):"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (channel, error) in errors)
|
||||||
|
{
|
||||||
|
await console.Output.WriteAsync($"{channel.Category} / {channel.Name}: ");
|
||||||
|
|
||||||
|
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||||
|
await console.Output.WriteLineAsync(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await console.Output.WriteLineAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail the command if ALL channels failed to export.
|
||||||
|
// Having some of the channels fail to export is fine and expected.
|
||||||
|
if (errors.Count >= channels.Count)
|
||||||
|
{
|
||||||
|
throw new CommandException("Export failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await console.Output.WriteLineAsync("Done.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ValueTask ExecuteAsync(IConsole console)
|
public override ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
if (ShouldReuseMedia && !ShouldDownloadMedia)
|
if (ShouldReuseMedia && !ShouldDownloadMedia)
|
||||||
{
|
{
|
||||||
throw new CommandException("The --reuse-media option cannot be used without the --media option.");
|
throw new CommandException("Option --reuse-media cannot be used without --media.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return default;
|
return default;
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Utilities;
|
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
|
||||||
using DiscordChatExporter.Domain.Exceptions;
|
|
||||||
using DiscordChatExporter.Domain.Exporting;
|
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
|
||||||
using Gress;
|
|
||||||
using Tyrrrz.Extensions;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands.Base
|
|
||||||
{
|
|
||||||
public abstract class ExportMultipleCommandBase : ExportCommandBase
|
|
||||||
{
|
|
||||||
[CommandOption("parallel", Description = "Limits how many channels can be exported in parallel.")]
|
|
||||||
public int ParallelLimit { get; init; } = 1;
|
|
||||||
|
|
||||||
protected async ValueTask ExportMultipleAsync(IConsole console, IReadOnlyList<Channel> channels)
|
|
||||||
{
|
|
||||||
// This uses a different route from ExportCommandBase.ExportAsync() because it runs
|
|
||||||
// in parallel and needs another way to report progress to console.
|
|
||||||
|
|
||||||
console.Output.Write($"Exporting {channels.Count} channels... ");
|
|
||||||
var progress = console.CreateProgressTicker();
|
|
||||||
|
|
||||||
var operations = progress.Wrap().CreateOperations(channels.Count);
|
|
||||||
|
|
||||||
var successfulExportCount = 0;
|
|
||||||
var errors = new ConcurrentBag<(Channel, string)>();
|
|
||||||
|
|
||||||
await channels.Zip(operations).ParallelForEachAsync(async tuple =>
|
|
||||||
{
|
|
||||||
var (channel, operation) = tuple;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var guild = await GetDiscordClient().GetGuildAsync(channel.GuildId);
|
|
||||||
|
|
||||||
var request = new ExportRequest(
|
|
||||||
guild,
|
|
||||||
channel,
|
|
||||||
OutputPath,
|
|
||||||
ExportFormat,
|
|
||||||
After,
|
|
||||||
Before,
|
|
||||||
PartitionLimit,
|
|
||||||
ShouldDownloadMedia,
|
|
||||||
ShouldReuseMedia,
|
|
||||||
DateFormat
|
|
||||||
);
|
|
||||||
|
|
||||||
await GetChannelExporter().ExportChannelAsync(request, operation);
|
|
||||||
|
|
||||||
Interlocked.Increment(ref successfulExportCount);
|
|
||||||
}
|
|
||||||
catch (DiscordChatExporterException ex) when (!ex.IsCritical)
|
|
||||||
{
|
|
||||||
errors.Add((channel, ex.Message));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
operation.Dispose();
|
|
||||||
}
|
|
||||||
}, ParallelLimit.ClampMin(1));
|
|
||||||
|
|
||||||
console.Output.WriteLine();
|
|
||||||
|
|
||||||
foreach (var (channel, error) in errors)
|
|
||||||
console.Error.WriteLine($"Channel '{channel}': {error}");
|
|
||||||
|
|
||||||
console.Output.WriteLine($"Successfully exported {successfulExportCount} channel(s).");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using DiscordChatExporter.Domain.Discord;
|
using CliFx.Infrastructure;
|
||||||
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands.Base
|
namespace DiscordChatExporter.Cli.Commands.Base
|
||||||
{
|
{
|
||||||
public abstract class TokenCommandBase : ICommand
|
public abstract class TokenCommandBase : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption("token", 't', IsRequired = true, EnvironmentVariableName = "DISCORD_TOKEN", Description = "Authorization token.")]
|
[CommandOption("token", 't', IsRequired = true, EnvironmentVariable = "DISCORD_TOKEN", Description = "Authentication token.")]
|
||||||
public string TokenValue { get; init; } = "";
|
public string TokenValue { get; init; } = "";
|
||||||
|
|
||||||
[CommandOption("bot", 'b', EnvironmentVariableName = "DISCORD_TOKEN_BOT", Description = "Authorize as a bot.")]
|
[CommandOption("bot", 'b', EnvironmentVariable = "DISCORD_TOKEN_BOT", Description = "Authenticate as a bot.")]
|
||||||
public bool IsBotToken { get; init; }
|
public bool IsBotToken { get; init; }
|
||||||
|
|
||||||
protected AuthToken GetAuthToken() => new(
|
private AuthToken GetAuthToken() => new(
|
||||||
IsBotToken
|
IsBotToken
|
||||||
? AuthTokenType.Bot
|
? AuthTokenType.Bot
|
||||||
: AuthTokenType.User,
|
: AuthTokenType.User,
|
||||||
TokenValue
|
TokenValue
|
||||||
);
|
);
|
||||||
|
|
||||||
protected DiscordClient GetDiscordClient() => new(GetAuthToken());
|
private DiscordClient? _discordClient;
|
||||||
|
protected DiscordClient Discord => _discordClient ??= new DiscordClient(GetAuthToken());
|
||||||
|
|
||||||
public abstract ValueTask ExecuteAsync(IConsole console);
|
public abstract ValueTask ExecuteAsync(IConsole console);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,42 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
[Command("exportall", Description = "Export all accessible channels.")]
|
[Command("exportall", Description = "Export all accessible channels.")]
|
||||||
public class ExportAllCommand : ExportMultipleCommandBase
|
public class ExportAllCommand : ExportCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("include-dm", Description = "Include direct message channels.")]
|
[CommandOption("include-dm", Description = "Include direct message channels.")]
|
||||||
public bool IncludeDirectMessages { get; init; } = true;
|
public bool IncludeDirectMessages { get; init; } = true;
|
||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
await base.ExecuteAsync(console);
|
||||||
|
|
||||||
|
// Get channel metadata
|
||||||
|
await console.Output.WriteLineAsync("Fetching channels...");
|
||||||
|
|
||||||
var channels = new List<Channel>();
|
var channels = new List<Channel>();
|
||||||
|
|
||||||
// Aggregate channels from all guilds
|
// Aggregate channels from all guilds
|
||||||
await foreach (var guild in GetDiscordClient().GetUserGuildsAsync())
|
await foreach (var guild in Discord.GetUserGuildsAsync())
|
||||||
{
|
{
|
||||||
// Skip DMs if instructed to
|
// Skip DMs if instructed to
|
||||||
if (!IncludeDirectMessages && guild.Id == Guild.DirectMessages.Id)
|
if (!IncludeDirectMessages && guild.Id == Guild.DirectMessages.Id)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
await foreach (var channel in GetDiscordClient().GetGuildChannelsAsync(guild.Id))
|
await foreach (var channel in Discord.GetGuildChannelsAsync(guild.Id))
|
||||||
{
|
{
|
||||||
channels.Add(channel);
|
channels.Add(channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ExportMultipleAsync(console, channels);
|
// Export
|
||||||
|
await ExportAsync(console, channels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
|
||||||
using DiscordChatExporter.Domain.Discord;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
|
||||||
{
|
|
||||||
[Command("export", Description = "Export a channel.")]
|
|
||||||
public class ExportChannelCommand : ExportCommandBase
|
|
||||||
{
|
|
||||||
[CommandOption("channel", 'c', IsRequired = true, Description = "Channel ID.")]
|
|
||||||
public Snowflake ChannelId { get; init; }
|
|
||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
await base.ExecuteAsync(console);
|
|
||||||
await ExportAsync(console, ChannelId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
|
{
|
||||||
|
[Command("export", Description = "Export one or multiple channels.")]
|
||||||
|
public class ExportChannelsCommand : ExportCommandBase
|
||||||
|
{
|
||||||
|
// TODO: change this to plural (breaking change)
|
||||||
|
[CommandOption("channel", 'c', IsRequired = true, Description = "Channel ID(s).")]
|
||||||
|
public IReadOnlyList<Snowflake> ChannelIds { get; init; } = Array.Empty<Snowflake>();
|
||||||
|
|
||||||
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
await base.ExecuteAsync(console);
|
||||||
|
|
||||||
|
// Get channel metadata
|
||||||
|
await console.Output.WriteLineAsync("Fetching channel(s)...");
|
||||||
|
|
||||||
|
var channels = new List<Channel>();
|
||||||
|
|
||||||
|
foreach (var channelId in ChannelIds)
|
||||||
|
{
|
||||||
|
var channel = await Discord.GetChannelAsync(channelId);
|
||||||
|
channels.Add(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export
|
||||||
|
await ExportAsync(console, channels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
[Command("exportdm", Description = "Export all direct message channels.")]
|
[Command("exportdm", Description = "Export all direct message channels.")]
|
||||||
public class ExportDirectMessagesCommand : ExportMultipleCommandBase
|
public class ExportDirectMessagesCommand : ExportCommandBase
|
||||||
{
|
{
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var channels = await GetDiscordClient().GetGuildChannelsAsync(Guild.DirectMessages.Id);
|
await base.ExecuteAsync(console);
|
||||||
await ExportMultipleAsync(console, channels);
|
|
||||||
|
// Get channel metadata
|
||||||
|
await console.Output.WriteLineAsync("Fetching channels...");
|
||||||
|
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id);
|
||||||
|
|
||||||
|
// Export
|
||||||
|
await ExportAsync(console, channels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,28 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Domain.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
[Command("exportguild", Description = "Export all channels within specified guild.")]
|
[Command("exportguild", Description = "Export all channels within specified guild.")]
|
||||||
public class ExportGuildCommand : ExportMultipleCommandBase
|
public class ExportGuildCommand : ExportCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")]
|
[CommandOption("guild", 'g', IsRequired = true, Description = "Guild ID.")]
|
||||||
public Snowflake GuildId { get; init; }
|
public Snowflake GuildId { get; init; }
|
||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var channels = await GetDiscordClient().GetGuildChannelsAsync(GuildId);
|
await base.ExecuteAsync(console);
|
||||||
await ExportMultipleAsync(console, channels);
|
|
||||||
|
// Get channel metadata
|
||||||
|
await console.Output.WriteLineAsync("Fetching channels...");
|
||||||
|
var channels = await Discord.GetGuildChannelsAsync(GuildId);
|
||||||
|
|
||||||
|
// Export
|
||||||
|
await ExportAsync(console, channels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
using System.Linq;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Domain.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
@@ -17,10 +17,23 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var channels = await GetDiscordClient().GetGuildChannelsAsync(GuildId);
|
var channels = await Discord.GetGuildChannelsAsync(GuildId);
|
||||||
|
|
||||||
foreach (var channel in channels.OrderBy(c => c.Category, PositionBasedComparer.Instance).ThenBy(c => c.Name))
|
foreach (var channel in channels.OrderBy(c => c.Category.Position).ThenBy(c => c.Name))
|
||||||
console.Output.WriteLine($"{channel.Id} | {channel.Category} / {channel.Name}");
|
{
|
||||||
|
// Channel ID
|
||||||
|
await console.Output.WriteAsync(channel.Id.ToString());
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
using (console.WithForegroundColor(ConsoleColor.DarkGray))
|
||||||
|
await console.Output.WriteAsync(" | ");
|
||||||
|
|
||||||
|
// Channel category / name
|
||||||
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
|
await console.Output.WriteAsync($"{channel.Category} / {channel.Name}");
|
||||||
|
|
||||||
|
await console.Output.WriteLineAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
using System.Linq;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
@@ -14,10 +14,23 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
{
|
{
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var channels = await GetDiscordClient().GetGuildChannelsAsync(Guild.DirectMessages.Id);
|
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id);
|
||||||
|
|
||||||
foreach (var channel in channels.OrderBy(c => c.Name))
|
foreach (var channel in channels.OrderBy(c => c.Name))
|
||||||
console.Output.WriteLine($"{channel.Id} | {channel.Category} / {channel.Name}");
|
{
|
||||||
|
// Channel ID
|
||||||
|
await console.Output.WriteAsync(channel.Id.ToString());
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
using (console.WithForegroundColor(ConsoleColor.DarkGray))
|
||||||
|
await console.Output.WriteAsync(" | ");
|
||||||
|
|
||||||
|
// Channel category / name
|
||||||
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
|
await console.Output.WriteAsync($"{channel.Category} / {channel.Name}");
|
||||||
|
|
||||||
|
await console.Output.WriteLineAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
using System.Linq;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
@@ -12,10 +13,23 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
{
|
{
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
var guilds = await GetDiscordClient().GetUserGuildsAsync();
|
var guilds = await Discord.GetUserGuildsAsync();
|
||||||
|
|
||||||
foreach (var guild in guilds.OrderBy(g => g.Name))
|
foreach (var guild in guilds.OrderBy(g => g.Name))
|
||||||
console.Output.WriteLine($"{guild.Id} | {guild.Name}");
|
{
|
||||||
|
// Guild ID
|
||||||
|
await console.Output.WriteAsync(guild.Id.ToString());
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
using (console.WithForegroundColor(ConsoleColor.DarkGray))
|
||||||
|
await console.Output.WriteAsync(" | ");
|
||||||
|
|
||||||
|
// Guild name
|
||||||
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
|
await console.Output.WriteAsync(guild.Name);
|
||||||
|
|
||||||
|
await console.Output.WriteLineAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands
|
namespace DiscordChatExporter.Cli.Commands
|
||||||
{
|
{
|
||||||
@@ -10,9 +11,9 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
{
|
{
|
||||||
public ValueTask ExecuteAsync(IConsole console)
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
console.WithForegroundColor(ConsoleColor.White, () =>
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
console.Output.WriteLine("To get user token:")
|
console.Output.WriteLine("To get user token:");
|
||||||
);
|
|
||||||
console.Output.WriteLine(" 1. Open Discord");
|
console.Output.WriteLine(" 1. Open Discord");
|
||||||
console.Output.WriteLine(" 2. Press Ctrl+Shift+I to show developer tools");
|
console.Output.WriteLine(" 2. Press Ctrl+Shift+I to show developer tools");
|
||||||
console.Output.WriteLine(" 3. Navigate to the Application tab");
|
console.Output.WriteLine(" 3. Navigate to the Application tab");
|
||||||
@@ -22,18 +23,18 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
console.Output.WriteLine(" * Automating user accounts is technically against TOS, use at your own risk.");
|
console.Output.WriteLine(" * Automating user accounts is technically against TOS, use at your own risk.");
|
||||||
console.Output.WriteLine();
|
console.Output.WriteLine();
|
||||||
|
|
||||||
console.WithForegroundColor(ConsoleColor.White, () =>
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
console.Output.WriteLine("To get bot token:")
|
console.Output.WriteLine("To get bot token:");
|
||||||
);
|
|
||||||
console.Output.WriteLine(" 1. Go to Discord developer portal");
|
console.Output.WriteLine(" 1. Go to Discord developer portal");
|
||||||
console.Output.WriteLine(" 2. Open your application's settings");
|
console.Output.WriteLine(" 2. Open your application's settings");
|
||||||
console.Output.WriteLine(" 3. Navigate to the Bot section on the left");
|
console.Output.WriteLine(" 3. Navigate to the Bot section on the left");
|
||||||
console.Output.WriteLine(" 4. Under Token click Copy");
|
console.Output.WriteLine(" 4. Under Token click Copy");
|
||||||
console.Output.WriteLine();
|
console.Output.WriteLine();
|
||||||
|
|
||||||
console.WithForegroundColor(ConsoleColor.White, () =>
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
console.Output.WriteLine("To get guild ID or guild channel ID:")
|
console.Output.WriteLine("To get guild ID or guild channel ID:");
|
||||||
);
|
|
||||||
console.Output.WriteLine(" 1. Open Discord");
|
console.Output.WriteLine(" 1. Open Discord");
|
||||||
console.Output.WriteLine(" 2. Open Settings");
|
console.Output.WriteLine(" 2. Open Settings");
|
||||||
console.Output.WriteLine(" 3. Go to Appearance section");
|
console.Output.WriteLine(" 3. Go to Appearance section");
|
||||||
@@ -41,9 +42,9 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
console.Output.WriteLine(" 5. Right click on the desired guild or channel and click Copy ID");
|
console.Output.WriteLine(" 5. Right click on the desired guild or channel and click Copy ID");
|
||||||
console.Output.WriteLine();
|
console.Output.WriteLine();
|
||||||
|
|
||||||
console.WithForegroundColor(ConsoleColor.White, () =>
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
console.Output.WriteLine("To get direct message channel ID:")
|
console.Output.WriteLine("To get direct message channel ID:");
|
||||||
);
|
|
||||||
console.Output.WriteLine(" 1. Open Discord");
|
console.Output.WriteLine(" 1. Open Discord");
|
||||||
console.Output.WriteLine(" 2. Open the desired direct message channel");
|
console.Output.WriteLine(" 2. Open the desired direct message channel");
|
||||||
console.Output.WriteLine(" 3. Press Ctrl+Shift+I to show developer tools");
|
console.Output.WriteLine(" 3. Press Ctrl+Shift+I to show developer tools");
|
||||||
@@ -52,12 +53,11 @@ namespace DiscordChatExporter.Cli.Commands
|
|||||||
console.Output.WriteLine(" 6. Copy the first long sequence of numbers inside the URL");
|
console.Output.WriteLine(" 6. Copy the first long sequence of numbers inside the URL");
|
||||||
console.Output.WriteLine();
|
console.Output.WriteLine();
|
||||||
|
|
||||||
console.WithForegroundColor(ConsoleColor.White,
|
using (console.WithForegroundColor(ConsoleColor.White))
|
||||||
() => console.Output.WriteLine("For more information, check out the wiki:")
|
console.Output.WriteLine("For more information, check out the wiki:");
|
||||||
);
|
|
||||||
console.WithForegroundColor(ConsoleColor.Blue,
|
using (console.WithForegroundColor(ConsoleColor.DarkCyan))
|
||||||
() => console.Output.WriteLine("https://github.com/Tyrrrz/DiscordChatExporter/wiki")
|
console.Output.WriteLine("https://github.com/Tyrrrz/DiscordChatExporter/wiki");
|
||||||
);
|
|
||||||
|
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CliFx" Version="1.6.0" />
|
<PackageReference Include="CliFx" Version="2.0.4" />
|
||||||
|
<PackageReference Include="Spectre.Console" Version="0.39.0" />
|
||||||
<PackageReference Include="Gress" Version="1.2.0" />
|
<PackageReference Include="Gress" Version="1.2.0" />
|
||||||
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
|
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DiscordChatExporter.Domain\DiscordChatExporter.Domain.csproj" />
|
<ProjectReference Include="..\DiscordChatExporter.Core\DiscordChatExporter.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Utils.Extensions
|
||||||
|
{
|
||||||
|
internal static class ConsoleExtensions
|
||||||
|
{
|
||||||
|
public static IAnsiConsole CreateAnsiConsole(this IConsole console) => AnsiConsole.Create(
|
||||||
|
new AnsiConsoleSettings
|
||||||
|
{
|
||||||
|
Ansi = AnsiSupport.Detect,
|
||||||
|
ColorSystem = ColorSystemSupport.Detect,
|
||||||
|
Out = new AnsiConsoleOutput(console.Output)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public static Progress CreateProgressTicker(this IConsole console) => console
|
||||||
|
.CreateAnsiConsole()
|
||||||
|
.Progress()
|
||||||
|
.AutoClear(false)
|
||||||
|
.AutoRefresh(true)
|
||||||
|
.HideCompleted(false)
|
||||||
|
.Columns(
|
||||||
|
new TaskDescriptionColumn {Alignment = Justify.Left},
|
||||||
|
new ProgressBarColumn(),
|
||||||
|
new PercentageColumn()
|
||||||
|
);
|
||||||
|
|
||||||
|
public static async ValueTask StartTaskAsync(
|
||||||
|
this ProgressContext progressContext,
|
||||||
|
string description,
|
||||||
|
Func<ProgressTask, ValueTask> performOperationAsync)
|
||||||
|
{
|
||||||
|
var progressTask = progressContext.AddTask(
|
||||||
|
// Don't recognize random square brackets as style tags
|
||||||
|
Markup.Escape(description),
|
||||||
|
new ProgressTaskSettings {MaxValue = 1}
|
||||||
|
);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await performOperationAsync(progressTask);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
progressTask.StopTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord
|
namespace DiscordChatExporter.Core.Discord
|
||||||
{
|
{
|
||||||
public enum AuthTokenType { User, Bot }
|
public enum AuthTokenType { User, Bot }
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ namespace DiscordChatExporter.Domain.Discord
|
|||||||
Value = value;
|
Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthenticationHeaderValue GetAuthorizationHeader() => Type switch
|
public AuthenticationHeaderValue GetAuthenticationHeader() => Type switch
|
||||||
{
|
{
|
||||||
AuthTokenType.Bot => new AuthenticationHeaderValue("Bot", Value),
|
AuthTokenType.Bot => new AuthenticationHeaderValue("Bot", Value),
|
||||||
_ => new AuthenticationHeaderValue(Value)
|
_ => new AuthenticationHeaderValue(Value)
|
||||||
+10
-4
@@ -2,11 +2,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/channel#attachment-object
|
// https://discord.com/developers/docs/resources/channel#attachment-object
|
||||||
public partial class Attachment : IHasId
|
public partial class Attachment : IHasId
|
||||||
@@ -32,7 +32,13 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
|||||||
|
|
||||||
public FileSize FileSize { get; }
|
public FileSize FileSize { get; }
|
||||||
|
|
||||||
public Attachment(Snowflake id, string url, string fileName, int? width, int? height, FileSize fileSize)
|
public Attachment(
|
||||||
|
Snowflake id,
|
||||||
|
string url,
|
||||||
|
string fileName,
|
||||||
|
int? width,
|
||||||
|
int? height,
|
||||||
|
FileSize fileSize)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Url = url;
|
Url = url;
|
||||||
+29
-23
@@ -1,12 +1,11 @@
|
|||||||
using System;
|
using System.Linq;
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||||
// Order of enum fields needs to match the order in the docs.
|
// Order of enum fields needs to match the order in the docs.
|
||||||
@@ -22,7 +21,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://discord.com/developers/docs/resources/channel#channel-object
|
// https://discord.com/developers/docs/resources/channel#channel-object
|
||||||
public partial class Channel : IHasIdAndPosition
|
public partial class Channel : IHasId, IHasPosition
|
||||||
{
|
{
|
||||||
public Snowflake Id { get; }
|
public Snowflake Id { get; }
|
||||||
|
|
||||||
@@ -45,12 +44,19 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
|||||||
|
|
||||||
public string? Topic { get; }
|
public string? Topic { get; }
|
||||||
|
|
||||||
public Channel(Snowflake id, ChannelType type, Snowflake guildId, ChannelCategory? category, string name, int? position, string? topic)
|
public Channel(
|
||||||
|
Snowflake id,
|
||||||
|
ChannelType type,
|
||||||
|
Snowflake guildId,
|
||||||
|
ChannelCategory? category,
|
||||||
|
string name,
|
||||||
|
int? position,
|
||||||
|
string? topic)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Type = type;
|
Type = type;
|
||||||
GuildId = guildId;
|
GuildId = guildId;
|
||||||
Category = category ?? GetDefaultCategory(type);
|
Category = category ?? GetFallbackCategory(type);
|
||||||
Name = name;
|
Name = name;
|
||||||
Position = position;
|
Position = position;
|
||||||
Topic = topic;
|
Topic = topic;
|
||||||
@@ -62,19 +68,19 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
|||||||
|
|
||||||
public partial class Channel
|
public partial class Channel
|
||||||
{
|
{
|
||||||
private static ChannelCategory GetDefaultCategory(ChannelType channelType) => new(
|
private static ChannelCategory GetFallbackCategory(ChannelType channelType) => new(
|
||||||
Snowflake.Zero,
|
Snowflake.Zero,
|
||||||
channelType switch
|
channelType switch
|
||||||
{
|
{
|
||||||
ChannelType.GuildTextChat => "Text",
|
ChannelType.GuildTextChat => "Text",
|
||||||
ChannelType.DirectTextChat => "Private",
|
ChannelType.DirectTextChat => "Private",
|
||||||
ChannelType.DirectGroupTextChat => "Group",
|
ChannelType.DirectGroupTextChat => "Group",
|
||||||
ChannelType.GuildNews => "News",
|
ChannelType.GuildNews => "News",
|
||||||
ChannelType.GuildStore => "Store",
|
ChannelType.GuildStore => "Store",
|
||||||
_ => "Default"
|
_ => "Default"
|
||||||
},
|
},
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
public static Channel Parse(JsonElement json, ChannelCategory? category = null, int? position = null)
|
public static Channel Parse(JsonElement json, ChannelCategory? category = null, int? position = null)
|
||||||
{
|
{
|
||||||
@@ -82,7 +88,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
|||||||
var guildId = json.GetPropertyOrNull("guild_id")?.GetString().Pipe(Snowflake.Parse);
|
var guildId = json.GetPropertyOrNull("guild_id")?.GetString().Pipe(Snowflake.Parse);
|
||||||
var topic = json.GetPropertyOrNull("topic")?.GetString();
|
var topic = json.GetPropertyOrNull("topic")?.GetString();
|
||||||
|
|
||||||
var type = (ChannelType)json.GetProperty("type").GetInt32();
|
var type = (ChannelType) json.GetProperty("type").GetInt32();
|
||||||
|
|
||||||
var name =
|
var name =
|
||||||
json.GetPropertyOrNull("name")?.GetString() ??
|
json.GetPropertyOrNull("name")?.GetString() ??
|
||||||
@@ -95,7 +101,7 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
|||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
guildId ?? Guild.DirectMessages.Id,
|
guildId ?? Guild.DirectMessages.Id,
|
||||||
category ?? GetDefaultCategory(type),
|
category ?? GetFallbackCategory(type),
|
||||||
name,
|
name,
|
||||||
position,
|
position,
|
||||||
topic
|
topic
|
||||||
+6
-7
@@ -1,14 +1,13 @@
|
|||||||
using System;
|
using System.Linq;
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
public partial class ChannelCategory : IHasIdAndPosition
|
public partial class ChannelCategory : IHasId, IHasPosition
|
||||||
{
|
{
|
||||||
public Snowflake Id { get; }
|
public Snowflake Id { get; }
|
||||||
|
|
||||||
@@ -45,6 +44,6 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ChannelCategory Empty { get; } = new(Snowflake.Zero, "Missing", 0);
|
public static ChannelCategory Empty { get; } = new(Snowflake.Zero, "<unknown category>", 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-15
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models.Common
|
namespace DiscordChatExporter.Core.Discord.Data.Common
|
||||||
{
|
{
|
||||||
// Loosely based on https://github.com/omar/ByteSize (MIT license)
|
// Loosely based on https://github.com/omar/ByteSize (MIT license)
|
||||||
public readonly partial struct FileSize
|
public readonly partial struct FileSize
|
||||||
@@ -10,19 +10,11 @@ namespace DiscordChatExporter.Domain.Discord.Models.Common
|
|||||||
public double TotalKiloBytes => TotalBytes / 1024.0;
|
public double TotalKiloBytes => TotalBytes / 1024.0;
|
||||||
public double TotalMegaBytes => TotalKiloBytes / 1024.0;
|
public double TotalMegaBytes => TotalKiloBytes / 1024.0;
|
||||||
public double TotalGigaBytes => TotalMegaBytes / 1024.0;
|
public double TotalGigaBytes => TotalMegaBytes / 1024.0;
|
||||||
public double TotalTeraBytes => TotalGigaBytes / 1024.0;
|
|
||||||
public double TotalPetaBytes => TotalTeraBytes / 1024.0;
|
|
||||||
|
|
||||||
public FileSize(long bytes) => TotalBytes = bytes;
|
public FileSize(long bytes) => TotalBytes = bytes;
|
||||||
|
|
||||||
private double GetLargestWholeNumberValue()
|
private double GetLargestWholeNumberValue()
|
||||||
{
|
{
|
||||||
if (Math.Abs(TotalPetaBytes) >= 1)
|
|
||||||
return TotalPetaBytes;
|
|
||||||
|
|
||||||
if (Math.Abs(TotalTeraBytes) >= 1)
|
|
||||||
return TotalTeraBytes;
|
|
||||||
|
|
||||||
if (Math.Abs(TotalGigaBytes) >= 1)
|
if (Math.Abs(TotalGigaBytes) >= 1)
|
||||||
return TotalGigaBytes;
|
return TotalGigaBytes;
|
||||||
|
|
||||||
@@ -37,12 +29,6 @@ namespace DiscordChatExporter.Domain.Discord.Models.Common
|
|||||||
|
|
||||||
private string GetLargestWholeNumberSymbol()
|
private string GetLargestWholeNumberSymbol()
|
||||||
{
|
{
|
||||||
if (Math.Abs(TotalPetaBytes) >= 1)
|
|
||||||
return "PB";
|
|
||||||
|
|
||||||
if (Math.Abs(TotalTeraBytes) >= 1)
|
|
||||||
return "TB";
|
|
||||||
|
|
||||||
if (Math.Abs(TotalGigaBytes) >= 1)
|
if (Math.Abs(TotalGigaBytes) >= 1)
|
||||||
return "GB";
|
return "GB";
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Domain.Discord.Models.Common
|
namespace DiscordChatExporter.Core.Discord.Data.Common
|
||||||
{
|
{
|
||||||
public interface IHasId
|
public interface IHasId
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Discord.Data.Common
|
||||||
|
{
|
||||||
|
public interface IHasPosition
|
||||||
|
{
|
||||||
|
int? Position { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-3
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models.Common
|
namespace DiscordChatExporter.Core.Discord.Data.Common
|
||||||
{
|
{
|
||||||
public partial class IdBasedEqualityComparer : IEqualityComparer<IHasId>
|
public partial class IdBasedEqualityComparer : IEqualityComparer<IHasId>
|
||||||
{
|
{
|
||||||
+2
-3
@@ -3,11 +3,10 @@ using System.Collections.Generic;
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/channel#embed-object
|
// https://discord.com/developers/docs/resources/channel#embed-object
|
||||||
public partial class Embed
|
public partial class Embed
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure
|
// https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure
|
||||||
public partial class EmbedAuthor
|
public partial class EmbedAuthor
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure
|
// https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure
|
||||||
public partial class EmbedField
|
public partial class EmbedField
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure
|
// https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure
|
||||||
public partial class EmbedFooter
|
public partial class EmbedFooter
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure
|
// https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure
|
||||||
public partial class EmbedImage
|
public partial class EmbedImage
|
||||||
+1
-1
@@ -5,7 +5,7 @@ using System.Text.Json;
|
|||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/emoji#emoji-object
|
// https://discord.com/developers/docs/resources/emoji#emoji-object
|
||||||
public partial class Emoji
|
public partial class Emoji
|
||||||
+8
-4
@@ -1,8 +1,8 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/guild#guild-object
|
// https://discord.com/developers/docs/resources/guild#guild-object
|
||||||
public partial class Guild : IHasId
|
public partial class Guild : IHasId
|
||||||
@@ -25,7 +25,11 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
|||||||
|
|
||||||
public partial class Guild
|
public partial class Guild
|
||||||
{
|
{
|
||||||
public static Guild DirectMessages { get; } = new(Snowflake.Zero, "Direct Messages", GetDefaultIconUrl());
|
public static Guild DirectMessages { get; } = new(
|
||||||
|
Snowflake.Zero,
|
||||||
|
"Direct Messages",
|
||||||
|
GetDefaultIconUrl()
|
||||||
|
);
|
||||||
|
|
||||||
private static string GetDefaultIconUrl() =>
|
private static string GetDefaultIconUrl() =>
|
||||||
"https://cdn.discordapp.com/embed/avatars/0.png";
|
"https://cdn.discordapp.com/embed/avatars/0.png";
|
||||||
+8
-4
@@ -2,11 +2,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/guild#guild-member-object
|
// https://discord.com/developers/docs/resources/guild#guild-member-object
|
||||||
public partial class Member : IHasId
|
public partial class Member : IHasId
|
||||||
@@ -31,7 +31,11 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
|||||||
|
|
||||||
public partial class Member
|
public partial class Member
|
||||||
{
|
{
|
||||||
public static Member CreateForUser(User user) => new(user, null, Array.Empty<Snowflake>());
|
public static Member CreateForUser(User user) => new(
|
||||||
|
user,
|
||||||
|
null,
|
||||||
|
Array.Empty<Snowflake>()
|
||||||
|
);
|
||||||
|
|
||||||
public static Member Parse(JsonElement json)
|
public static Member Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/channel#message-object-message-types
|
// https://discord.com/developers/docs/resources/channel#message-object-message-types
|
||||||
public enum MessageType
|
public enum MessageType
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure
|
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure
|
||||||
public partial class MessageReference
|
public partial class MessageReference
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/channel#reaction-object
|
// https://discord.com/developers/docs/resources/channel#reaction-object
|
||||||
public partial class Reaction
|
public partial class Reaction
|
||||||
+8
-5
@@ -1,11 +1,10 @@
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/topics/permissions#role-object
|
// https://discord.com/developers/docs/topics/permissions#role-object
|
||||||
public partial class Role : IHasId
|
public partial class Role : IHasId
|
||||||
@@ -18,7 +17,11 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
|||||||
|
|
||||||
public Color? Color { get; }
|
public Color? Color { get; }
|
||||||
|
|
||||||
public Role(Snowflake id, string name, int position, Color? color)
|
public Role(
|
||||||
|
Snowflake id,
|
||||||
|
string name,
|
||||||
|
int position,
|
||||||
|
Color? color)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
+12
-5
@@ -1,10 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord.Models
|
namespace DiscordChatExporter.Core.Discord.Data
|
||||||
{
|
{
|
||||||
// https://discord.com/developers/docs/resources/user#user-object
|
// https://discord.com/developers/docs/resources/user#user-object
|
||||||
public partial class User : IHasId
|
public partial class User : IHasId
|
||||||
@@ -15,13 +15,20 @@ namespace DiscordChatExporter.Domain.Discord.Models
|
|||||||
|
|
||||||
public int Discriminator { get; }
|
public int Discriminator { get; }
|
||||||
|
|
||||||
|
public string DiscriminatorFormatted => $"{Discriminator:0000}";
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public string FullName => $"{Name}#{Discriminator:0000}";
|
public string FullName => $"{Name}#{DiscriminatorFormatted}";
|
||||||
|
|
||||||
public string AvatarUrl { get; }
|
public string AvatarUrl { get; }
|
||||||
|
|
||||||
public User(Snowflake id, bool isBot, int discriminator, string name, string avatarUrl)
|
public User(
|
||||||
|
Snowflake id,
|
||||||
|
bool isBot,
|
||||||
|
int discriminator,
|
||||||
|
string name,
|
||||||
|
string avatarUrl)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
IsBot = isBot;
|
IsBot = isBot;
|
||||||
+29
-15
@@ -5,14 +5,15 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Exceptions;
|
using DiscordChatExporter.Core.Exceptions;
|
||||||
using DiscordChatExporter.Domain.Internal;
|
using DiscordChatExporter.Core.Utils;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Http;
|
using JsonExtensions.Http;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord
|
namespace DiscordChatExporter.Core.Discord
|
||||||
{
|
{
|
||||||
public class DiscordClient
|
public class DiscordClient
|
||||||
{
|
{
|
||||||
@@ -34,7 +35,7 @@ namespace DiscordChatExporter.Domain.Discord
|
|||||||
await Http.ResponsePolicy.ExecuteAsync(async () =>
|
await Http.ResponsePolicy.ExecuteAsync(async () =>
|
||||||
{
|
{
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
|
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
|
||||||
request.Headers.Authorization = _token.GetAuthorizationHeader();
|
request.Headers.Authorization = _token.GetAuthenticationHeader();
|
||||||
|
|
||||||
return await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
return await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
});
|
});
|
||||||
@@ -120,7 +121,8 @@ namespace DiscordChatExporter.Domain.Discord
|
|||||||
var orderedResponse = response
|
var orderedResponse = response
|
||||||
.EnumerateArray()
|
.EnumerateArray()
|
||||||
.OrderBy(j => j.GetProperty("position").GetInt32())
|
.OrderBy(j => j.GetProperty("position").GetInt32())
|
||||||
.ThenBy(j => ulong.Parse(j.GetProperty("id").GetString()));
|
.ThenBy(j => ulong.Parse(j.GetProperty("id").GetString()))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
var categories = orderedResponse
|
var categories = orderedResponse
|
||||||
.Where(j => j.GetProperty("type").GetInt32() == (int)ChannelType.GuildCategory)
|
.Where(j => j.GetProperty("type").GetInt32() == (int)ChannelType.GuildCategory)
|
||||||
@@ -135,7 +137,7 @@ namespace DiscordChatExporter.Domain.Discord
|
|||||||
var category = !string.IsNullOrWhiteSpace(parentId)
|
var category = !string.IsNullOrWhiteSpace(parentId)
|
||||||
? categories.GetValueOrDefault(parentId)
|
? categories.GetValueOrDefault(parentId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var channel = Channel.Parse(channelJson, category, position);
|
var channel = Channel.Parse(channelJson, category, position);
|
||||||
|
|
||||||
// Skip non-text channels
|
// Skip non-text channels
|
||||||
@@ -193,7 +195,7 @@ namespace DiscordChatExporter.Domain.Discord
|
|||||||
|
|
||||||
var parentId = response.GetPropertyOrNull("parent_id")?.GetString().Pipe(Snowflake.Parse);
|
var parentId = response.GetPropertyOrNull("parent_id")?.GetString().Pipe(Snowflake.Parse);
|
||||||
|
|
||||||
var category = parentId != null
|
var category = parentId is not null
|
||||||
? await GetChannelCategoryAsync(parentId.Value)
|
? await GetChannelCategoryAsync(parentId.Value)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -223,7 +225,7 @@ namespace DiscordChatExporter.Domain.Discord
|
|||||||
// will not appear in the output.
|
// will not appear in the output.
|
||||||
// Additionally, it provides the date of the last message, which is used to calculate progress.
|
// Additionally, it provides the date of the last message, which is used to calculate progress.
|
||||||
var lastMessage = await TryGetLastMessageAsync(channelId, before);
|
var lastMessage = await TryGetLastMessageAsync(channelId, before);
|
||||||
if (lastMessage == null || lastMessage.Timestamp < after?.ToDate())
|
if (lastMessage is null || lastMessage.Timestamp < after?.ToDate())
|
||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
// Keep track of first message in range in order to calculate progress
|
// Keep track of first message in range in order to calculate progress
|
||||||
@@ -258,11 +260,23 @@ namespace DiscordChatExporter.Domain.Discord
|
|||||||
if (message.Timestamp > lastMessage.Timestamp)
|
if (message.Timestamp > lastMessage.Timestamp)
|
||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
// Report progress based on the duration of parsed messages divided by total
|
// Report progress based on the duration of exported messages divided by total
|
||||||
progress?.Report(
|
if (progress is not null)
|
||||||
(message.Timestamp - firstMessage.Timestamp) /
|
{
|
||||||
(lastMessage.Timestamp - firstMessage.Timestamp)
|
var exportedDuration = (message.Timestamp - firstMessage.Timestamp).Duration();
|
||||||
);
|
var totalDuration = (lastMessage.Timestamp - firstMessage.Timestamp).Duration();
|
||||||
|
|
||||||
|
if (totalDuration > TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
progress.Report(exportedDuration / totalDuration);
|
||||||
|
}
|
||||||
|
// Avoid division by zero if all messages have the exact same timestamp
|
||||||
|
// (which can happen easily if there's only one message in the channel)
|
||||||
|
else
|
||||||
|
{
|
||||||
|
progress.Report(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
yield return message;
|
yield return message;
|
||||||
currentAfter = message.Id;
|
currentAfter = message.Id;
|
||||||
+3
-3
@@ -2,7 +2,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Discord
|
namespace DiscordChatExporter.Core.Discord
|
||||||
{
|
{
|
||||||
public readonly partial struct Snowflake
|
public readonly partial struct Snowflake
|
||||||
{
|
{
|
||||||
@@ -32,7 +32,7 @@ namespace DiscordChatExporter.Domain.Discord
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
// As number
|
// As number
|
||||||
if (Regex.IsMatch(str, @"^\d{15,}$") &&
|
if (Regex.IsMatch(str, @"^\d+$") &&
|
||||||
ulong.TryParse(str, NumberStyles.Number, formatProvider, out var value))
|
ulong.TryParse(str, NumberStyles.Number, formatProvider, out var value))
|
||||||
{
|
{
|
||||||
return new Snowflake(value);
|
return new Snowflake(value);
|
||||||
@@ -48,7 +48,7 @@ namespace DiscordChatExporter.Domain.Discord
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Snowflake Parse(string str, IFormatProvider? formatProvider) =>
|
public static Snowflake Parse(string str, IFormatProvider? formatProvider) =>
|
||||||
TryParse(str, formatProvider) ?? throw new FormatException($"Invalid snowflake: {str}.");
|
TryParse(str, formatProvider) ?? throw new FormatException($"Invalid snowflake '{str}'.");
|
||||||
|
|
||||||
public static Snowflake Parse(string str) => Parse(str, null);
|
public static Snowflake Parse(string str) => Parse(str, null);
|
||||||
}
|
}
|
||||||
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="JsonExtensions" Version="1.0.1" />
|
<PackageReference Include="JsonExtensions" Version="1.0.1" />
|
||||||
<PackageReference Include="MiniRazor" Version="2.0.3" />
|
<PackageReference Include="MiniRazor.CodeGen" Version="2.1.2" />
|
||||||
<PackageReference Include="Polly" Version="7.2.1" />
|
<PackageReference Include="Polly" Version="7.2.2" />
|
||||||
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
|
<PackageReference Include="Tyrrrz.Extensions" Version="1.6.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
+11
-21
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exceptions
|
namespace DiscordChatExporter.Core.Exceptions
|
||||||
{
|
{
|
||||||
public partial class DiscordChatExporterException : Exception
|
public partial class DiscordChatExporterException : Exception
|
||||||
{
|
{
|
||||||
@@ -21,35 +21,25 @@ namespace DiscordChatExporter.Domain.Exceptions
|
|||||||
var message = $@"
|
var message = $@"
|
||||||
Failed to perform an HTTP request.
|
Failed to perform an HTTP request.
|
||||||
|
|
||||||
|
[Request]
|
||||||
{response.RequestMessage}
|
{response.RequestMessage}
|
||||||
|
|
||||||
|
[Response]
|
||||||
{response}";
|
{response}";
|
||||||
|
|
||||||
return new DiscordChatExporterException(message.Trim(), true);
|
return new DiscordChatExporterException(message.Trim(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static DiscordChatExporterException Unauthorized()
|
internal static DiscordChatExporterException Unauthorized() =>
|
||||||
{
|
new("Authentication token is invalid.");
|
||||||
const string message = "Authentication token is invalid.";
|
|
||||||
return new DiscordChatExporterException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static DiscordChatExporterException Forbidden()
|
internal static DiscordChatExporterException Forbidden() =>
|
||||||
{
|
new("Access is forbidden.");
|
||||||
const string message = "Access is forbidden.";
|
|
||||||
return new DiscordChatExporterException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static DiscordChatExporterException NotFound()
|
internal static DiscordChatExporterException NotFound() =>
|
||||||
{
|
new("Requested resource does not exist.");
|
||||||
const string message = "Requested resource does not exist.";
|
|
||||||
return new DiscordChatExporterException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static DiscordChatExporterException ChannelIsEmpty()
|
internal static DiscordChatExporterException ChannelIsEmpty() =>
|
||||||
{
|
new("No messages found for the specified period.");
|
||||||
var message = $"No messages for the specified period.";
|
|
||||||
return new DiscordChatExporterException(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-6
@@ -2,13 +2,13 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Discord.Models.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Domain.Exceptions;
|
using DiscordChatExporter.Core.Exceptions;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting
|
namespace DiscordChatExporter.Core.Exporting
|
||||||
{
|
{
|
||||||
public class ChannelExporter
|
public class ChannelExporter
|
||||||
{
|
{
|
||||||
+5
-5
@@ -5,12 +5,12 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting
|
namespace DiscordChatExporter.Core.Exporting
|
||||||
{
|
{
|
||||||
internal class ExportContext
|
internal class ExportContext
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,7 @@ namespace DiscordChatExporter.Domain.Exporting
|
|||||||
var roles = member?.RoleIds.Join(Roles, i => i, r => r.Id, (_, role) => role);
|
var roles = member?.RoleIds.Join(Roles, i => i, r => r.Id, (_, role) => role);
|
||||||
|
|
||||||
return roles?
|
return roles?
|
||||||
.Where(r => r.Color != null)
|
.Where(r => r.Color is not null)
|
||||||
.OrderByDescending(r => r.Position)
|
.OrderByDescending(r => r.Position)
|
||||||
.Select(r => r.Color)
|
.Select(r => r.Color)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting
|
namespace DiscordChatExporter.Core.Exporting
|
||||||
{
|
{
|
||||||
public enum ExportFormat
|
public enum ExportFormat
|
||||||
{
|
{
|
||||||
+10
-9
@@ -2,11 +2,12 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DiscordChatExporter.Domain.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Internal;
|
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||||
|
using DiscordChatExporter.Core.Utils;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting
|
namespace DiscordChatExporter.Core.Exporting
|
||||||
{
|
{
|
||||||
public partial class ExportRequest
|
public partial class ExportRequest
|
||||||
{
|
{
|
||||||
@@ -28,7 +29,7 @@ namespace DiscordChatExporter.Domain.Exporting
|
|||||||
|
|
||||||
public Snowflake? Before { get; }
|
public Snowflake? Before { get; }
|
||||||
|
|
||||||
public int? PartitionLimit { get; }
|
public PartitionLimit PartitionLimit { get; }
|
||||||
|
|
||||||
public bool ShouldDownloadMedia { get; }
|
public bool ShouldDownloadMedia { get; }
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ namespace DiscordChatExporter.Domain.Exporting
|
|||||||
ExportFormat format,
|
ExportFormat format,
|
||||||
Snowflake? after,
|
Snowflake? after,
|
||||||
Snowflake? before,
|
Snowflake? before,
|
||||||
int? partitionLimit,
|
PartitionLimit partitionLimit,
|
||||||
bool shouldDownloadMedia,
|
bool shouldDownloadMedia,
|
||||||
bool shouldReuseMedia,
|
bool shouldReuseMedia,
|
||||||
string dateFormat)
|
string dateFormat)
|
||||||
@@ -127,17 +128,17 @@ namespace DiscordChatExporter.Domain.Exporting
|
|||||||
buffer.Append($"{guild.Name} - {channel.Category.Name} - {channel.Name} [{channel.Id}]");
|
buffer.Append($"{guild.Name} - {channel.Category.Name} - {channel.Name} [{channel.Id}]");
|
||||||
|
|
||||||
// Date range
|
// Date range
|
||||||
if (after != null || before != null)
|
if (after is not null || before is not null)
|
||||||
{
|
{
|
||||||
buffer.Append(" (");
|
buffer.Append(" (");
|
||||||
|
|
||||||
// Both 'after' and 'before' are set
|
// Both 'after' and 'before' are set
|
||||||
if (after != null && before != null)
|
if (after is not null && before is not null)
|
||||||
{
|
{
|
||||||
buffer.Append($"{after?.ToDate():yyyy-MM-dd} to {before?.ToDate():yyyy-MM-dd}");
|
buffer.Append($"{after?.ToDate():yyyy-MM-dd} to {before?.ToDate():yyyy-MM-dd}");
|
||||||
}
|
}
|
||||||
// Only 'after' is set
|
// Only 'after' is set
|
||||||
else if (after != null)
|
else if (after is not null)
|
||||||
{
|
{
|
||||||
buffer.Append($"after {after?.ToDate():yyyy-MM-dd}");
|
buffer.Append($"after {after?.ToDate():yyyy-MM-dd}");
|
||||||
}
|
}
|
||||||
+4
-5
@@ -7,11 +7,10 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Internal;
|
using DiscordChatExporter.Core.Utils;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting
|
namespace DiscordChatExporter.Core.Exporting
|
||||||
{
|
{
|
||||||
internal partial class MediaDownloader
|
internal partial class MediaDownloader
|
||||||
{
|
{
|
||||||
@@ -63,7 +62,7 @@ namespace DiscordChatExporter.Domain.Exporting
|
|||||||
: (DateTimeOffset?) null
|
: (DateTimeOffset?) null
|
||||||
);
|
);
|
||||||
|
|
||||||
if (lastModified != null)
|
if (lastModified is not null)
|
||||||
{
|
{
|
||||||
File.SetCreationTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
File.SetCreationTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
||||||
File.SetLastWriteTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
File.SetLastWriteTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
||||||
+8
-17
@@ -1,16 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Exporting.Writers;
|
using DiscordChatExporter.Core.Exporting.Writers;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting
|
namespace DiscordChatExporter.Core.Exporting
|
||||||
{
|
{
|
||||||
internal partial class MessageExporter : IAsyncDisposable
|
internal partial class MessageExporter : IAsyncDisposable
|
||||||
{
|
{
|
||||||
private readonly ExportContext _context;
|
private readonly ExportContext _context;
|
||||||
|
|
||||||
private long _messageCount;
|
|
||||||
private int _partitionIndex;
|
private int _partitionIndex;
|
||||||
private MessageWriter? _writer;
|
private MessageWriter? _writer;
|
||||||
|
|
||||||
@@ -19,15 +18,9 @@ namespace DiscordChatExporter.Domain.Exporting
|
|||||||
_context = context;
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsPartitionLimitReached() =>
|
|
||||||
_messageCount > 0 &&
|
|
||||||
_context.Request.PartitionLimit != null &&
|
|
||||||
_context.Request.PartitionLimit != 0 &&
|
|
||||||
_messageCount % _context.Request.PartitionLimit == 0;
|
|
||||||
|
|
||||||
private async ValueTask ResetWriterAsync()
|
private async ValueTask ResetWriterAsync()
|
||||||
{
|
{
|
||||||
if (_writer != null)
|
if (_writer is not null)
|
||||||
{
|
{
|
||||||
await _writer.WritePostambleAsync();
|
await _writer.WritePostambleAsync();
|
||||||
await _writer.DisposeAsync();
|
await _writer.DisposeAsync();
|
||||||
@@ -38,14 +31,15 @@ namespace DiscordChatExporter.Domain.Exporting
|
|||||||
private async ValueTask<MessageWriter> GetWriterAsync()
|
private async ValueTask<MessageWriter> GetWriterAsync()
|
||||||
{
|
{
|
||||||
// Ensure partition limit has not been exceeded
|
// Ensure partition limit has not been exceeded
|
||||||
if (IsPartitionLimitReached())
|
if (_writer is not null &&
|
||||||
|
_context.Request.PartitionLimit.IsReached(_writer.MessagesWritten, _writer.BytesWritten))
|
||||||
{
|
{
|
||||||
await ResetWriterAsync();
|
await ResetWriterAsync();
|
||||||
_partitionIndex++;
|
_partitionIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writer is still valid - return
|
// Writer is still valid - return
|
||||||
if (_writer != null)
|
if (_writer is not null)
|
||||||
return _writer;
|
return _writer;
|
||||||
|
|
||||||
var filePath = GetPartitionFilePath(_context.Request.OutputBaseFilePath, _partitionIndex);
|
var filePath = GetPartitionFilePath(_context.Request.OutputBaseFilePath, _partitionIndex);
|
||||||
@@ -64,7 +58,6 @@ namespace DiscordChatExporter.Domain.Exporting
|
|||||||
{
|
{
|
||||||
var writer = await GetWriterAsync();
|
var writer = await GetWriterAsync();
|
||||||
await writer.WriteMessageAsync(message);
|
await writer.WriteMessageAsync(message);
|
||||||
_messageCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync() => await ResetWriterAsync();
|
public async ValueTask DisposeAsync() => await ResetWriterAsync();
|
||||||
@@ -72,9 +65,7 @@ namespace DiscordChatExporter.Domain.Exporting
|
|||||||
|
|
||||||
internal partial class MessageExporter
|
internal partial class MessageExporter
|
||||||
{
|
{
|
||||||
private static string GetPartitionFilePath(
|
private static string GetPartitionFilePath(string baseFilePath, int partitionIndex)
|
||||||
string baseFilePath,
|
|
||||||
int partitionIndex)
|
|
||||||
{
|
{
|
||||||
// First partition - don't change file name
|
// First partition - don't change file name
|
||||||
if (partitionIndex <= 0)
|
if (partitionIndex <= 0)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Exporting.Partitioning
|
||||||
|
{
|
||||||
|
public class FileSizePartitionLimit : PartitionLimit
|
||||||
|
{
|
||||||
|
private readonly long _limit;
|
||||||
|
|
||||||
|
public FileSizePartitionLimit(long limit) => _limit = limit;
|
||||||
|
|
||||||
|
public override bool IsReached(long messagesWritten, long bytesWritten) =>
|
||||||
|
bytesWritten >= _limit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Exporting.Partitioning
|
||||||
|
{
|
||||||
|
public class MessageCountPartitionLimit : PartitionLimit
|
||||||
|
{
|
||||||
|
private readonly long _limit;
|
||||||
|
|
||||||
|
public MessageCountPartitionLimit(long limit) => _limit = limit;
|
||||||
|
|
||||||
|
public override bool IsReached(long messagesWritten, long bytesWritten) =>
|
||||||
|
messagesWritten >= _limit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Exporting.Partitioning
|
||||||
|
{
|
||||||
|
public class NullPartitionLimit : PartitionLimit
|
||||||
|
{
|
||||||
|
public static NullPartitionLimit Instance { get; } = new();
|
||||||
|
|
||||||
|
public override bool IsReached(long messagesWritten, long bytesWritten) => false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Exporting.Partitioning
|
||||||
|
{
|
||||||
|
public abstract partial class PartitionLimit
|
||||||
|
{
|
||||||
|
public abstract bool IsReached(long messagesWritten, long bytesWritten);
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class PartitionLimit
|
||||||
|
{
|
||||||
|
private static long? TryParseFileSizeBytes(string value, IFormatProvider? formatProvider = null)
|
||||||
|
{
|
||||||
|
var match = Regex.Match(value, @"^\s*(\d+[\.,]?\d*)\s*(\w)?b\s*$", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
// Number part
|
||||||
|
if (!double.TryParse(
|
||||||
|
match.Groups[1].Value,
|
||||||
|
NumberStyles.Float,
|
||||||
|
formatProvider,
|
||||||
|
out var number))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Magnitude part
|
||||||
|
var magnitude = match.Groups[2].Value.ToUpperInvariant() switch
|
||||||
|
{
|
||||||
|
"G" => 1_000_000_000,
|
||||||
|
"M" => 1_000_000,
|
||||||
|
"K" => 1_000,
|
||||||
|
"" => 1,
|
||||||
|
_ => -1
|
||||||
|
};
|
||||||
|
|
||||||
|
if (magnitude < 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (long) (number * magnitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PartitionLimit? TryParse(string value, IFormatProvider? formatProvider = null)
|
||||||
|
{
|
||||||
|
var fileSizeLimit = TryParseFileSizeBytes(value, formatProvider);
|
||||||
|
if (fileSizeLimit is not null)
|
||||||
|
return new FileSizePartitionLimit(fileSizeLimit.Value);
|
||||||
|
|
||||||
|
if (int.TryParse(value, NumberStyles.Integer, formatProvider, out var messageCountLimit))
|
||||||
|
return new MessageCountPartitionLimit(messageCountLimit);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PartitionLimit Parse(string value, IFormatProvider? formatProvider = null) =>
|
||||||
|
TryParse(value, formatProvider) ?? throw new FormatException($"Invalid partition limit '{value}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-4
@@ -2,11 +2,11 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors;
|
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting.Writers
|
namespace DiscordChatExporter.Core.Exporting.Writers
|
||||||
{
|
{
|
||||||
internal partial class CsvMessageWriter : MessageWriter
|
internal partial class CsvMessageWriter : MessageWriter
|
||||||
{
|
{
|
||||||
@@ -58,6 +58,8 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
|
|
||||||
public override async ValueTask WriteMessageAsync(Message message)
|
public override async ValueTask WriteMessageAsync(Message message)
|
||||||
{
|
{
|
||||||
|
await base.WriteMessageAsync(message);
|
||||||
|
|
||||||
// Author ID
|
// Author ID
|
||||||
await _writer.WriteAsync(CsvEncode(message.Author.Id.ToString()));
|
await _writer.WriteAsync(CsvEncode(message.Author.Id.ToString()));
|
||||||
await _writer.WriteAsync(',');
|
await _writer.WriteAsync(',');
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting.Writers.Html
|
namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||||
{
|
{
|
||||||
// Used for grouping contiguous messages in HTML export
|
// Used for grouping contiguous messages in HTML export
|
||||||
internal partial class MessageGroup
|
internal partial class MessageGroup
|
||||||
+19
-19
@@ -1,8 +1,8 @@
|
|||||||
@using System
|
@using System
|
||||||
@using System.Linq
|
@using System.Linq
|
||||||
@using System.Threading.Tasks
|
@using System.Threading.Tasks
|
||||||
@namespace DiscordChatExporter.Domain.Exporting.Writers.Html
|
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||||
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Domain.Exporting.Writers.Html.MessageGroupTemplateContext>
|
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Core.Exporting.Writers.Html.MessageGroupTemplateContext>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
|
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
var userColor = Model.ExportContext.TryGetUserColor(Model.MessageGroup.Author.Id);
|
var userColor = Model.ExportContext.TryGetUserColor(Model.MessageGroup.Author.Id);
|
||||||
|
|
||||||
var userColorStyle = userColor != null
|
var userColorStyle = userColor is not null
|
||||||
? $"color: rgb({userColor?.R},{userColor?.G},{userColor?.B})"
|
? $"color: rgb({userColor?.R},{userColor?.G},{userColor?.B})"
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -25,19 +25,19 @@
|
|||||||
? Model.MessageGroup.Author.Name
|
? Model.MessageGroup.Author.Name
|
||||||
: userMember?.Nick ?? Model.MessageGroup.Author.Name;
|
: userMember?.Nick ?? Model.MessageGroup.Author.Name;
|
||||||
|
|
||||||
var referencedUserMember = Model.MessageGroup.ReferencedMessage != null
|
var referencedUserMember = Model.MessageGroup.ReferencedMessage is not null
|
||||||
? Model.ExportContext.TryGetMember(Model.MessageGroup.ReferencedMessage.Author.Id)
|
? Model.ExportContext.TryGetMember(Model.MessageGroup.ReferencedMessage.Author.Id)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var referencedUserColor = Model.MessageGroup.ReferencedMessage != null
|
var referencedUserColor = Model.MessageGroup.ReferencedMessage is not null
|
||||||
? Model.ExportContext.TryGetUserColor(Model.MessageGroup.ReferencedMessage.Author.Id)
|
? Model.ExportContext.TryGetUserColor(Model.MessageGroup.ReferencedMessage.Author.Id)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var referencedUserColorStyle = referencedUserColor != null
|
var referencedUserColorStyle = referencedUserColor is not null
|
||||||
? $"color: rgb({referencedUserColor?.R},{referencedUserColor?.G},{referencedUserColor?.B})"
|
? $"color: rgb({referencedUserColor?.R},{referencedUserColor?.G},{referencedUserColor?.B})"
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var referencedUserNick = Model.MessageGroup.ReferencedMessage != null
|
var referencedUserNick = Model.MessageGroup.ReferencedMessage is not null
|
||||||
? Model.MessageGroup.ReferencedMessage.Author.IsBot
|
? Model.MessageGroup.ReferencedMessage.Author.IsBot
|
||||||
? Model.MessageGroup.ReferencedMessage.Author.Name
|
? Model.MessageGroup.ReferencedMessage.Author.Name
|
||||||
: referencedUserMember?.Nick ?? Model.MessageGroup.ReferencedMessage.Author.Name
|
: referencedUserMember?.Nick ?? Model.MessageGroup.ReferencedMessage.Author.Name
|
||||||
@@ -45,12 +45,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="chatlog__message-group">
|
<div class="chatlog__message-group">
|
||||||
@if (Model.MessageGroup.Reference != null)
|
@if (Model.MessageGroup.Reference is not null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__reference-symbol">
|
<div class="chatlog__reference-symbol">
|
||||||
</div>
|
</div>
|
||||||
<div class="chatlog__reference">
|
<div class="chatlog__reference">
|
||||||
@if (Model.MessageGroup.ReferencedMessage != null)
|
@if (Model.MessageGroup.ReferencedMessage is not null)
|
||||||
{
|
{
|
||||||
<img class="chatlog__reference-avatar" src="@await ResolveUrlAsync(Model.MessageGroup.ReferencedMessage.Author.AvatarUrl)" alt="Avatar">
|
<img class="chatlog__reference-avatar" src="@await ResolveUrlAsync(Model.MessageGroup.ReferencedMessage.Author.AvatarUrl)" alt="Avatar">
|
||||||
<span class="chatlog__reference-name" title="@Model.MessageGroup.ReferencedMessage.Author.FullName" style="@referencedUserColorStyle">@referencedUserNick</span>
|
<span class="chatlog__reference-name" title="@Model.MessageGroup.ReferencedMessage.Author.FullName" style="@referencedUserColorStyle">@referencedUserNick</span>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@if (Model.MessageGroup.ReferencedMessage.EditedTimestamp != null)
|
@if (Model.MessageGroup.ReferencedMessage.EditedTimestamp is not null)
|
||||||
{
|
{
|
||||||
<span class="chatlog__reference-edited-timestamp" title="@FormatDate(Model.MessageGroup.ReferencedMessage.EditedTimestamp.Value)">(edited)</span>
|
<span class="chatlog__reference-edited-timestamp" title="@FormatDate(Model.MessageGroup.ReferencedMessage.EditedTimestamp.Value)">(edited)</span>
|
||||||
}
|
}
|
||||||
@@ -98,13 +98,13 @@
|
|||||||
var isPinnedStyle = message.IsPinned ? "chatlog__message--pinned" : null;
|
var isPinnedStyle = message.IsPinned ? "chatlog__message--pinned" : null;
|
||||||
|
|
||||||
<div class="chatlog__message @isPinnedStyle" data-message-id="@message.Id" id="message-@message.Id">
|
<div class="chatlog__message @isPinnedStyle" data-message-id="@message.Id" id="message-@message.Id">
|
||||||
@if (!string.IsNullOrWhiteSpace(message.Content) || message.EditedTimestamp != null)
|
@if (!string.IsNullOrWhiteSpace(message.Content) || message.EditedTimestamp is not null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__content">
|
<div class="chatlog__content">
|
||||||
<div class="markdown">
|
<div class="markdown">
|
||||||
<span class="preserve-whitespace">@Raw(FormatMarkdown(message.Content))</span>
|
<span class="preserve-whitespace">@Raw(FormatMarkdown(message.Content))</span>
|
||||||
|
|
||||||
@if (message.EditedTimestamp != null)
|
@if (message.EditedTimestamp is not null)
|
||||||
{
|
{
|
||||||
<span class="chatlog__edited-timestamp" title="@FormatDate(message.EditedTimestamp.Value)">(edited)</span>
|
<span class="chatlog__edited-timestamp" title="@FormatDate(message.EditedTimestamp.Value)">(edited)</span>
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
@foreach (var embed in message.Embeds)
|
@foreach (var embed in message.Embeds)
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed">
|
<div class="chatlog__embed">
|
||||||
@if (embed.Color != null)
|
@if (embed.Color is not null)
|
||||||
{
|
{
|
||||||
var embedColorStyle = $"background-color: rgba({embed.Color?.R},{embed.Color?.G},{embed.Color?.B},{embed.Color?.A})";
|
var embedColorStyle = $"background-color: rgba({embed.Color?.R},{embed.Color?.G},{embed.Color?.B},{embed.Color?.A})";
|
||||||
<div class="chatlog__embed-color-pill" style="@embedColorStyle"></div>
|
<div class="chatlog__embed-color-pill" style="@embedColorStyle"></div>
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
<div class="chatlog__embed-content-container">
|
<div class="chatlog__embed-content-container">
|
||||||
<div class="chatlog__embed-content">
|
<div class="chatlog__embed-content">
|
||||||
<div class="chatlog__embed-text">
|
<div class="chatlog__embed-text">
|
||||||
@if (embed.Author != null)
|
@if (embed.Author is not null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-author">
|
<div class="chatlog__embed-author">
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))
|
@if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))
|
||||||
@@ -249,7 +249,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (embed.Thumbnail != null)
|
@if (embed.Thumbnail is not null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-thumbnail-container">
|
<div class="chatlog__embed-thumbnail-container">
|
||||||
<a class="chatlog__embed-thumbnail-link" href="@await ResolveUrlAsync(embed.Thumbnail.Url)">
|
<a class="chatlog__embed-thumbnail-link" href="@await ResolveUrlAsync(embed.Thumbnail.Url)">
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (embed.Image != null)
|
@if (embed.Image is not null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-image-container">
|
<div class="chatlog__embed-image-container">
|
||||||
<a class="chatlog__embed-image-link" href="@await ResolveUrlAsync(embed.Image.Url)">
|
<a class="chatlog__embed-image-link" href="@await ResolveUrlAsync(embed.Image.Url)">
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (embed.Footer != null || embed.Timestamp != null)
|
@if (embed.Footer is not null || embed.Timestamp is not null)
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-footer">
|
<div class="chatlog__embed-footer">
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Footer?.IconUrl))
|
@if (!string.IsNullOrWhiteSpace(embed.Footer?.IconUrl))
|
||||||
@@ -282,12 +282,12 @@
|
|||||||
@embed.Footer.Text
|
@embed.Footer.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Footer?.Text) && embed.Timestamp != null)
|
@if (!string.IsNullOrWhiteSpace(embed.Footer?.Text) && embed.Timestamp is not null)
|
||||||
{
|
{
|
||||||
@(" • ")
|
@(" • ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (embed.Timestamp != null)
|
@if (embed.Timestamp is not null)
|
||||||
{
|
{
|
||||||
@FormatDate(embed.Timestamp.Value)
|
@FormatDate(embed.Timestamp.Value)
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
using DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors;
|
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting.Writers.Html
|
namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||||
{
|
{
|
||||||
internal class MessageGroupTemplateContext
|
internal class MessageGroupTemplateContext
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||||
|
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Core.Exporting.Writers.Html.PostambleTemplateContext>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="postamble">
|
||||||
|
<div class="postamble__entry">Exported @Model.MessagesWritten.ToString("N0") message(s)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||||
|
{
|
||||||
|
internal class PostambleTemplateContext
|
||||||
|
{
|
||||||
|
public ExportContext ExportContext { get; }
|
||||||
|
|
||||||
|
public long MessagesWritten { get; }
|
||||||
|
|
||||||
|
public PostambleTemplateContext(ExportContext exportContext, long messagesWritten)
|
||||||
|
{
|
||||||
|
ExportContext = exportContext;
|
||||||
|
MessagesWritten = messagesWritten;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-7
@@ -1,15 +1,15 @@
|
|||||||
@using System
|
@using System
|
||||||
@using System.Threading.Tasks
|
@using System.Threading.Tasks
|
||||||
@using Tyrrrz.Extensions
|
@using Tyrrrz.Extensions
|
||||||
@namespace DiscordChatExporter.Domain.Exporting.Writers.Html
|
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||||
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Domain.Exporting.Writers.Html.LayoutTemplateContext>
|
@inherits MiniRazor.TemplateBase<DiscordChatExporter.Core.Exporting.Writers.Html.PreambleTemplateContext>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
|
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
|
||||||
|
|
||||||
ValueTask<string> ResolveUrlAsync(string url) => Model.ExportContext.ResolveMediaUrlAsync(url);
|
ValueTask<string> ResolveUrlAsync(string url) => Model.ExportContext.ResolveMediaUrlAsync(url);
|
||||||
|
|
||||||
string GetStyleSheet(string name) => Model.GetType().Assembly.GetManifestResourceString($"DiscordChatExporter.Domain.Exporting.Writers.Html.{name}.css");
|
string GetStyleSheet(string name) => Model.GetType().Assembly.GetManifestResourceString($"DiscordChatExporter.Core.Exporting.Writers.Html.{name}.css");
|
||||||
}
|
}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -78,18 +78,18 @@
|
|||||||
<div class="preamble__entry preamble__entry--small">@Model.ExportContext.Request.Channel.Topic</div>
|
<div class="preamble__entry preamble__entry--small">@Model.ExportContext.Request.Channel.Topic</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (Model.ExportContext.Request.After != null || Model.ExportContext.Request.Before != null)
|
@if (Model.ExportContext.Request.After is not null || Model.ExportContext.Request.Before is not null)
|
||||||
{
|
{
|
||||||
<div class="preamble__entry preamble__entry--small">
|
<div class="preamble__entry preamble__entry--small">
|
||||||
@if (Model.ExportContext.Request.After != null && Model.ExportContext.Request.Before != null)
|
@if (Model.ExportContext.Request.After is not null && Model.ExportContext.Request.Before is not null)
|
||||||
{
|
{
|
||||||
@($"Between {FormatDate(Model.ExportContext.Request.After.Value.ToDate())} and {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}")
|
@($"Between {FormatDate(Model.ExportContext.Request.After.Value.ToDate())} and {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}")
|
||||||
}
|
}
|
||||||
else if (Model.ExportContext.Request.After != null)
|
else if (Model.ExportContext.Request.After is not null)
|
||||||
{
|
{
|
||||||
@($"After {FormatDate(Model.ExportContext.Request.After.Value.ToDate())}")
|
@($"After {FormatDate(Model.ExportContext.Request.After.Value.ToDate())}")
|
||||||
}
|
}
|
||||||
else if (Model.ExportContext.Request.Before != null)
|
else if (Model.ExportContext.Request.Before is not null)
|
||||||
{
|
{
|
||||||
@($"Before {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}")
|
@($"Before {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}")
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||||
|
{
|
||||||
|
internal class PreambleTemplateContext
|
||||||
|
{
|
||||||
|
public ExportContext ExportContext { get; }
|
||||||
|
|
||||||
|
public string ThemeName { get; }
|
||||||
|
|
||||||
|
public PreambleTemplateContext(ExportContext exportContext, string themeName)
|
||||||
|
{
|
||||||
|
ExportContext = exportContext;
|
||||||
|
ThemeName = themeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-10
@@ -2,10 +2,10 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Exporting.Writers.Html;
|
using DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting.Writers
|
namespace DiscordChatExporter.Core.Exporting.Writers
|
||||||
{
|
{
|
||||||
internal class HtmlMessageWriter : MessageWriter
|
internal class HtmlMessageWriter : MessageWriter
|
||||||
{
|
{
|
||||||
@@ -14,8 +14,6 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
|
|
||||||
private readonly List<Message> _messageGroupBuffer = new();
|
private readonly List<Message> _messageGroupBuffer = new();
|
||||||
|
|
||||||
private long _messageCount;
|
|
||||||
|
|
||||||
public HtmlMessageWriter(Stream stream, ExportContext context, string themeName)
|
public HtmlMessageWriter(Stream stream, ExportContext context, string themeName)
|
||||||
: base(stream, context)
|
: base(stream, context)
|
||||||
{
|
{
|
||||||
@@ -25,7 +23,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
|
|
||||||
public override async ValueTask WritePreambleAsync()
|
public override async ValueTask WritePreambleAsync()
|
||||||
{
|
{
|
||||||
var templateContext = new LayoutTemplateContext(Context, _themeName, _messageCount);
|
var templateContext = new PreambleTemplateContext(Context, _themeName);
|
||||||
|
|
||||||
await _writer.WriteLineAsync(
|
await _writer.WriteLineAsync(
|
||||||
await PreambleTemplate.RenderAsync(templateContext)
|
await PreambleTemplate.RenderAsync(templateContext)
|
||||||
@@ -43,6 +41,8 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
|
|
||||||
public override async ValueTask WriteMessageAsync(Message message)
|
public override async ValueTask WriteMessageAsync(Message message)
|
||||||
{
|
{
|
||||||
|
await base.WriteMessageAsync(message);
|
||||||
|
|
||||||
// If message group is empty or the given message can be grouped, buffer the given message
|
// If message group is empty or the given message can be grouped, buffer the given message
|
||||||
if (!_messageGroupBuffer.Any() || MessageGroup.CanJoin(_messageGroupBuffer.Last(), message))
|
if (!_messageGroupBuffer.Any() || MessageGroup.CanJoin(_messageGroupBuffer.Last(), message))
|
||||||
{
|
{
|
||||||
@@ -56,9 +56,6 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
_messageGroupBuffer.Clear();
|
_messageGroupBuffer.Clear();
|
||||||
_messageGroupBuffer.Add(message);
|
_messageGroupBuffer.Add(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment message count
|
|
||||||
_messageCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async ValueTask WritePostambleAsync()
|
public override async ValueTask WritePostambleAsync()
|
||||||
@@ -67,7 +64,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
if (_messageGroupBuffer.Any())
|
if (_messageGroupBuffer.Any())
|
||||||
await WriteMessageGroupAsync(MessageGroup.Join(_messageGroupBuffer));
|
await WriteMessageGroupAsync(MessageGroup.Join(_messageGroupBuffer));
|
||||||
|
|
||||||
var templateContext = new LayoutTemplateContext(Context, _themeName, _messageCount);
|
var templateContext = new PostambleTemplateContext(Context, MessagesWritten);
|
||||||
|
|
||||||
await _writer.WriteLineAsync(
|
await _writer.WriteLineAsync(
|
||||||
await PostambleTemplate.RenderAsync(templateContext)
|
await PostambleTemplate.RenderAsync(templateContext)
|
||||||
+22
-20
@@ -1,27 +1,26 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Text.Encodings.Web;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
|
||||||
using DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using DiscordChatExporter.Domain.Internal.Extensions;
|
|
||||||
using JsonExtensions.Writing;
|
using JsonExtensions.Writing;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting.Writers
|
namespace DiscordChatExporter.Core.Exporting.Writers
|
||||||
{
|
{
|
||||||
internal class JsonMessageWriter : MessageWriter
|
internal class JsonMessageWriter : MessageWriter
|
||||||
{
|
{
|
||||||
private readonly Utf8JsonWriter _writer;
|
private readonly Utf8JsonWriter _writer;
|
||||||
|
|
||||||
private long _messageCount;
|
|
||||||
|
|
||||||
public JsonMessageWriter(Stream stream, ExportContext context)
|
public JsonMessageWriter(Stream stream, ExportContext context)
|
||||||
: base(stream, context)
|
: base(stream, context)
|
||||||
{
|
{
|
||||||
_writer = new Utf8JsonWriter(stream, new JsonWriterOptions
|
_writer = new Utf8JsonWriter(stream, new JsonWriterOptions
|
||||||
{
|
{
|
||||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
Indented = true
|
Indented = true,
|
||||||
|
SkipValidation = true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,19 +116,19 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
_writer.WriteString("timestamp", embed.Timestamp);
|
_writer.WriteString("timestamp", embed.Timestamp);
|
||||||
_writer.WriteString("description", FormatMarkdown(embed.Description));
|
_writer.WriteString("description", FormatMarkdown(embed.Description));
|
||||||
|
|
||||||
if (embed.Color != null)
|
if (embed.Color is not null)
|
||||||
_writer.WriteString("color", embed.Color.Value.ToHex());
|
_writer.WriteString("color", embed.Color.Value.ToHex());
|
||||||
|
|
||||||
if (embed.Author != null)
|
if (embed.Author is not null)
|
||||||
await WriteEmbedAuthorAsync(embed.Author);
|
await WriteEmbedAuthorAsync(embed.Author);
|
||||||
|
|
||||||
if (embed.Thumbnail != null)
|
if (embed.Thumbnail is not null)
|
||||||
await WriteEmbedThumbnailAsync(embed.Thumbnail);
|
await WriteEmbedThumbnailAsync(embed.Thumbnail);
|
||||||
|
|
||||||
if (embed.Image != null)
|
if (embed.Image is not null)
|
||||||
await WriteEmbedImageAsync(embed.Image);
|
await WriteEmbedImageAsync(embed.Image);
|
||||||
|
|
||||||
if (embed.Footer != null)
|
if (embed.Footer is not null)
|
||||||
await WriteEmbedFooterAsync(embed.Footer);
|
await WriteEmbedFooterAsync(embed.Footer);
|
||||||
|
|
||||||
// Fields
|
// Fields
|
||||||
@@ -168,7 +167,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
|
|
||||||
_writer.WriteString("id", mentionedUser.Id.ToString());
|
_writer.WriteString("id", mentionedUser.Id.ToString());
|
||||||
_writer.WriteString("name", mentionedUser.Name);
|
_writer.WriteString("name", mentionedUser.Name);
|
||||||
_writer.WriteNumber("discriminator", mentionedUser.Discriminator);
|
_writer.WriteString("discriminator", mentionedUser.DiscriminatorFormatted);
|
||||||
_writer.WriteString("nickname", Context.TryGetMember(mentionedUser.Id)?.Nick ?? mentionedUser.Name);
|
_writer.WriteString("nickname", Context.TryGetMember(mentionedUser.Id)?.Nick ?? mentionedUser.Name);
|
||||||
_writer.WriteBoolean("isBot", mentionedUser.IsBot);
|
_writer.WriteBoolean("isBot", mentionedUser.IsBot);
|
||||||
|
|
||||||
@@ -192,6 +191,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
_writer.WriteStartObject("channel");
|
_writer.WriteStartObject("channel");
|
||||||
_writer.WriteString("id", Context.Request.Channel.Id.ToString());
|
_writer.WriteString("id", Context.Request.Channel.Id.ToString());
|
||||||
_writer.WriteString("type", Context.Request.Channel.Type.ToString());
|
_writer.WriteString("type", Context.Request.Channel.Type.ToString());
|
||||||
|
_writer.WriteString("categoryId", Context.Request.Channel.Category.Id.ToString());
|
||||||
_writer.WriteString("category", Context.Request.Channel.Category.Name);
|
_writer.WriteString("category", Context.Request.Channel.Category.Name);
|
||||||
_writer.WriteString("name", Context.Request.Channel.Name);
|
_writer.WriteString("name", Context.Request.Channel.Name);
|
||||||
_writer.WriteString("topic", Context.Request.Channel.Topic);
|
_writer.WriteString("topic", Context.Request.Channel.Topic);
|
||||||
@@ -210,6 +210,8 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
|
|
||||||
public override async ValueTask WriteMessageAsync(Message message)
|
public override async ValueTask WriteMessageAsync(Message message)
|
||||||
{
|
{
|
||||||
|
await base.WriteMessageAsync(message);
|
||||||
|
|
||||||
_writer.WriteStartObject();
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
// Metadata
|
// Metadata
|
||||||
@@ -227,7 +229,9 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
_writer.WriteStartObject("author");
|
_writer.WriteStartObject("author");
|
||||||
_writer.WriteString("id", message.Author.Id.ToString());
|
_writer.WriteString("id", message.Author.Id.ToString());
|
||||||
_writer.WriteString("name", message.Author.Name);
|
_writer.WriteString("name", message.Author.Name);
|
||||||
_writer.WriteString("discriminator", $"{message.Author.Discriminator:0000}");
|
_writer.WriteString("discriminator", message.Author.DiscriminatorFormatted);
|
||||||
|
_writer.WriteString("nickname", Context.TryGetMember(message.Author.Id)?.Nick ?? message.Author.Name);
|
||||||
|
_writer.WriteString("color", Context.TryGetUserColor(message.Author.Id)?.ToHex());
|
||||||
_writer.WriteBoolean("isBot", message.Author.IsBot);
|
_writer.WriteBoolean("isBot", message.Author.IsBot);
|
||||||
_writer.WriteString("avatarUrl", await Context.ResolveMediaUrlAsync(message.Author.AvatarUrl));
|
_writer.WriteString("avatarUrl", await Context.ResolveMediaUrlAsync(message.Author.AvatarUrl));
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
@@ -265,7 +269,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
_writer.WriteEndArray();
|
_writer.WriteEndArray();
|
||||||
|
|
||||||
// Message reference
|
// Message reference
|
||||||
if (message.Reference != null)
|
if (message.Reference is not null)
|
||||||
{
|
{
|
||||||
_writer.WriteStartObject("reference");
|
_writer.WriteStartObject("reference");
|
||||||
_writer.WriteString("messageId", message.Reference.MessageId?.ToString());
|
_writer.WriteString("messageId", message.Reference.MessageId?.ToString());
|
||||||
@@ -276,8 +280,6 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
|
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
await _writer.FlushAsync();
|
await _writer.FlushAsync();
|
||||||
|
|
||||||
_messageCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async ValueTask WritePostambleAsync()
|
public override async ValueTask WritePostambleAsync()
|
||||||
@@ -285,7 +287,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
// Message array (end)
|
// Message array (end)
|
||||||
_writer.WriteEndArray();
|
_writer.WriteEndArray();
|
||||||
|
|
||||||
_writer.WriteNumber("messageCount", _messageCount);
|
_writer.WriteNumber("messageCount", MessagesWritten);
|
||||||
|
|
||||||
// Root object (end)
|
// Root object (end)
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
@@ -298,4 +300,4 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
await base.DisposeAsync();
|
await base.DisposeAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-7
@@ -3,13 +3,13 @@ using System.Linq;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DiscordChatExporter.Domain.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Markdown;
|
using DiscordChatExporter.Core.Markdown;
|
||||||
using DiscordChatExporter.Domain.Markdown.Ast;
|
using DiscordChatExporter.Core.Markdown.Ast;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors
|
namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
||||||
{
|
{
|
||||||
internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||||
{
|
{
|
||||||
@@ -112,7 +112,7 @@ namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors
|
|||||||
var name = role?.Name ?? "deleted-role";
|
var name = role?.Name ?? "deleted-role";
|
||||||
var color = role?.Color;
|
var color = role?.Color;
|
||||||
|
|
||||||
var style = color != null
|
var style = color is not null
|
||||||
? $"color: rgb({color?.R}, {color?.G}, {color?.B}); background-color: rgba({color?.R}, {color?.G}, {color?.B}, 0.1);"
|
? $"color: rgb({color?.R}, {color?.G}, {color?.B}); background-color: rgba({color?.R}, {color?.G}, {color?.B}, 0.1);"
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
+5
-5
@@ -1,10 +1,10 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using DiscordChatExporter.Domain.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Domain.Markdown;
|
using DiscordChatExporter.Core.Markdown;
|
||||||
using DiscordChatExporter.Domain.Markdown.Ast;
|
using DiscordChatExporter.Core.Markdown.Ast;
|
||||||
using DiscordChatExporter.Domain.Utilities;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors
|
namespace DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
||||||
{
|
{
|
||||||
internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
||||||
{
|
{
|
||||||
+11
-3
@@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting.Writers
|
namespace DiscordChatExporter.Core.Exporting.Writers
|
||||||
{
|
{
|
||||||
internal abstract class MessageWriter : IAsyncDisposable
|
internal abstract class MessageWriter : IAsyncDisposable
|
||||||
{
|
{
|
||||||
@@ -11,6 +11,10 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
|
|
||||||
protected ExportContext Context { get; }
|
protected ExportContext Context { get; }
|
||||||
|
|
||||||
|
public long MessagesWritten { get; private set; }
|
||||||
|
|
||||||
|
public long BytesWritten => Stream.Length;
|
||||||
|
|
||||||
protected MessageWriter(Stream stream, ExportContext context)
|
protected MessageWriter(Stream stream, ExportContext context)
|
||||||
{
|
{
|
||||||
Stream = stream;
|
Stream = stream;
|
||||||
@@ -19,7 +23,11 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
|
|
||||||
public virtual ValueTask WritePreambleAsync() => default;
|
public virtual ValueTask WritePreambleAsync() => default;
|
||||||
|
|
||||||
public abstract ValueTask WriteMessageAsync(Message message);
|
public virtual ValueTask WriteMessageAsync(Message message)
|
||||||
|
{
|
||||||
|
MessagesWritten++;
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
public virtual ValueTask WritePostambleAsync() => default;
|
public virtual ValueTask WritePostambleAsync() => default;
|
||||||
|
|
||||||
+11
-10
@@ -2,18 +2,16 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Domain.Discord.Models;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Domain.Exporting.Writers.MarkdownVisitors;
|
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
|
||||||
using Tyrrrz.Extensions;
|
using Tyrrrz.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Exporting.Writers
|
namespace DiscordChatExporter.Core.Exporting.Writers
|
||||||
{
|
{
|
||||||
internal class PlainTextMessageWriter : MessageWriter
|
internal class PlainTextMessageWriter : MessageWriter
|
||||||
{
|
{
|
||||||
private readonly TextWriter _writer;
|
private readonly TextWriter _writer;
|
||||||
|
|
||||||
private long _messageCount;
|
|
||||||
|
|
||||||
public PlainTextMessageWriter(Stream stream, ExportContext context)
|
public PlainTextMessageWriter(Stream stream, ExportContext context)
|
||||||
: base(stream, context)
|
: base(stream, context)
|
||||||
{
|
{
|
||||||
@@ -118,10 +116,10 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
|
if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
|
||||||
await _writer.WriteLineAsync($"Topic: {Context.Request.Channel.Topic}");
|
await _writer.WriteLineAsync($"Topic: {Context.Request.Channel.Topic}");
|
||||||
|
|
||||||
if (Context.Request.After != null)
|
if (Context.Request.After is not null)
|
||||||
await _writer.WriteLineAsync($"After: {Context.FormatDate(Context.Request.After.Value.ToDate())}");
|
await _writer.WriteLineAsync($"After: {Context.FormatDate(Context.Request.After.Value.ToDate())}");
|
||||||
|
|
||||||
if (Context.Request.Before != null)
|
if (Context.Request.Before is not null)
|
||||||
await _writer.WriteLineAsync($"Before: {Context.FormatDate(Context.Request.Before.Value.ToDate())}");
|
await _writer.WriteLineAsync($"Before: {Context.FormatDate(Context.Request.Before.Value.ToDate())}");
|
||||||
|
|
||||||
await _writer.WriteLineAsync('='.Repeat(62));
|
await _writer.WriteLineAsync('='.Repeat(62));
|
||||||
@@ -130,26 +128,29 @@ namespace DiscordChatExporter.Domain.Exporting.Writers
|
|||||||
|
|
||||||
public override async ValueTask WriteMessageAsync(Message message)
|
public override async ValueTask WriteMessageAsync(Message message)
|
||||||
{
|
{
|
||||||
|
await base.WriteMessageAsync(message);
|
||||||
|
|
||||||
|
// Header
|
||||||
await WriteMessageHeaderAsync(message);
|
await WriteMessageHeaderAsync(message);
|
||||||
|
|
||||||
|
// Content
|
||||||
if (!string.IsNullOrWhiteSpace(message.Content))
|
if (!string.IsNullOrWhiteSpace(message.Content))
|
||||||
await _writer.WriteLineAsync(FormatMarkdown(message.Content));
|
await _writer.WriteLineAsync(FormatMarkdown(message.Content));
|
||||||
|
|
||||||
await _writer.WriteLineAsync();
|
await _writer.WriteLineAsync();
|
||||||
|
|
||||||
|
// Attachments, embeds, reactions
|
||||||
await WriteAttachmentsAsync(message.Attachments);
|
await WriteAttachmentsAsync(message.Attachments);
|
||||||
await WriteEmbedsAsync(message.Embeds);
|
await WriteEmbedsAsync(message.Embeds);
|
||||||
await WriteReactionsAsync(message.Reactions);
|
await WriteReactionsAsync(message.Reactions);
|
||||||
|
|
||||||
await _writer.WriteLineAsync();
|
await _writer.WriteLineAsync();
|
||||||
|
|
||||||
_messageCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async ValueTask WritePostambleAsync()
|
public override async ValueTask WritePostambleAsync()
|
||||||
{
|
{
|
||||||
await _writer.WriteLineAsync('='.Repeat(62));
|
await _writer.WriteLineAsync('='.Repeat(62));
|
||||||
await _writer.WriteLineAsync($"Exported {_messageCount:N0} message(s)");
|
await _writer.WriteLineAsync($"Exported {MessagesWritten:N0} message(s)");
|
||||||
await _writer.WriteLineAsync('='.Repeat(62));
|
await _writer.WriteLineAsync('='.Repeat(62));
|
||||||
}
|
}
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Domain.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown.Ast
|
||||||
{
|
{
|
||||||
internal class EmojiNode : MarkdownNode
|
internal class EmojiNode : MarkdownNode
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown.Ast
|
||||||
{
|
{
|
||||||
internal enum TextFormatting
|
internal enum TextFormatting
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Domain.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown.Ast
|
||||||
{
|
{
|
||||||
internal class InlineCodeBlockNode : MarkdownNode
|
internal class InlineCodeBlockNode : MarkdownNode
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Domain.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown.Ast
|
||||||
{
|
{
|
||||||
internal class LinkNode : MarkdownNode
|
internal class LinkNode : MarkdownNode
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Domain.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown.Ast
|
||||||
{
|
{
|
||||||
internal abstract class MarkdownNode
|
internal abstract class MarkdownNode
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Domain.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown.Ast
|
||||||
{
|
{
|
||||||
internal enum MentionType
|
internal enum MentionType
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Domain.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown.Ast
|
||||||
{
|
{
|
||||||
internal class MultiLineCodeBlockNode : MarkdownNode
|
internal class MultiLineCodeBlockNode : MarkdownNode
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Domain.Markdown.Ast
|
namespace DiscordChatExporter.Core.Markdown.Ast
|
||||||
{
|
{
|
||||||
internal class TextNode : MarkdownNode
|
internal class TextNode : MarkdownNode
|
||||||
{
|
{
|
||||||
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DiscordChatExporter.Domain.Markdown.Ast;
|
using DiscordChatExporter.Core.Markdown.Ast;
|
||||||
using DiscordChatExporter.Domain.Markdown.Matching;
|
using DiscordChatExporter.Core.Markdown.Matching;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Markdown
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
{
|
{
|
||||||
// The following parsing logic is meant to replicate Discord's markdown grammar as close as possible
|
// The following parsing logic is meant to replicate Discord's markdown grammar as close as possible
|
||||||
internal static partial class MarkdownParser
|
internal static partial class MarkdownParser
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using DiscordChatExporter.Domain.Markdown.Ast;
|
using DiscordChatExporter.Core.Markdown.Ast;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Markdown
|
namespace DiscordChatExporter.Core.Markdown
|
||||||
{
|
{
|
||||||
internal abstract class MarkdownVisitor
|
internal abstract class MarkdownVisitor
|
||||||
{
|
{
|
||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Matching
|
||||||
{
|
{
|
||||||
internal class AggregateMatcher<T> : IMatcher<T>
|
internal class AggregateMatcher<T> : IMatcher<T>
|
||||||
{
|
{
|
||||||
@@ -27,11 +27,11 @@ namespace DiscordChatExporter.Domain.Markdown.Matching
|
|||||||
var match = matcher.TryMatch(stringPart);
|
var match = matcher.TryMatch(stringPart);
|
||||||
|
|
||||||
// If there's no match - continue
|
// If there's no match - continue
|
||||||
if (match == null)
|
if (match is null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// If this match is earlier than previous earliest - replace
|
// If this match is earlier than previous earliest - replace
|
||||||
if (earliestMatch == null || match.StringPart.StartIndex < earliestMatch.StringPart.StartIndex)
|
if (earliestMatch is null || match.StringPart.StartIndex < earliestMatch.StringPart.StartIndex)
|
||||||
earliestMatch = match;
|
earliestMatch = match;
|
||||||
|
|
||||||
// If the earliest match starts at the very beginning - break,
|
// If the earliest match starts at the very beginning - break,
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Matching
|
||||||
{
|
{
|
||||||
internal interface IMatcher<T>
|
internal interface IMatcher<T>
|
||||||
{
|
{
|
||||||
@@ -21,7 +21,7 @@ namespace DiscordChatExporter.Domain.Markdown.Matching
|
|||||||
var match = matcher.TryMatch(stringPart.Slice(currentIndex, stringPart.EndIndex - currentIndex));
|
var match = matcher.TryMatch(stringPart.Slice(currentIndex, stringPart.EndIndex - currentIndex));
|
||||||
|
|
||||||
// If there's no match - break
|
// If there's no match - break
|
||||||
if (match == null)
|
if (match is null)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// If this match doesn't start immediately at current index - transform and yield fallback first
|
// If this match doesn't start immediately at current index - transform and yield fallback first
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Matching
|
||||||
{
|
{
|
||||||
internal class ParsedMatch<T>
|
internal class ParsedMatch<T>
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Matching
|
||||||
{
|
{
|
||||||
internal class RegexMatcher<T> : IMatcher<T>
|
internal class RegexMatcher<T> : IMatcher<T>
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Matching
|
||||||
{
|
{
|
||||||
internal class StringMatcher<T> : IMatcher<T>
|
internal class StringMatcher<T> : IMatcher<T>
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Markdown.Matching
|
namespace DiscordChatExporter.Core.Markdown.Matching
|
||||||
{
|
{
|
||||||
internal readonly struct StringPart
|
internal readonly struct StringPart
|
||||||
{
|
{
|
||||||
+1
-1
@@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Utilities
|
namespace DiscordChatExporter.Core.Utils.Extensions
|
||||||
{
|
{
|
||||||
public static class AsyncExtensions
|
public static class AsyncExtensions
|
||||||
{
|
{
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Internal.Extensions
|
namespace DiscordChatExporter.Core.Utils.Extensions
|
||||||
{
|
{
|
||||||
internal static class BinaryExtensions
|
public static class BinaryExtensions
|
||||||
{
|
{
|
||||||
public static string ToHex(this byte[] data)
|
public static string ToHex(this byte[] data)
|
||||||
{
|
{
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Internal.Extensions
|
namespace DiscordChatExporter.Core.Utils.Extensions
|
||||||
{
|
{
|
||||||
internal static class ColorExtensions
|
public static class ColorExtensions
|
||||||
{
|
{
|
||||||
public static Color WithAlpha(this Color color, int alpha) => Color.FromArgb(alpha, color);
|
public static Color WithAlpha(this Color color, int alpha) => Color.FromArgb(alpha, color);
|
||||||
|
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Internal.Extensions
|
namespace DiscordChatExporter.Core.Utils.Extensions
|
||||||
{
|
{
|
||||||
internal static class DateExtensions
|
public static class DateExtensions
|
||||||
{
|
{
|
||||||
public static string ToLocalString(this DateTimeOffset dateTime, string format) =>
|
public static string ToLocalString(this DateTimeOffset dateTime, string format) =>
|
||||||
dateTime.ToLocalTime().ToString(format, CultureInfo.InvariantCulture);
|
dateTime.ToLocalTime().ToString(format, CultureInfo.InvariantCulture);
|
||||||
+4
-2
@@ -1,9 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Internal.Extensions
|
namespace DiscordChatExporter.Core.Utils.Extensions
|
||||||
{
|
{
|
||||||
internal static class GenericExtensions
|
public static class GenericExtensions
|
||||||
{
|
{
|
||||||
|
public static TOut Pipe<TIn, TOut>(this TIn input, Func<TIn, TOut> transform) => transform(input);
|
||||||
|
|
||||||
public static T? NullIf<T>(this T value, Func<T, bool> predicate) where T : struct =>
|
public static T? NullIf<T>(this T value, Func<T, bool> predicate) where T : struct =>
|
||||||
!predicate(value)
|
!predicate(value)
|
||||||
? value
|
? value
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Internal.Extensions
|
namespace DiscordChatExporter.Core.Utils.Extensions
|
||||||
{
|
{
|
||||||
internal static class HttpExtensions
|
public static class HttpExtensions
|
||||||
{
|
{
|
||||||
public static string? TryGetValue(this HttpContentHeaders headers, string name) =>
|
public static string? TryGetValue(this HttpContentHeaders headers, string name) =>
|
||||||
headers.TryGetValues(name, out var values)
|
headers.TryGetValues(name, out var values)
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Internal.Extensions
|
namespace DiscordChatExporter.Core.Utils.Extensions
|
||||||
{
|
{
|
||||||
internal static class StringExtensions
|
public static class StringExtensions
|
||||||
{
|
{
|
||||||
public static string Truncate(this string str, int charCount) =>
|
public static string Truncate(this string str, int charCount) =>
|
||||||
str.Length > charCount
|
str.Length > charCount
|
||||||
+3
-3
@@ -7,9 +7,9 @@ using System.Text.RegularExpressions;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Polly;
|
using Polly;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Internal
|
namespace DiscordChatExporter.Core.Utils
|
||||||
{
|
{
|
||||||
internal static class Http
|
public static class Http
|
||||||
{
|
{
|
||||||
public static HttpClient Client { get; } = new();
|
public static HttpClient Client { get; } = new();
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ namespace DiscordChatExporter.Domain.Internal
|
|||||||
if (i > 3)
|
if (i > 3)
|
||||||
{
|
{
|
||||||
var retryAfterDelay = result.Result.Headers.RetryAfter.Delta;
|
var retryAfterDelay = result.Result.Headers.RetryAfter.Delta;
|
||||||
if (retryAfterDelay != null)
|
if (retryAfterDelay is not null)
|
||||||
return retryAfterDelay.Value + TimeSpan.FromSeconds(1); // margin just in case
|
return retryAfterDelay.Value + TimeSpan.FromSeconds(1); // margin just in case
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Internal
|
namespace DiscordChatExporter.Core.Utils
|
||||||
{
|
{
|
||||||
internal static class PathEx
|
public static class PathEx
|
||||||
{
|
{
|
||||||
public static StringBuilder EscapePath(StringBuilder pathBuffer)
|
public static StringBuilder EscapePath(StringBuilder pathBuffer)
|
||||||
{
|
{
|
||||||
+2
-2
@@ -4,9 +4,9 @@ using System.Linq;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Domain.Internal
|
namespace DiscordChatExporter.Core.Utils
|
||||||
{
|
{
|
||||||
internal class UrlBuilder
|
public class UrlBuilder
|
||||||
{
|
{
|
||||||
private string _path = "";
|
private string _path = "";
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace DiscordChatExporter.Domain.Discord.Models.Common
|
|
||||||
{
|
|
||||||
public interface IHasIdAndPosition : IHasId
|
|
||||||
{
|
|
||||||
int? Position { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user