mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-07 14:44:39 +02:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6f9a364e1 | |||
| 0f6841593f | |||
| 2262d0abfb | |||
| ab535233dc | |||
| dcaceaddaf | |||
| 2a787d2749 | |||
| 481ddc4cd5 | |||
| c493518eaf | |||
| 450866d1f5 | |||
| 60c8703e78 | |||
| 1c343b75f8 | |||
| b2a22ab2a9 | |||
| b2ac5d8b45 | |||
| 008c2e64aa | |||
| 576ee73bfc | |||
| db1da5545a | |||
| 5397b0db7d | |||
| e89701e3f9 | |||
| a46bf9bf11 | |||
| 8dc024d244 | |||
| 9a125db0ea | |||
| d9c35f4405 | |||
| 5846129cc6 | |||
| 6f5f747632 | |||
| 6a71c59b6d | |||
| 73574ade48 | |||
| 743db67f91 | |||
| 5d7a74eece | |||
| 129df42de4 | |||
| 6508455f3a | |||
| 50b26c557f | |||
| b709fc0f15 | |||
| c118a0bb94 | |||
| 1418bec0c7 | |||
| 750fd69a50 | |||
| b1297bb7df | |||
| 3189111a0e | |||
| 6ec2c04aa3 | |||
| 7c014bf64d | |||
| c875f8f4f0 | |||
| 69e2c337d8 | |||
| 77c770acdd | |||
| 6f8749a105 | |||
| 4896d748aa | |||
| 4876ed192a | |||
| 4f6e6f4f0b | |||
| 8c75f04115 | |||
| df04b123cd | |||
| 578ed9be17 | |||
| ebceacc707 | |||
| eaab5786f2 | |||
| ebf7d65334 | |||
| 52a23828ce | |||
| 2edd781e32 | |||
| 19cdf47c6e | |||
| 65e0a63bda | |||
| 692438d10c | |||
| c607c6f307 | |||
| aae43821e7 | |||
| 7b9a4e1aff | |||
| d8e57f94a8 | |||
| 7b7df564ed | |||
| 80b59d1254 | |||
| 6579563816 | |||
| e2971ed3a6 | |||
| a0a263f074 | |||
| 1438fd9f77 | |||
| b298b45cf7 | |||
| 8d01c6be35 | |||
| ce4dbb2616 | |||
| 033d83bc4f | |||
| fb6cde38b6 | |||
| 46ede471a9 | |||
| 09f8937d99 | |||
| ad2dab2157 | |||
| c5e426289f | |||
| b8e5034aea | |||
| 5fc40fe7d5 | |||
| f7d94975b2 | |||
| 3eb0421ec6 | |||
| 57ed5578c9 | |||
| a8895663fe | |||
| 89f2084759 | |||
| 9180d51e5e | |||
| a58509fda8 | |||
| fbfff4e51f | |||
| c466c17793 | |||
| 1b2bcf771d | |||
| 99b49f0b8f | |||
| 850414b017 | |||
| 92bf6eab2d |
+13
-16
@@ -2,21 +2,22 @@
|
||||
|
||||
Docker distribution of DiscordChatExporter provides a way to run the app in a virtualized and isolated environment. Due to the nature of Docker, you also don't need to install any prerequisites otherwise required by DCE.
|
||||
|
||||
Note that only the CLI flavor of DiscordChatExporter is available for use with Docker.
|
||||
> **Note**:
|
||||
> Only the CLI flavor of DiscordChatExporter is available for use with Docker.
|
||||
|
||||
## Pulling
|
||||
|
||||
This will download the [Docker image from the registry](https://hub.docker.com/r/tyrrrz/discordchatexporter) to your computer. You can run this command again to update when a new version is released.
|
||||
|
||||
```
|
||||
```console
|
||||
docker pull tyrrrz/discordchatexporter:stable
|
||||
```
|
||||
|
||||
Note the `:stable` tag. DiscordChatExporter images are tagged according to the following patterns:
|
||||
|
||||
- `stable`: Latest stable version release and maps to the latest GitHub release. This tag is updated with release of each new version. Recommended for personal use.
|
||||
- `latest`: Built from the latest commit and maps to the latest CI build on GitHub. This tag is updated with each new commit to `master` branch. Not recommended for everyday use, but you can use it if you want to try out some features that haven't been officially released yet.
|
||||
- `x.y.z` (e.g. `2.30.1`): Fixed version release and maps to the corresponding tag on GitHub. This tag is never updated once that version has been released. Recommended for use in automation scenarios.
|
||||
- `stable` — latest stable version release. This tag is updated with each release of a new project version. Recommended for personal use.
|
||||
- `x.y.z` (e.g. `2.30.1`) — specific stable version release. This tag is pushed when the corresponding version is released and never updated thereafter. Recommended for use in automation scenarios.
|
||||
- `latest` — latest (potentially unstable) build. This tag is updated with each new commit to the `master` branch. Not recommended, unless you want to test a new feature that has not been released in a stable version yet.
|
||||
|
||||
You can see all available tags [here](https://hub.docker.com/r/tyrrrz/discordchatexporter/tags?ordering=name).
|
||||
|
||||
@@ -24,28 +25,28 @@ You can see all available tags [here](https://hub.docker.com/r/tyrrrz/discordcha
|
||||
|
||||
To run the CLI in Docker and render help text:
|
||||
|
||||
```
|
||||
```console
|
||||
docker run --rm tyrrrz/discordchatexporter:stable
|
||||
```
|
||||
|
||||
To export a channel:
|
||||
|
||||
```
|
||||
```console
|
||||
docker run --rm -v /path/on/machine:/out tyrrrz/discordchatexporter:stable export -t TOKEN -c CHANNELID
|
||||
```
|
||||
|
||||
If you want colored output and real-time progress reporting, pass the `-it` (interactive) option:
|
||||
If you want colored output and real-time progress reporting, pass the `-it` (interactive + pseudo-terminal) option:
|
||||
|
||||
```
|
||||
```console
|
||||
docker run --rm -it -v /path/on/machine:/out tyrrrz/discordchatexporter:stable export -t TOKEN -c CHANNELID
|
||||
```
|
||||
|
||||
Note the `-v /path/on/machine:/out` option, which instructs Docker to bind the `/out` directory inside the container to a path on your host machine. Replace `/path/on/machine` with the directory you want the files to be saved at.
|
||||
The `-v /path/on/machine:/out` option instructs Docker to bind the `/out` directory inside the container to a path on your host machine. Replace `/path/on/machine` with the directory you want the files to be saved at.
|
||||
|
||||
> **Note**:
|
||||
> If you are running SELinux, you will need to add the `:z` option after `/out`, e.g.:
|
||||
>
|
||||
> ```
|
||||
> ```console
|
||||
> docker run --rm -v /path/on/machine:/out:z tyrrrz/discordchatexporter:stable export -t TOKEN -c CHANNELID
|
||||
> ```
|
||||
>
|
||||
@@ -58,14 +59,10 @@ You can also use the current working directory as the output directory by specif
|
||||
|
||||
For more information, please refer to the [Dockerfile](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/DiscordChatExporter.Cli.dockerfile) and [Docker documentation](https://docs.docker.com/engine/reference/run).
|
||||
|
||||
To get your Token and Channel IDs, please refer to [this page](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md).
|
||||
To get your Token and Channel IDs, please refer to [this page](Token-and-IDs.md).
|
||||
|
||||
## Environment variables
|
||||
|
||||
DiscordChatExpoter CLI accepts the `DISCORD_TOKEN` environment variable as a fallback for the `--token` option. You can set this variable either with the `--env` Docker option or with a combination of the `--env-file` Docker option and a `.env` file.
|
||||
|
||||
Please refer to the [Docker documentation](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file) for more information.
|
||||
|
||||
## Permission issues with Linux hosts
|
||||
|
||||
When bounding volumes between the container and the host, files mirrored from the Docker container will be owned by the Docker user (in most setups this is `root`). Please refer to this [issue comment](https://github.com/Tyrrrz/DiscordChatExporter/issues/800#issuecomment-1030471970) for instructions on how to override this behavior.
|
||||
|
||||
+27
-42
@@ -1,69 +1,54 @@
|
||||
# Install .NET runtime
|
||||
|
||||
**.NET v7.0 Runtime** is required by **DiscordChatExporter v2.37 and newer**.
|
||||
**.NET 8.0 Runtime** is required by **DiscordChatExporter**.
|
||||
|
||||
If you are using GUI flavor of DiscordChatExporter, the runtime will be installed automatically (since v2.27.1).
|
||||
If you are using the CLI from Docker, installing the runtime on the host machine is not needed.
|
||||
> **Note**:
|
||||
> Installing the **.NET Runtime** is not required if you running **DiscordChatExporter** using [Docker](Docker.md).
|
||||
|
||||
- Install
|
||||
- [Windows and macOS](#windows-and-macos)
|
||||
- [Linux](#linux)
|
||||
- [Docker](#docker)
|
||||
- [Verify installation](#Verify-installation-optional)
|
||||
## Windows
|
||||
|
||||
## Windows and macOS
|
||||
Both the GUI and the CLI flavors of **DiscordChatExporter** for Windows come with a bootstrapper that should automatically install all the required prerequisites. Simply run the application and follow the presented instructions.
|
||||
|
||||
**Download from the direct links:**
|
||||
If, for some reason, you need to install the runtime manually, use one of the download links below:
|
||||
|
||||
- [.NET v7.0 Runtime for **Windows x64**](https://dotnet.microsoft.com/download/dotnet/thank-you/runtime-desktop-7.0.1-windows-x64-installer)
|
||||
- [.NET v7.0 Runtime for **Windows x86**](https://dotnet.microsoft.com/download/dotnet/thank-you/runtime-desktop-7.0.1-windows-x86-installer)
|
||||
- [.NET v7.0 Runtime for **Macs with Apple silicon**](https://dotnet.microsoft.com/download/dotnet/thank-you/runtime-7.0.1-macos-arm64-installer)
|
||||
- [.NET v7.0 Runtime for **Macs with an Intel processor**](https://dotnet.microsoft.com/download/dotnet/thank-you/runtime-7.0.1-macos-x64-installer)
|
||||
- [.NET 8.0 Runtime for **Windows x64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-windows-x86-installer)
|
||||
- [.NET 8.0 Runtime for **Windows x86**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-windows-x64-installer)
|
||||
- [.NET 8.0 Runtime for **Windows arm64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-windows-arm64-installer)
|
||||
|
||||
> **Note**:
|
||||
> [How can I tell if my computer is running a 32-bit (x86) or a 64-bit (x64) version of Windows?](https://support.microsoft.com/help/15056/windows-32-64-bit-faq)
|
||||
|
||||
## macOS
|
||||
|
||||
Use one of the download links below:
|
||||
|
||||
- [.NET 8.0 Runtime for **macOS x64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-macos-x64-installer)
|
||||
- [.NET 8.0 Runtime for **macOS arm64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-macos-arm64-installer)
|
||||
|
||||
> **Note**:
|
||||
> [Is my Mac using an Intel processor or Apple silicon?](https://support.apple.com/HT211814)
|
||||
|
||||
**Otherwise:**
|
||||
[Check out the latest version in this page](https://dotnet.microsoft.com/download/dotnet/7.0).
|
||||
Look for the download link in the '**.NET Runtime**' section, under **Installers**:
|
||||
|
||||
[](https://dotnet.microsoft.com/download/dotnet/7.0)
|
||||
> [Is my Mac using an Intel processor (x64) or Apple Silicon (arm64)?](https://support.apple.com/HT211814)
|
||||
|
||||
## Linux
|
||||
|
||||
**You only need to install .NET Runtime v7.0**
|
||||
Installing the .NET Runtime works differently depending on which Linux distribution you use. Check out the instructions for your distro:
|
||||
|
||||
Check out the instructions for your distro:
|
||||
|
||||
- [Ubuntu](https://docs.microsoft.com/dotnet/core/install/linux-ubuntu#supported-distributions)
|
||||
- [Debian](https://docs.microsoft.com/dotnet/core/install/linux-debian#supported-distributions)
|
||||
- [Fedora](https://docs.microsoft.com/dotnet/core/install/linux-fedora#supported-distributions)
|
||||
- [Ubuntu](https://docs.microsoft.com/dotnet/core/install/linux-ubuntu)
|
||||
- [Debian](https://docs.microsoft.com/dotnet/core/install/linux-debian)
|
||||
- [Fedora](https://docs.microsoft.com/dotnet/core/install/linux-fedora)
|
||||
|
||||
For other distros, please check the _'Install on Linux'_ menu on the left of [this page](https://docs.microsoft.com/dotnet/core/install/linux).
|
||||
|
||||
## Docker
|
||||
|
||||
Installing .NET Runtime is not needed. Please refer to the [Docker usage instructions](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Docker.md) page for more information.
|
||||
|
||||
## Verify installation (optional)
|
||||
|
||||
You can check which version of **.NET Runtime** is installed by following these instructions:
|
||||
|
||||
### Step 1
|
||||
|
||||
**Windows:** Open Command Prompt (aka `cmd`)
|
||||
**macOS and Linux:** Open `Terminal`
|
||||
|
||||
### Step 2
|
||||
|
||||
Run: `dotnet --info`
|
||||
If **.NET Runtime** is correctly installed, the command will output the following:
|
||||
You can check which version of **.NET Runtime** is installed by running the following command in a terminal:
|
||||
|
||||
```console
|
||||
dotnet --info
|
||||
```
|
||||
|
||||
If the **.NET Runtime** is correctly installed, the command will output something similar to the following:
|
||||
|
||||
```console
|
||||
.NET runtimes installed:
|
||||
Microsoft.NETCore.App 7.0.x [C:\path\to\dotnet\shared\Microsoft.NETCore.App]
|
||||
Microsoft.WindowsDesktop.App 7.0.x [C:\path\to\dotnet\shared\Microsoft.WindowsDesktop.App]
|
||||
|
||||
+8
-451
@@ -2,15 +2,16 @@
|
||||
|
||||
Welcome to the getting started page!
|
||||
Here you'll learn how to use every **DiscordChatExporter** (DCE for short) feature.
|
||||
For other things you can do with DCE, check out the [Guides](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Readme.md#Guides) section.
|
||||
For other things you can do with DCE, check out the [Guides](Readme.md#guides) section.
|
||||
|
||||
If you still have unanswered questions after reading this page or if you have encountered a problem, please visit our [FAQ & Troubleshooting](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Troubleshooting.md) section.
|
||||
If you still have unanswered questions after reading this page or if you have encountered a problem, please visit our [FAQ & Troubleshooting](Troubleshooting.md) section.
|
||||
|
||||
The information presented on this page is valid for **all** platforms.
|
||||
|
||||
## GUI or CLI?
|
||||
|
||||

|
||||

|
||||
|
||||
**DCE** has two different versions:
|
||||
|
||||
- **Graphical User Interface** (**GUI**) - it's the preferred version for newcomers as it is easy to use.
|
||||
@@ -19,449 +20,12 @@ The information presented on this page is valid for **all** platforms.
|
||||
You can get it by [downloading](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest) the `DiscordChatExporter.CLI.zip` file.
|
||||
|
||||
If you're not comfortable with **Windows'** Command-line (cmd), please choose the GUI.
|
||||
[**macOS**](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/MacOS.md), [**Linux**](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Linux.md) and [**Docker**](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Docker.md) users can only use the CLI version.
|
||||
[**macOS**](MacOS.md), [**Linux**](Linux.md) and [**Docker**](Docker.md) users can only use the CLI version.
|
||||
|
||||
**(Skip to [Using the CLI](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#using-the-cli) or [File Formats](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#file-formats))**
|
||||
There are dedicated guides for each version:
|
||||
|
||||
## Using the GUI
|
||||
|
||||
### Video tutorial
|
||||
|
||||
[](https://youtube.com/watch?v=jjtu0VQXV7I)
|
||||
|
||||
> Video by [NoIntro Tutorials](https://youtube.com/channel/UCFezKSxdNKJe77-hYiuXu3Q).
|
||||
|
||||
### Guide
|
||||
|
||||
1. After extracting the `.zip`, open `DiscordChatExporter.exe`
|
||||
2. Please refer to the on-screen instructions to get your token, then paste your token in the upper text box and hit ENTER or click the arrow (→).
|
||||
|
||||
> **Warning**:
|
||||
> **Never share your token!**
|
||||
> A token gives full access to an account, treat it like a password.
|
||||
|
||||
<img src="https://i.imgur.com/SuLQ5tZ.png" height="400"/>
|
||||
|
||||
3. DCE will display your Direct Messages and a sidebar with your server list. Select the channel you would like to export, then click the  button to continue.
|
||||
|
||||
> **Note**:
|
||||
> You can export multiple channels at once by holding `CTRL` or `SHIFT` while selecting.
|
||||
> You can also double-click a channel to export it without clicking the  button.
|
||||
|
||||
<img src="https://i.imgur.com/JHMFRh2.png" height="400"/>
|
||||
|
||||
4. In this screen you can customize the following:
|
||||
|
||||
- **Export format** - HTML (Dark), HTML (Light), TXT, CSV and JSON
|
||||
|
||||
Click the menu button (☰) to see more options:
|
||||
|
||||
- **Date range (after/before)** (Optional) - If set, only messages sent in the provided date range will be exported. Only one value (either after or before) is required if you want to use this option.
|
||||
> **Note**:
|
||||
> Please note that the time defaults to **12:00 AM** (midnight/00:00). This means that if you choose to export between Sep 17th and Sep 18th, messages from Sep 18th won't be exported.
|
||||
- **Filter** (Optional) - Special notation for filtering the messages that get included in the export. See [Message filters](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Message-filters.md) for more info.
|
||||
- **Messages per partition** (Optional) - Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb). For example, a channel with 36 messages set to be partitioned every 10 messages will output 4 files.
|
||||
- **Download assets** (Optional) - If this option is set, the export will include additional files such as user avatars, attached files, images, etc.
|
||||
Only files that are referenced by the export are downloaded, which means that, for example, user avatars will not be downloaded when using the plain text (TXT) export format.
|
||||
A folder containing the assets will be created along with the exported chat. They must be kept together.
|
||||
|
||||
5. Click `EXPORT` to export.
|
||||
|
||||
<img src="https://i.imgur.com/KPgMH2D.png" height="400"/><img src="https://i.imgur.com/mtUFojS.png" height="400"/>
|
||||
|
||||
### Settings
|
||||
|
||||
**Auto-updates**
|
||||
Perform automatic updates on every launch.
|
||||
Default: Enabled
|
||||
|
||||
> **Note**:
|
||||
> Keep this option enabled to receive the latest features and bug fixes!
|
||||
|
||||
**Dark mode**
|
||||
Use darker colors in the UI (User Interface).
|
||||
Default: Disabled
|
||||
|
||||
**Save token**
|
||||
Persist last used token between sessions.
|
||||
Default: Enabled
|
||||
|
||||
**Reuse downloaded assets**
|
||||
Reuse already downloaded assets to skip redundant requests.
|
||||
Default: Disabled
|
||||
|
||||
**Date format**
|
||||
You can customize how dates are formatted in the exported files in the settings menu ().
|
||||
|
||||
- The default one is `17-Sep-19 11:34 PM` - `dd-MMM-yy hh:mm tt`
|
||||
- To change it to `09-17-19 11:34 PM` use `MM-dd-yy hh:mm tt`
|
||||
- To change it to `17/09/2019 23:34` use `dd/MM/yyyy HH:mm`
|
||||
- To change it to `2019-09-17 23:34:05.6534` use `yyyy-MM-dd HH:mm:ss.ffff`
|
||||
- To change it to UTC `2019-09-18 04:34:05Z` use `u`
|
||||
- To display the time zone, add `K`. E.g. to change it to `17-Sep-19 11:34 PM (UTC-05:00)` use `dd-MMM-yy hh:mm tt (UTCK)`
|
||||
- To change it to Unix time `1568694845` use `unix`
|
||||
- To change it to Unix time in milliseconds `1568694845653` use `unixms`
|
||||
|
||||
More info about .NET date formats [here](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings).
|
||||
|
||||
**Parallel limit**
|
||||
The number of channels can be exported at the same time.
|
||||
Default: 1
|
||||
|
||||
> **Note**:
|
||||
> Try to keep this number low so that your account doesn't get flagged.
|
||||
|
||||
**(Skip to [File Formats](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#file-formats))**
|
||||
|
||||
## Using the CLI
|
||||
|
||||
> **Note**:
|
||||
> Make sure you have [.NET Core installed](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Dotnet.md) before attempting to run the commands below.
|
||||
> **Docker** users, please refer to the [Docker usage instructions](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Docker.md).
|
||||
|
||||
1. After extracting the `.zip`, open Command Prompt, aka `cmd` (`Terminal` on **macOS** and **Linux**).
|
||||
|
||||
2. Change the current directory to DCE's folder with `cd C:\path\to\directory`, then press ENTER to run the command.
|
||||
|
||||
**Windows** users can quickly get the directory's path by clicking the address bar while inside the folder.
|
||||

|
||||
**macOS** users can select the `.exe`, hit Command+I (⌘I), and copy what's after `Where:` to get the directory
|
||||

|
||||
You can also drag and drop the folder on **every platform**.
|
||||

|
||||
|
||||
3. Now we're ready to run the commands. The examples on this page follow the Windows file path format, change the file paths according to your system.
|
||||
|
||||
Let's do the following to list DCE's options:
|
||||
|
||||
Type the following in Command Prompt (Terminal), then press ENTER to run it:
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll
|
||||
```
|
||||
|
||||
> **Docker** users, please refer to the [Docker usage instructions](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Docker.md).
|
||||
|
||||
### DCE.CLI commands
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
|
||||
| [export](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#export) | Exports a channel |
|
||||
| [exportdm](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#exportdm) | Exports all direct message channels |
|
||||
| [exportguild](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#exportguild) | Exports all channels within the specified server |
|
||||
| [exportall](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#exportall) | Exports all accessible channels |
|
||||
| [channels](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#channels) | Outputs the list of channels in the given server |
|
||||
| [dm](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#dm) | Outputs the list of direct message channels |
|
||||
| [guilds](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#guilds) | Outputs the list of accessible servers |
|
||||
| [guide](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#guide) | Explains how to obtain token, guild, and channel ID |
|
||||
|
||||
To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md), or run `dotnet DiscordChatExporter.Cli.dll guide`.
|
||||
To get help with a specific command, please run:
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll command -h
|
||||
```
|
||||
|
||||
#### `export`
|
||||
|
||||
| | Option | Description |
|
||||
| ------ | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **\*** | [-c](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#basic-usage) | [Channel ID(s)](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#how-to-get-guild-id-or-guild-channel-id) |
|
||||
| **\*** | [-t](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#basic-usage) | [Authorization token](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#how-to-get-user-token). Environment variable: `DISCORD_TOKEN` |
|
||||
| | [-o](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#changing-the-output-filename) | Output file or directory path |
|
||||
| | [-f](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#changing-the-format) | [Output file format](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#file-formats). Default: HtmlDark |
|
||||
| | [--after](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-ranges) | Only include messages sent after this date |
|
||||
| | [--before](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-ranges) | Only include messages sent before this date |
|
||||
| | [-p](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#partitioning) | Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb) |
|
||||
| | [--filter](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Message-filters.md) | Special notation for filtering the messages that get included in the export |
|
||||
| | [--media](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#Downloading-assets) | Download assets referenced by the export (user avatars, attached files, embedded images, etc.). Default: false |
|
||||
| | [--reuse-media](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#Reuse-assets) | Reuse previously downloaded assets to avoid redundant requests. Default: false. |
|
||||
| | [--dateformat](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-formats) | Format used when writing dates. |
|
||||
| | -h | Shows help text |
|
||||
|
||||
> **Note**:
|
||||
> Options with an asterisk (**\***) are required. The order of the options doesn't matter.
|
||||
|
||||
With this command you can export Server Channels and Direct Messages.
|
||||
|
||||
##### Basic usage
|
||||
|
||||
You can quickly export with DCE's default settings by using just `-t token` and `-c channelid`.
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555
|
||||
```
|
||||
|
||||
##### Changing the format
|
||||
|
||||
You can change the export format to `HtmlDark`, `HtmlLight`, `PlainText` `Json` or `Csv` with `-f format`. The default format is `HtmlDark`.
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -f Json
|
||||
```
|
||||
|
||||
##### Changing the output filename
|
||||
|
||||
You can change the filename by using `-o name.ext`. e.g. for the `HTML` format:
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -o myserver.html
|
||||
```
|
||||
|
||||
##### Changing the output directory
|
||||
|
||||
You can change the export directory by using `-o` and providing a path that ends with a slash or does not have a file extension.
|
||||
If any of the folders in the path have a space in its name, escape them with quotes (").
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -o "C:\Discord Exports"
|
||||
```
|
||||
|
||||
##### Changing the filename and output directory
|
||||
|
||||
You can change both the filename and export directory by using `-o directory\name.ext`.
|
||||
Note that the filename must have an extension, otherwise it will be considered a directory name.
|
||||
If any of the folders in the path have a space in its name, escape them with quotes (").
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -o "C:\Discord Exports\myserver.html"
|
||||
```
|
||||
|
||||
##### Generating the filename and output directory dynamically
|
||||
|
||||
You can use template tokens to generate the output file path based on the guild and channel metadata.
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -o "C:\Discord Exports\%G\%T\%C.html"
|
||||
```
|
||||
|
||||
Assuming you are exporting a channel named `"my-channel"` in the `"Text channels"` category from a server called `"My server"`, you will get the following output file path: `C:\Discord Exports\My server\Text channels\my-channel.html`
|
||||
|
||||
Here is the full list of supported template tokens:
|
||||
|
||||
- `%g` - guild ID
|
||||
- `%G` - guild name
|
||||
- `%t` - category ID
|
||||
- `%T` - category name
|
||||
- `%c` - channel ID
|
||||
- `%C` - channel name
|
||||
- `%p` - channel position
|
||||
- `%P` - category position
|
||||
- `%a` - the "after" date
|
||||
- `%b` - the "before" date
|
||||
- `%%` - escapes `%`
|
||||
|
||||
##### Partitioning
|
||||
|
||||
You can use partitioning to split files after a given number of messages or file size.
|
||||
For example, a channel with 36 messages set to be partitioned every 10 messages will output 4 files.
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -p 10
|
||||
```
|
||||
|
||||
A 45mb channel set to be partitioned every 20mb will output 3 files.
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -p 20mb
|
||||
```
|
||||
|
||||
##### Downloading assets
|
||||
|
||||
If this option is set, the export will include additional files such as user avatars, attached files, images, etc.
|
||||
Only files that are referenced by the export are downloaded, which means that, for example, user avatars will not be downloaded when using the plain text (TXT) export format.
|
||||
A folder containing the assets will be created along with the exported chat. They must be kept together.
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --media
|
||||
```
|
||||
|
||||
##### Reuse assets
|
||||
|
||||
Previously downloaded assets can be reused to skip redundant downloads as long as the chat is always exported to the same folder. Using this option can speed up future exports. This option requires the `--media` option.
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --media --reuse-media
|
||||
```
|
||||
|
||||
##### Date formats
|
||||
|
||||
This setting changes how dates are formatted in the exported files.
|
||||
|
||||
- The default one is `17-Sep-19 11:34 PM` - `dd-MMM-yy hh:mm tt`
|
||||
- To change it to `09-17-19 11:34 PM` use `MM-dd-yy hh:mm tt`
|
||||
- To change it to `17/09/2019 23:34` use `dd/MM/yyyy HH:mm`
|
||||
- To change it to `2019-09-17 23:34:05.6534` use `yyyy-MM-dd HH:mm:ss.ffff`
|
||||
- To change it to UTC `2019-09-18 04:34:05Z` use `u`
|
||||
- To display the time zone, add `K`. E.g. to change it to `17-Sep-19 11:34 PM (UTC-05:00)` use `dd-MMM-yy hh:mm tt (UTCK)`
|
||||
- To change it to Unix time `1568694845` use `unix`
|
||||
- To change it to Unix time in milliseconds `1568694845653` use `unixms`
|
||||
|
||||
More info about .NET date formats [here](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings).
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --dateformat "yyyy-MM-dd HH:mm"
|
||||
```
|
||||
|
||||
> Special thanks to [@andrewkolos](https://github.com/andrewkolos) for adding the Unix options
|
||||
|
||||
##### Date ranges
|
||||
|
||||
**Messages sent before a date**
|
||||
Use `--before` to export messages sent before the provided date. E.g. messages sent before September 18th, 2019:
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --before 2019-09-18
|
||||
```
|
||||
|
||||
**Messages sent after a date**
|
||||
Use `--after` to export messages sent after the provided date. E.g. messages sent after September 17th, 2019 11:34 PM:
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --after "2019-09-17 23:34"
|
||||
```
|
||||
|
||||
**Messages sent in a date range**
|
||||
Use `--before` and `--after` to export messages sent during the provided date range. E.g. messages sent between September 17th, 2019 11:34 PM and September 18th:
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --after "2019-09-17 23:34" --before "2019-09-18"
|
||||
```
|
||||
|
||||
You can try different formats like `17-SEP-2019 11:34 PM` or even refine your ranges down to milliseconds `17-SEP-2019 23:45:30.6170`!
|
||||
Don't forget to quote (") the date if it has spaces!
|
||||
More info about .NET date formats [here](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings).
|
||||
|
||||
#### `exportdm`
|
||||
|
||||
| | Option | Description |
|
||||
| ------ | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **\*** | [-t](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#basic-usage) | [Authorization token](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#how-to-get-user-token). Environment variable: `DISCORD_TOKEN` |
|
||||
| | --parallel | Limits how many channels can be exported in parallel. Try to keep this number low so that your account doesn't get flagged. Default: 1 |
|
||||
| | [-o](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#changing-the-output-filename) | Output file or directory path |
|
||||
| | [-f](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#changing-the-format) | [Output file format](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#file-formats). Default: HtmlDark |
|
||||
| | [--after](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-ranges) | Only include messages sent after this date |
|
||||
| | [--before](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-ranges) | Only include messages sent before this date |
|
||||
| | [-p](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#partitioning) | Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb) |
|
||||
| | [--filter](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Message-filters.md) | Special notation for filtering the messages that get included in the export |
|
||||
| | [--media](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#Downloading-assets) | Download assets referenced by the export (user avatars, attached files, embedded images, etc.). Default: false |
|
||||
| | [--reuse-media](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#Reuse-assets) | Reuse previously downloaded assets to avoid redundant requests. Default: false. |
|
||||
| | [--dateformat](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-formats) | Date format used in output |
|
||||
| | -h | Shows help text |
|
||||
|
||||
> **Note**:
|
||||
> Options with an asterisk (**\***) are required. The order of the options doesn't matter.
|
||||
|
||||
This command exports all your Direct Messages.
|
||||
|
||||
#### `exportguild`
|
||||
|
||||
| | Option | Description |
|
||||
| ------ | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **\*** | -g | [Server ID](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#how-to-get-guild-id-or-guild-channel-id) |
|
||||
| **\*** | [-t](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#basic-usage) | [Authorization token](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#how-to-get-user-token). Environment variable: `DISCORD_TOKEN` |
|
||||
| | --parallel | Limits how many channels can be exported in parallel. Try to keep this number low so that your account doesn't get flagged. Default: 1 |
|
||||
| | [-o](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#changing-the-output-filename) | Output file or directory path |
|
||||
| | [-f](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#changing-the-format) | [Output file format](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#file-formats). Default: HtmlDark |
|
||||
| | [--after](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-ranges) | Only include messages sent after this date |
|
||||
| | [--before](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-ranges) | Only include messages sent before this date |
|
||||
| | [-p](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#partitioning) | Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb) |
|
||||
| | [--filter](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Message-filters.md) | Special notation for filtering the messages that get included in the export |
|
||||
| | [--media](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#Downloading-assets) | Download assets referenced by the export (user avatars, attached files, embedded images, etc.). Default: false |
|
||||
| | [--reuse-media](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#Reuse-assets) | Reuse previously downloaded assets to avoid redundant requests. Default: false. |
|
||||
| | [--dateformat](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-formats) | Date format used in output |
|
||||
| | -h | Shows help text |
|
||||
|
||||
> **Note**:
|
||||
> Options with an asterisk (**\***) are required. The order of the options doesn't matter.
|
||||
|
||||
This command exports all channels of a Server.
|
||||
|
||||
#### `exportall`
|
||||
|
||||
| | Option | Description |
|
||||
| ------ | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **\*** | [-t](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#basic-usage) | [Authorization token](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#how-to-get-user-token). Environment variable: `DISCORD_TOKEN` |
|
||||
| | --include-dm | Include direct message channels. Default: true |
|
||||
| | --parallel | Limits how many channels can be exported in parallel. Try to keep this number low so that your account doesn't get flagged. Default: 1 |
|
||||
| | [-o](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#changing-the-output-filename) | Output file or directory path |
|
||||
| | [-f](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#changing-the-format) | [Output file format](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#file-formats). Default: HtmlDark |
|
||||
| | [--after](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-ranges) | Only include messages sent after this date |
|
||||
| | [--before](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-ranges) | Only include messages sent before this date |
|
||||
| | [-p](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#partitioning) | Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb) |
|
||||
| | [--filter](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Message-filters.md) | Special notation for filtering the messages that get included in the export |
|
||||
| | [--media](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#Downloading-assets) | Download assets referenced by the export (user avatars, attached files, embedded images, etc.). Default: false |
|
||||
| | [--reuse-media](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#Reuse-assets) | Reuse previously downloaded assets to avoid redundant requests. Default: false. |
|
||||
| | [--dateformat](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#date-formats) | Date format used in output |
|
||||
| | -h | Shows help text |
|
||||
|
||||
> **Note**:
|
||||
> Options with an asterisk (**\***) are required. The order of the options doesn't matter.
|
||||
|
||||
This command exports all accessible channels, including server channels and DMs.
|
||||
|
||||
To exclude DMs, add the `--include-dm false` option:
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll exportall -t "mfa.Ifrn" --include-dm false
|
||||
```
|
||||
|
||||
#### `channels`
|
||||
|
||||
| | Option | Description |
|
||||
| ------ | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **\*** | -g | [Server ID](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#how-to-get-guild-id-or-guild-channel-id) |
|
||||
| **\*** | [-t](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#basic-usage) | [Authorization token](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#how-to-get-user-token). Environment variable: `DISCORD_TOKEN` |
|
||||
| | -h | Shows help text |
|
||||
|
||||
> **Note**:
|
||||
> Options with an asterisk (**\***) are required. The order of the options doesn't matter.
|
||||
|
||||
This command outputs the server channels in the following format:
|
||||
`Channel ID | Channel Name`
|
||||
To save the output to a file, run the command below. If the file already exists, it will be overwritten.
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll channels -t "mfa.Ifrn" -g 21814 > C:\path\to\output.txt
|
||||
```
|
||||
|
||||
#### `dm`
|
||||
|
||||
| | Option | Description |
|
||||
| ------ | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **\*** | [-t](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#basic-usage) | [Authorization token](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#how-to-get-user-token). Environment variable: `DISCORD_TOKEN` |
|
||||
| | -h | Shows help text |
|
||||
|
||||
> **Note**:
|
||||
> Options with an asterisk (**\***) are required. The order of the options doesn't matter.
|
||||
|
||||
This command outputs the list of Direct Messages.
|
||||
To save the output to a file, run the command with `> C:\path\…` like the example below. If the file already exists, it will be overwritten.
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll dm -t "mfa.Ifrn" > C:\path\to\output.txt
|
||||
```
|
||||
|
||||
#### `guilds`
|
||||
|
||||
| | Option | Description |
|
||||
| ------ | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **\*** | [-t](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#basic-usage) | [Authorization token](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#how-to-get-user-token). Environment variable: `DISCORD_TOKEN` |
|
||||
| | -h | Shows help text |
|
||||
|
||||
> **Note**:
|
||||
> Options with an asterisk (**\***) are required. The order of the options doesn't matter.
|
||||
|
||||
This command outputs a list of your Server List.
|
||||
To save the output to a file, run the command with `> C:\path\…` like the example below. If the file already exists, it will be overwritten.
|
||||
|
||||
```powershell
|
||||
dotnet DiscordChatExporter.Cli.dll guilds -t "mfa.Ifrn" > C:\path\to\output.txt
|
||||
```
|
||||
|
||||
#### `guide`
|
||||
|
||||
This command explains [how to get your token, guild, and channel IDs](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md).
|
||||
- [Using the GUI](Using-the-GUI.md)
|
||||
- [Using the CLI](Using-the-CLI.md)
|
||||
|
||||
## File formats
|
||||
|
||||
@@ -497,10 +61,3 @@ You can open `.json` files with a text editor, such as Notepad.
|
||||
|
||||
The CSV format allows for easy parsing of the chat log. Depending on your needs, the JSON format might be better.
|
||||
You can open `.csv` files with a text editor, such as Notepad, or a spreadsheet app, like Microsoft Excel and Google Sheets.
|
||||
|
||||
**([Back to top](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#getting-started))**
|
||||
|
||||
---
|
||||
|
||||
Further reading: [.NET date formats](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings)
|
||||
Special thanks to [@Yudi](https://github.com/Yudi)
|
||||
|
||||
+10
-4
@@ -2,7 +2,7 @@
|
||||
|
||||
## Installing .NET Runtime
|
||||
|
||||
Please follow the [instructions provided here](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Dotnet.md).
|
||||
Please follow the [instructions provided here](Dotnet.md).
|
||||
|
||||
## Downloading and using DiscordChatExporter.Cli
|
||||
|
||||
@@ -11,10 +11,16 @@ Please follow the [instructions provided here](https://github.com/Tyrrrz/Discord
|
||||
3. `cd` into the extracted folder. You can do this in Terminal by typing `cd`, then press the SPACE key, drag and drop the extracted folder into the Terminal window, and press the ENTER key.
|
||||
4. Replace `TOKEN` and `CHANNEL`, then execute this command to export:
|
||||
|
||||
```
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t TOKEN -c CHANNEL
|
||||
```
|
||||
|
||||
> [How to get Token and Channel IDs](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md).
|
||||
If the above command throws a "Permission denied" error, execute this command to fix the permissions:
|
||||
|
||||
There's much more you can do with DCE.CLI! Read the [CLI explained](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#dcecli-commands-) page to get started.
|
||||
```console
|
||||
chmod 644 *.dll DiscordChatExporter.*
|
||||
```
|
||||
|
||||
> [How to get Token and Channel IDs](Token-and-IDs.md).
|
||||
|
||||
There's much more you can do with DCE.CLI! Read the [CLI explained](Using-the-CLI.md) page to get started.
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@
|
||||
|
||||
## Installing .NET Runtime
|
||||
|
||||
Please follow the [instructions provided here](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Dotnet.md).
|
||||
Please follow the [instructions provided here](Dotnet.md).
|
||||
|
||||
## Downloading and using DiscordChatExporter.Cli
|
||||
|
||||
@@ -13,10 +13,10 @@ Please follow the [instructions provided here](https://github.com/Tyrrrz/Discord
|
||||
3. In the Terminal window, type `cd` , press the SPACE key, then drag and drop the extracted folder into the window, then press the RETURN key.
|
||||
4. Execute the following command to export, replacing `TOKEN` and `CHANNEL` with your own values:
|
||||
|
||||
```
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t TOKEN -c CHANNEL
|
||||
```
|
||||
|
||||
> [How to get Token and Channel IDs](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md).
|
||||
> [How to get Token and Channel IDs](Token-and-IDs.md).
|
||||
|
||||
There's much more you can do with DCE.CLI! Read the [CLI explained](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#dcecli-commands-) page to get started.
|
||||
There's much more you can do with DCE.CLI! Read the [CLI explained](Using-the-CLI.md) page to get started.
|
||||
|
||||
+17
-17
@@ -8,79 +8,79 @@ To configure a filter, specify it in advanced export parameters when using the G
|
||||
|
||||
- Filter by user
|
||||
|
||||
```
|
||||
```console
|
||||
from:Tyrrrz
|
||||
```
|
||||
|
||||
- Filter by user (with discriminator)
|
||||
|
||||
```
|
||||
```console
|
||||
from:Tyrrrz#1234
|
||||
```
|
||||
|
||||
- Filter by message content (allowed values: `link`, `embed`, `file`, `video`, `image`, `sound`)
|
||||
|
||||
```
|
||||
```console
|
||||
has:image
|
||||
```
|
||||
|
||||
- Filter by mentioned user (same rules apply as with `from:` filter)
|
||||
|
||||
```
|
||||
```console
|
||||
mentions:Tyrrrz#1234
|
||||
```
|
||||
|
||||
- Filter by contained text (has word "hello" and word "world" somewhere in the message text):
|
||||
|
||||
```
|
||||
```console
|
||||
hello world
|
||||
```
|
||||
|
||||
- Filter by contained text (has the string "hello world" somewhere in the message text):
|
||||
|
||||
```
|
||||
```console
|
||||
"hello world"
|
||||
```
|
||||
|
||||
- Combine multiple filters ('and'):
|
||||
|
||||
```
|
||||
```console
|
||||
from:Tyrrrz has:image
|
||||
```
|
||||
|
||||
- Same thing but with an explicit operator:
|
||||
|
||||
```
|
||||
```console
|
||||
from:Tyrrrz & has:image
|
||||
```
|
||||
|
||||
- Combine multiple filters ('or'):
|
||||
|
||||
```
|
||||
```console
|
||||
from:Tyrrrz | from:"96-LB"
|
||||
```
|
||||
|
||||
- Combine multiple filters using groups:
|
||||
|
||||
```
|
||||
```console
|
||||
(from:Tyrrrz | from:"96-LB") has:image
|
||||
```
|
||||
|
||||
- Negate a filter:
|
||||
|
||||
```
|
||||
```console
|
||||
-from:Tyrrrz | -has:image
|
||||
```
|
||||
|
||||
- Negate a grouped filter:
|
||||
|
||||
```
|
||||
```console
|
||||
-(from:Tyrrrz has:image)
|
||||
```
|
||||
|
||||
- Escape special characters (`-` is escaped below, so it's not parsed as negation operator):
|
||||
|
||||
```
|
||||
```console
|
||||
from:96\-LB
|
||||
```
|
||||
|
||||
@@ -88,18 +88,18 @@ from:96\-LB
|
||||
|
||||
In most cases, you will need to enclose your filter in quotes (`"`) to escape characters that may have special meaning in your shell:
|
||||
|
||||
```
|
||||
```console
|
||||
DiscordChatExporter.Cli export [...] --filter "from:Tyrrrz has:image"
|
||||
```
|
||||
|
||||
If you need to include quotes inside the filter itself as well, use single quotes (`'`) for those instead:
|
||||
|
||||
```
|
||||
```console
|
||||
DiscordChatExporter.Cli export [...] --filter "from:Tyrrrz 'hello world'"
|
||||
```
|
||||
|
||||
Additionally, negated filters (those that start with `-`) may cause parsing issues even when enclosed in quotes. To avoid this, use the tilde (`~`) character instead of the dash (`-`):
|
||||
|
||||
```
|
||||
```console
|
||||
DiscordChatExporter.Cli export [...] --filter ~from:Tyrrrz
|
||||
```
|
||||
```
|
||||
|
||||
+16
-16
@@ -2,21 +2,21 @@
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
- [Get .NET Core Runtime](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Dotnet.md) (Required for CLI; Installed automatically for GUI; Not required in Docker)
|
||||
- [Windows](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#gui-or-cli) | [macOS](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/MacOS.md) | [Linux](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Linux.md) | [Docker](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Docker.md)
|
||||
- [Get .NET Core Runtime](Dotnet.md) (Required for CLI; Installed automatically for GUI; Not required in Docker)
|
||||
- [Windows](Getting-started.md#gui-or-cli) | [macOS](MacOS.md) | [Linux](Linux.md) | [Docker](Docker.md)
|
||||
- Getting started:
|
||||
- [Using the GUI](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#using-the-gui)
|
||||
- [Using the CLI](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#using-the-cli)
|
||||
- [File formats](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#file-formats)
|
||||
- [Using the GUI](Using-the-CLI.md)
|
||||
- [Using the CLI](Using-the-CLI.md)
|
||||
- [File formats](Getting-started.md#file-formats)
|
||||
|
||||
## Guides
|
||||
|
||||
- [How to get Token and Channel IDs](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md)
|
||||
- [How to use message filters](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Message-filters.md)
|
||||
- [How to get Token and Channel IDs](Token-and-IDs.md)
|
||||
- [How to use message filters](Message-filters.md)
|
||||
- Export scheduling with CLI:
|
||||
- [Windows](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Scheduling-Windows.md)
|
||||
- [macOS](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Scheduling-MacOS.md)
|
||||
- [Linux](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Scheduling-Linux.md)
|
||||
- [Windows](Scheduling-Windows.md)
|
||||
- [macOS](Scheduling-MacOS.md)
|
||||
- [Linux](Scheduling-Linux.md)
|
||||
|
||||
## Video tutorial
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
|
||||
## FAQ & Troubleshooting
|
||||
|
||||
- [General questions](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Troubleshooting.md#general)
|
||||
- [First steps help](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Troubleshooting.md#first-steps)
|
||||
- [It's crashing/failing](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Troubleshooting.md#DCE-is-crashingfailing)
|
||||
- [.NET Core Runtime is required](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Troubleshooting.md#net-core-runtime-is-required)
|
||||
- [Errors](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Troubleshooting.md#errors)
|
||||
- [**More help**](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Troubleshooting.md)
|
||||
- [General questions](Troubleshooting.md#general)
|
||||
- [First steps help](Troubleshooting.md#first-steps)
|
||||
- [It's crashing/failing](Troubleshooting.md#DCE-is-crashingfailing)
|
||||
- [.NET Core Runtime is required](Troubleshooting.md#net-core-runtime-is-required)
|
||||
- [Errors](Troubleshooting.md#errors)
|
||||
- [**More help**](Troubleshooting.md)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# Scheduling exports with Cron
|
||||
|
||||
Make sure you already have **DiscordChatExporter.CLI** and **.NET Core** properly installed ([instructions here](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Linux.md)).
|
||||
Make sure you already have **DiscordChatExporter.CLI** and **.NET Core** properly installed ([instructions here](Linux.md)).
|
||||
|
||||
You can use Cron on macOS, but [this method](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-MacOS.md) is preferred.
|
||||
|
||||
---
|
||||
## Creating the script
|
||||
|
||||
1. Open Terminal and create a new text file with `nano /path/to/DiscordChatExporter/cron.sh`
|
||||
|
||||
@@ -66,8 +64,8 @@ exit 0
|
||||
|
||||
3. Replace:
|
||||
|
||||
- `tokenhere` with your [Token](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md).
|
||||
- `channelhere` with a [Channel ID](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md).
|
||||
- `tokenhere` with your [Token](Token-and-IDs.md).
|
||||
- `channelhere` with a [Channel ID](Token-and-IDs.md).
|
||||
- `dceFOLDERpathhere` with DCE's **directory path** (e.g. `/path/to/folder`, NOT `/path/to/folder/DiscordChatExporter.dll`).
|
||||
- `filenamehere` with the exported channel's filename, without spaces.
|
||||
- `dirhere` with the export directory (e.g. /home/user/Documents/Discord\ Exports).
|
||||
@@ -108,7 +106,7 @@ Verify your cron time [here](https://crontab.guru).
|
||||
|
||||
---
|
||||
|
||||
**Extra information**
|
||||
**Additional information**
|
||||
|
||||
The week starts on Sunday. 0 = SUN, 1 = MON ... 7 = SUN.
|
||||
|
||||
|
||||
+42
-42
@@ -2,7 +2,7 @@
|
||||
|
||||
Scheduling on macOS is a bit tricky, but it should work if you follow the instructions accordingly.
|
||||
|
||||
Make sure you already have **DiscordChatExporter.CLI** and **.NET Core** properly installed ([instructions here](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/macOS-usage-instructions.md)).
|
||||
Make sure you already have **DiscordChatExporter.CLI** and **.NET Core** properly installed ([instructions here](MacOS.md)).
|
||||
|
||||
## Creating the script
|
||||
|
||||
@@ -70,8 +70,8 @@ exit 0
|
||||
|
||||
4. Replace:
|
||||
|
||||
- `tokenhere` with your [Token](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md)
|
||||
- `channelhere` with a [Channel ID](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md)
|
||||
- `tokenhere` with your [Token](Token-and-IDs.md)
|
||||
- `channelhere` with a [Channel ID](Token-and-IDs.md)
|
||||
- `dceFOLDERpathhere` with DCE's **directory's path** (e.g. `/Users/user/Desktop/DiscordChatExporterFolder`, NOT `/Users/user/Desktop/DiscordChatExporterFolder/DiscordChatExporter.DLL`)
|
||||
- `filenamehere` with the exported channel's filename, without spaces
|
||||
- `dirhere` with the directory you want the files to be saved at (e.g. `/Users/user/Documents/Discord\ Exports`)
|
||||
@@ -88,7 +88,7 @@ After copying and pasting, make sure the file/folder name is not missing. If a f
|
||||
- `/Users/user/Documents/Discord\ Exports` - Correct ✓
|
||||
- `/Users/user/Desktop/DiscordChatExporter` - Correct ✓
|
||||
|
||||

|
||||

|
||||
|
||||
5. Save the file as `filename.sh`, not `.txt`
|
||||
6. Open Terminal.app, type `chmod +x`, press the SPACE key, then drag & drop the `filename.sh` into the Terminal window and hit RETURN. You may be prompted for your password, and you won't be able to see it as you type.
|
||||
@@ -101,13 +101,13 @@ Open TextEdit, make a Plain Text (⇧⌘T) and then paste the following into it:
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>local.discordchatexporter</string>
|
||||
<key>Program</key>
|
||||
<string>/path/to/filename.sh</string>
|
||||
REPLACEME
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>local.discordchatexporter</string>
|
||||
<key>Program</key>
|
||||
<string>/path/to/filename.sh</string>
|
||||
REPLACEME
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
@@ -138,16 +138,16 @@ The following example is to export every 3600 seconds (1 hour), replace the inte
|
||||
```xml
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Weekday</key>
|
||||
<integer>0</integer>
|
||||
<key>Month</key>
|
||||
<integer>0</integer>
|
||||
<key>Day</key>
|
||||
<integer>0</integer>
|
||||
<key>Hour</key>
|
||||
<integer>0</integer>
|
||||
<key>Minute</key>
|
||||
<integer>0</integer>
|
||||
<key>Weekday</key>
|
||||
<integer>0</integer>
|
||||
<key>Month</key>
|
||||
<integer>0</integer>
|
||||
<key>Day</key>
|
||||
<integer>0</integer>
|
||||
<key>Hour</key>
|
||||
<integer>0</integer>
|
||||
<key>Minute</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
```
|
||||
|
||||
@@ -168,17 +168,17 @@ Omitted keys are interpreted as wildcards, for example, if you delete the Minute
|
||||
|
||||
Be aware that if you set the day to '31', the script will only run on months that have the 31st day.
|
||||
|
||||
**Check the examples below ([or skip to step 3 (loading the file)](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-MacOS.md#3-loading-the-plist-into-launchctl)):**
|
||||
**Check the examples below ([or skip to step 3 (loading the file)](#3-loading-the-plist-into-launchctl)):**
|
||||
|
||||
Export everyday at 5:15 PM:
|
||||
|
||||
```xml
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>17</integer>
|
||||
<key>Minute</key>
|
||||
<integer>15</integer>
|
||||
<key>Hour</key>
|
||||
<integer>17</integer>
|
||||
<key>Minute</key>
|
||||
<integer>15</integer>
|
||||
</dict>
|
||||
|
||||
```
|
||||
@@ -188,8 +188,8 @@ Every 15 minutes of an hour (xx:15):
|
||||
```xml
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Minute</key>
|
||||
<integer>15</integer>
|
||||
<key>Minute</key>
|
||||
<integer>15</integer>
|
||||
</dict>
|
||||
|
||||
```
|
||||
@@ -199,20 +199,20 @@ Every Sunday at midnight and every Wednesday full hour (xx:00). Notice the inclu
|
||||
```xml
|
||||
<key>StartCalendarInterval</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Weekday</key>
|
||||
<integer>0</integer>
|
||||
<key>Hour</key>
|
||||
<integer>00</integer>
|
||||
<key>Minute</key>
|
||||
<integer>00</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Weekday</key>
|
||||
<integer>3</integer>
|
||||
<key>Minute</key>
|
||||
<integer>00</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Weekday</key>
|
||||
<integer>0</integer>
|
||||
<key>Hour</key>
|
||||
<integer>00</integer>
|
||||
<key>Minute</key>
|
||||
<integer>00</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Weekday</key>
|
||||
<integer>3</integer>
|
||||
<key>Minute</key>
|
||||
<integer>00</integer>
|
||||
</dict>
|
||||
</array>
|
||||
```
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
We'll be using [DiscordChatExporter CLI](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest), PowerShell, and Task Scheduler.
|
||||
|
||||
## Creating the script
|
||||
|
||||
1. Open a text editor such as Notepad and paste:
|
||||
|
||||
```powershell
|
||||
# Info: https://github.com/Tyrrrz/DiscordChatExporter/wiki
|
||||
```console
|
||||
# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs
|
||||
|
||||
$TOKEN = "tokenhere"
|
||||
$CHANNEL = "channelhere"
|
||||
@@ -31,8 +33,8 @@ exit
|
||||
|
||||
2. Replace:
|
||||
|
||||
- `tokenhere` with your [Token](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md)
|
||||
- `channelhere` with a [Channel ID](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md)
|
||||
- `tokenhere` with your [Token](Token-and-IDs.md)
|
||||
- `channelhere` with a [Channel ID](Token-and-IDs.md)
|
||||
- `exefolderhere` with the .exe **directory's path** (e.g. C:\Users\User\Desktop\DiscordChatExporter)
|
||||
- `filenamehere` with a filename without spaces
|
||||
- `dirhere` with the export directory (e.g. C:\Users\User\Documents\Exports)
|
||||
@@ -40,7 +42,9 @@ exit
|
||||
|
||||
Make sure not to delete the quotes (")
|
||||
|
||||
3. Save the file as `filename.ps1` not `.txt`
|
||||
3. Save the file as `filename.ps1`, not as `.txt`
|
||||
|
||||
> **Note**: You can also modify the script to use other options, such as `include-threads` or switch to a different command, e. g. `exportguild`.
|
||||
|
||||
## Export at Startup
|
||||
|
||||
@@ -49,14 +53,14 @@ Make sure not to delete the quotes (")
|
||||
|
||||
## Scheduling with Task Scheduler
|
||||
|
||||
Please notice your computer must be turned on so the exportation can occur.
|
||||
Please note that your computer must be turned on for the export to happen.
|
||||
|
||||
1. Press Windows + R, type `taskschd.msc` and press ENTER
|
||||
2. Select `Task Scheduler Library`, create a Basic Task, and follow the instructions on-screen
|
||||
|
||||
<img src="https://i.imgur.com/MHRVGDi.png" height="500"/>
|
||||
|
||||
[  ](https://i.imgur.com/3gHkF0t.png)
|
||||

|
||||
|
||||
3. At 'Start a Program', write `powershell -file -ExecutionPolicy ByPass -WindowStyle Hidden "C:\path\to\filename.ps1"` in the Program/script text box
|
||||
|
||||
|
||||
+42
-8
@@ -14,6 +14,23 @@ Prerequisite step: Navigate to [discord.com](https://discord.com) and login.
|
||||
|
||||
#### In Chrome
|
||||
|
||||
##### Using the console
|
||||
|
||||
1. <img width="500" align="right" src="https://i.imgur.com/zdDwIT5.jpg" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>I</kbd> on macOS). Chrome's [DevTools](https://developer.chrome.com/docs/devtools/overview) tools will display.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
2. Click the `Console` tab. The [console](https://developer.chrome.com/docs/devtools/console/) will open.
|
||||
|
||||
3. Type
|
||||
|
||||
```console
|
||||
(webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken()
|
||||
```
|
||||
|
||||
into the console and press <kbd>Enter</kbd>. The console will display your user token.
|
||||
|
||||
##### Using the network monitor
|
||||
|
||||
1. <img width="500" align="right" src="https://i.imgur.com/zdDwIT5.jpg" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>I</kbd> on macOS). Chrome's [DevTools](https://developer.chrome.com/docs/devtools/overview) tools will display.
|
||||
@@ -31,12 +48,12 @@ Prerequisite step: Navigate to [discord.com](https://discord.com) and login.
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
4. <img width="500" align="right" src="https://i.imgur.com/rnZG8Id.png" />Click the text box labelled `Filter` and type `library`. The entries will filter down to a single entry named `library`.
|
||||
4. <img width="500" align="right" src="https://i.imgur.com/rnZG8Id.png" />Click the text box labelled `Filter` and type `messages`. The entries will filter down to a single request named `messages`. If the request doesn't appear, switch to any other Discord channel to trigger it.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
5. <img width="500" align="right" src="https://i.imgur.com/29dE3fR.png" />Click the entry named `library`. A panel will open to the right and display details about the entry. Click the `Headers` tab if it isn't already active.
|
||||
5. <img width="500" align="right" src="https://i.imgur.com/29dE3fR.png" />Click the entry named `messages`. A panel will open to the right and display details about the entry. Click the `Headers` tab if it isn't already active.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
@@ -90,6 +107,23 @@ Prerequisite step: Navigate to [discord.com](https://discord.com) and login.
|
||||
|
||||
#### In Firefox
|
||||
|
||||
##### Using the console
|
||||
|
||||
1. <img width="500" align="right" src="https://i.imgur.com/O34nwdG.png" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>K</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>K</kbd> on macOS). Firefox’s [web developer tools](https://firefox-source-docs.mozilla.org/devtools-user/) will display at the bottom of the window, and the [web console](https://firefox-source-docs.mozilla.org/devtools-user/console/index.html) will display.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
2. Click the `Console` tab. The [console](https://firefox-source-docs.mozilla.org/devtools-user/console/index.html) will open.
|
||||
|
||||
1. Type
|
||||
|
||||
```console
|
||||
(webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken()
|
||||
```
|
||||
|
||||
into the console and press <kbd>Enter</kbd>. The console will display your user token.
|
||||
|
||||
##### Using the network monitor
|
||||
|
||||
1. <img width="500" align="right" src="https://i.imgur.com/O34nwdG.png" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>E</kbd> on macOS). Firefox’s [web developer tools](https://firefox-source-docs.mozilla.org/devtools-user/) will display at the bottom of the window, and the [network monitor](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/) will display.
|
||||
@@ -102,12 +136,12 @@ Prerequisite step: Navigate to [discord.com](https://discord.com) and login.
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
3. <img width="500" align="right" src="https://i.imgur.com/efUCfBO.png" />Type `library` into the filter. The network request list will filter out any entries not containing the string `library`.
|
||||
3. <img width="500" align="right" src="https://i.imgur.com/efUCfBO.png" />Type `messages` into the filter. The network request list will filter out any entries not containing the string `messages`. If the request doesn't appear, switch to any other Discord channel to trigger it.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
4. <img width="500" align="right" src="https://i.imgur.com/cdJZ7Q1.png" />Click `library`. The [network request details pane](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/request_details/index.html) will display. The [headers tab](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/request_details/index.html#network-monitor-request-details-headers-tab) should be active by default. If it isn’t, click it.
|
||||
4. <img width="500" align="right" src="https://i.imgur.com/cdJZ7Q1.png" />Click `messages`. The [network request details pane](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/request_details/index.html) will display. The [headers tab](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/request_details/index.html#network-monitor-request-details-headers-tab) should be active by default. If it isn’t, click it.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
@@ -164,7 +198,7 @@ Prerequisite step: Navigate to [discord.com](https://discord.com) and login.
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
### Through the desktop app / Enabling web developer tools
|
||||
### Through the desktop app / enabling web developer tools
|
||||
|
||||
#### By editing the settings file
|
||||
|
||||
@@ -198,7 +232,7 @@ Prerequisite step: Navigate to [discord.com](https://discord.com) and login.
|
||||
|
||||
4. Launch Discord.
|
||||
|
||||
5. To find your user token, continue [here](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#in-chrome).
|
||||
5. To find your user token, continue [here](#in-chrome).
|
||||
|
||||
#### Via settings menu (BetterDiscord only)
|
||||
|
||||
@@ -218,7 +252,7 @@ Prerequisite step: Navigate to [discord.com](https://discord.com) and login.
|
||||
<br />
|
||||
|
||||
4. Press <kbd>Esc</kbd>. The settings page will close.
|
||||
5. To find your user token, continue [here](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#in-chrome).
|
||||
5. To find your user token, continue [here](#in-chrome).
|
||||
|
||||
## How to get a Bot Token
|
||||
|
||||
@@ -239,4 +273,4 @@ Prerequisite step: Navigate to [discord.com](https://discord.com) and login.
|
||||
1. Open Discord Settings
|
||||
2. Go to the **Advanced** section
|
||||
3. Enable **Developer Mode**
|
||||
4. Right-click on the desired server or channel and click Copy Server ID or Copy Channel ID
|
||||
4. Right-click on the desired server or channel and click **Copy Server ID** or **Copy Channel ID**
|
||||
|
||||
+39
-32
@@ -6,7 +6,7 @@ Here you'll find the answers to most of the questions related to **DiscordChatEx
|
||||
- ❓ If you still have unanswered questions _after_ reading this page, feel free to [create a new discussion](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new).
|
||||
- 🐞 If you have encountered a problem that's not described here, has not [been discussed before](https://github.com/Tyrrrz/DiscordChatExporter/discussions), and is not a [known issue](https://github.com/Tyrrrz/DiscordChatExporter/issues?q=is%3Aissue), please [create a new discussion](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) or [open a bug report](https://github.com/Tyrrrz/DiscordChatExporter/issues/new). Don't forget to include your platform (Windows, Mac, Linux, etc.) and a detailed description of your question/problem.
|
||||
|
||||
## General
|
||||
## General questions
|
||||
|
||||
### Token stealer?
|
||||
|
||||
@@ -20,7 +20,7 @@ A token can be used to log into your account, so treat it like a password and ne
|
||||
|
||||
### How can I reset my token?
|
||||
|
||||
Follow the [instructions here](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md).
|
||||
Follow the [instructions here](Token-and-IDs.md).
|
||||
|
||||
### Will I get banned if I use this?
|
||||
|
||||
@@ -46,6 +46,10 @@ Yes, and other media too. Export using the "Download media" (`--media`) option.
|
||||
|
||||
Yes.
|
||||
|
||||
### Can DCE export multiple formats at once?
|
||||
|
||||
No, you can only export one format at a time.
|
||||
|
||||
### Can DCE recreate the exported chats in Discord?
|
||||
|
||||
No, DCE is an exporter.
|
||||
@@ -54,49 +58,52 @@ No, DCE is an exporter.
|
||||
|
||||
No, DCE is an exporter.
|
||||
|
||||
### Can DCE add new messages to an existing export?
|
||||
|
||||
No.
|
||||
|
||||
## First steps
|
||||
|
||||
### How can I find my token?
|
||||
|
||||
Check the following page: [Obtaining token](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md)
|
||||
Check the following page: [Obtaining token](Token-and-IDs.md)
|
||||
|
||||
### When I open DCE a black window pops up quickly or nothing shows up.
|
||||
### When I open DCE a black window pops up quickly or nothing shows up
|
||||
|
||||
If you have [.NET Core Runtime correctly installed](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Dotnet.md), you might have downloaded DCE.CLI, try [downloading the GUI](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#gui-or-cli) instead.
|
||||
|
||||
### I can't open DCE. It sends me to a Visual Studio webpage.
|
||||
|
||||
Check the following page:
|
||||
[Installation and usage](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Readme.md#installation--usage)
|
||||
If you have [.NET Core Runtime correctly installed](Dotnet.md), you might have downloaded the CLi flavor, try [downloading the GUI](Getting-started.md#gui-or-cli) instead.
|
||||
|
||||
### How do I run DCE on macOS or Linux?
|
||||
|
||||
Check the following pages:
|
||||
|
||||
- [macOS usage instructions](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/MacOS.md)
|
||||
- [Linux usage instructions](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Linux.md)
|
||||
- [macOS usage instructions](MacOS.md)
|
||||
- [Linux usage instructions](Linux.md)
|
||||
|
||||
### How can I set DCE to export automatically at certain times?
|
||||
|
||||
Check the following pages to learn how to schedule **DiscordChatExporter.CLI** runs (advanced):
|
||||
|
||||
- [Windows scheduling](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-windows.md)
|
||||
- [macOS scheduling](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-MacOS.md)
|
||||
- [Linux scheduling](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-Linux.md)
|
||||
- [Windows scheduling](Scheduling-Windows.md)
|
||||
- [macOS scheduling](Scheduling-MacOS.md)
|
||||
- [Linux scheduling](Scheduling-Linux.md)
|
||||
|
||||
### The exported file is too large, I can't open it!
|
||||
### The exported file is too large, I can't open it
|
||||
|
||||
Try opening it with a different program, try partitioning or use a different file format, like `PlainText`.
|
||||
|
||||
### DCE is crashing/failing.
|
||||
### DCE is crashing/failing
|
||||
|
||||
Check the following page: [Installing .NET Core Runtime](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Dotnet.md)
|
||||
Check the following page: [Installing .NET Core Runtime](Dotnet.md)
|
||||
|
||||
If you already have .NET Core installed, please check if your problem is a [known issue](https://github.com/Tyrrrz/DiscordChatExporter/issues?q=is%3Aissue) before [opening a bug report](https://github.com/Tyrrrz/DiscordChatExporter/issues/new).
|
||||
|
||||
### .NET Core Runtime is required.
|
||||
### .NET Core Runtime is required
|
||||
|
||||
Check the following page: [Installing .NET Core Runtime](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Dotnet.md)
|
||||
Check the following page: [Installing .NET Core Runtime](Dotnet.md)
|
||||
|
||||
### I see messages in the export, but they have no content
|
||||
|
||||
Your bot is missing the 'Message Content Intent'. Go to the [Discord Developer Portal](https://discord.com/developers/applications), navigate to the 'Bot' section and enable it.
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -104,54 +111,54 @@ Check the following page: [Installing .NET Core Runtime](https://github.com/Tyrr
|
||||
|
||||
Check the following page:
|
||||
|
||||
- [Using the CLI](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#using-the-cli)
|
||||
- [Using the CLI](Using-the-CLI.md)
|
||||
|
||||
If you're using **Docker**, please refer to the [Docker Usage Instructions](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Docker.md) instead.
|
||||
If you're using **Docker**, please refer to the [Docker Usage Instructions](Docker.md) instead.
|
||||
|
||||
### Where can I find the 'Channel IDs'?
|
||||
|
||||
Check the following page:
|
||||
|
||||
- [Obtaining Channel IDs](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md)
|
||||
- [Obtaining Channel IDs](Token-and-IDs.md)
|
||||
|
||||
### I can't find Docker exported chats
|
||||
|
||||
Check the following page:
|
||||
|
||||
- [Docker usage instructions](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/docker)
|
||||
- [Docker usage instructions](Docker.md)
|
||||
|
||||
### I can't export Direct Messages
|
||||
|
||||
Make sure you're [copying the DM Channel ID](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md#how-to-get-a-direct-message-channel-id), not the person's user ID.
|
||||
Make sure you're [copying the DM Channel ID](Token-and-IDs.md#how-to-get-a-direct-message-channel-id), not the person's user ID.
|
||||
|
||||
## Errors
|
||||
|
||||
```
|
||||
```console
|
||||
DiscordChatExporter.Domain.Exceptions.DiscordChatExporterException: Authentication token is invalid.
|
||||
...
|
||||
```
|
||||
|
||||
↳ Make sure the provided token is correct.
|
||||
|
||||
```
|
||||
```console
|
||||
DiscordChatExporter.Domain.Exceptions.DiscordChatExporterException: Requested resource does not exist.
|
||||
```
|
||||
|
||||
↳ Check your channel ID, it might be invalid. [Read this if you need help](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md).
|
||||
↳ Check your channel ID, it might be invalid. [Read this if you need help](Token-and-IDs.md).
|
||||
|
||||
```
|
||||
```console
|
||||
DiscordChatExporter.Domain.Exceptions.DiscordChatExporterException: Access is forbidden.
|
||||
```
|
||||
|
||||
↳ This means you don't have access to the channel.
|
||||
|
||||
```
|
||||
```console
|
||||
The application to execute does not exist:
|
||||
```
|
||||
|
||||
↳ The `DiscordChatExporter.Cli.dll` file is missing. Keep the `.exe` and all the `.dll` files together. If you didn't move the files, try unzipping again.
|
||||
|
||||
```
|
||||
```console
|
||||
System.Net.WebException: Error: TrustFailure ... Invalid certificate received from server.
|
||||
...
|
||||
```
|
||||
@@ -167,5 +174,5 @@ If it still doesn't work, try mozroots: `mozroots --import --ask-remove`
|
||||
---
|
||||
|
||||
> ❓ If you still have unanswered questions, feel free to [create a new discussion](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new).
|
||||
|
||||
>
|
||||
> 🐞 If you have encountered a problem that's not described here, has not [been discussed before](https://github.com/Tyrrrz/DiscordChatExporter/discussions), and is not a [known issue](https://github.com/Tyrrrz/DiscordChatExporter/issues?q=is%3Aissue), please [create a new discussion](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) or [open a bug report](https://github.com/Tyrrrz/DiscordChatExporter/issues/new).
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
# Using the CLI
|
||||
|
||||
## Guide
|
||||
|
||||
> **Note**:
|
||||
> Make sure you have [.NET Core installed](Dotnet.md) before attempting to run the commands below.
|
||||
> **Docker** users, please refer to the [Docker usage instructions](Docker.md).
|
||||
|
||||
## Step 1
|
||||
|
||||
After extracting the `.zip`, open Command Prompt, aka `cmd` (`Terminal` on **macOS** and **Linux**).
|
||||
|
||||
## Step 2
|
||||
|
||||
Change the current directory to DCE's folder with `cd C:\path\to\directory`, then press ENTER to run the command.
|
||||
|
||||
**Windows** users can quickly get the directory's path by clicking the address bar while inside the folder.
|
||||

|
||||
|
||||
**macOS** users can select the `.exe`, hit Command+I (⌘I), and copy what's after `Where:` to get the directory
|
||||

|
||||
|
||||
You can also drag and drop the folder on **every platform**.
|
||||

|
||||
|
||||
## Step 3
|
||||
|
||||
Now we're ready to run the commands. The examples on this page follow the Windows file path format, change the file
|
||||
paths according to your system.
|
||||
|
||||
Type the following in Command Prompt (Terminal), then press ENTER to run it. This will list DCE's options.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll
|
||||
```
|
||||
|
||||
> **Docker** users, please refer to the [Docker usage instructions](Docker.md).
|
||||
|
||||
## CLI commands
|
||||
|
||||
| Command | Description |
|
||||
|-------------------------|-----------------------------------------------------|
|
||||
| export | Exports a channel |
|
||||
| exportdm | Exports all direct message channels |
|
||||
| exportguild | Exports all channels within the specified server |
|
||||
| exportall | Exports all accessible channels |
|
||||
| channels | Outputs the list of channels in the given server |
|
||||
| dm | Outputs the list of direct message channels |
|
||||
| guilds | Outputs the list of accessible servers |
|
||||
| guide | Explains how to obtain token, guild, and channel ID |
|
||||
|
||||
To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `dotnet DiscordChatExporter.Cli.dll guide`.
|
||||
|
||||
To get help with a specific command, run:
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll command --help
|
||||
```
|
||||
|
||||
For example, to figure out how to use the `export` command, run:
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export --help
|
||||
```
|
||||
|
||||
## Export a specific channel
|
||||
|
||||
You can quickly export with DCE's default settings by using just `-t token` and `-c channelid`.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555
|
||||
```
|
||||
|
||||
#### Changing the format
|
||||
|
||||
You can change the export format to `HtmlDark`, `HtmlLight`, `PlainText` `Json` or `Csv` with `-f format`. The default
|
||||
format is `HtmlDark`.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -f Json
|
||||
```
|
||||
|
||||
#### Changing the output filename
|
||||
|
||||
You can change the filename by using `-o name.ext`. e.g. for the `HTML` format:
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -o myserver.html
|
||||
```
|
||||
|
||||
#### Changing the output directory
|
||||
|
||||
You can change the export directory by using `-o` and providing a path that ends with a slash or does not have a file
|
||||
extension.
|
||||
If any of the folders in the path have a space in its name, escape them with quotes (").
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -o "C:\Discord Exports"
|
||||
```
|
||||
|
||||
#### Changing the filename and output directory
|
||||
|
||||
You can change both the filename and export directory by using `-o directory\name.ext`.
|
||||
Note that the filename must have an extension, otherwise it will be considered a directory name.
|
||||
If any of the folders in the path have a space in its name, escape them with quotes (").
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -o "C:\Discord Exports\myserver.html"
|
||||
```
|
||||
|
||||
#### Generating the filename and output directory dynamically
|
||||
|
||||
You can use template tokens to generate the output file path based on the guild and channel metadata.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -o "C:\Discord Exports\%G\%T\%C.html"
|
||||
```
|
||||
|
||||
Assuming you are exporting a channel named `"my-channel"` in the `"Text channels"` category from a server
|
||||
called `"My server"`, you will get the following output file
|
||||
path: `C:\Discord Exports\My server\Text channels\my-channel.html`
|
||||
|
||||
Here is the full list of supported template tokens:
|
||||
|
||||
- `%g` - guild ID
|
||||
- `%G` - guild name
|
||||
- `%t` - category ID
|
||||
- `%T` - category name
|
||||
- `%c` - channel ID
|
||||
- `%C` - channel name
|
||||
- `%p` - channel position
|
||||
- `%P` - category position
|
||||
- `%a` - the "after" date
|
||||
- `%b` - the "before" date
|
||||
- `%%` - escapes `%`
|
||||
|
||||
#### Partitioning
|
||||
|
||||
You can use partitioning to split files after a given number of messages or file size.
|
||||
For example, a channel with 36 messages set to be partitioned every 10 messages will output 4 files.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -p 10
|
||||
```
|
||||
|
||||
A 45 MB channel set to be partitioned every 20 MB will output 3 files.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 -p 20mb
|
||||
```
|
||||
|
||||
#### Downloading assets
|
||||
|
||||
If this option is set, the export will include additional files such as user avatars, attached files, images, etc.
|
||||
Only files that are referenced by the export are downloaded, which means that, for example, user avatars will not be
|
||||
downloaded when using the plain text (TXT) export format.
|
||||
A folder containing the assets will be created along with the exported chat. They must be kept together.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --media
|
||||
```
|
||||
|
||||
#### Reusing assets
|
||||
|
||||
Previously downloaded assets can be reused to skip redundant downloads as long as the chat is always exported to the
|
||||
same folder. Using this option can speed up future exports. This option requires the `--media` option.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --media --reuse-media
|
||||
```
|
||||
|
||||
#### Changing the media directory
|
||||
|
||||
By default, the media directory is created alongside the exported chat. You can change this by using `--media-dir` and
|
||||
providing a path that ends with a slash. All of the exported media will be stored in this directory.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --media --media-dir "C:\Discord Media"
|
||||
```
|
||||
|
||||
#### Changing the date format
|
||||
|
||||
You can customize how dates are formatted in the exported files by using `--locale` and inserting one of Discord's
|
||||
locales. The default locale is `en-US`.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --locale "de-DE"
|
||||
```
|
||||
|
||||
#### Date ranges
|
||||
|
||||
**Messages sent before a date**
|
||||
Use `--before` to export messages sent before the provided date. E.g. messages sent before September 18th, 2019:
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --before 2019-09-18
|
||||
```
|
||||
|
||||
**Messages sent after a date**
|
||||
Use `--after` to export messages sent after the provided date. E.g. messages sent after September 17th, 2019 11:34 PM:
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --after "2019-09-17 23:34"
|
||||
```
|
||||
|
||||
**Messages sent in a date range**
|
||||
Use `--before` and `--after` to export messages sent during the provided date range. E.g. messages sent between
|
||||
September 17th, 2019 11:34 PM and September 18th:
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll export -t "mfa.Ifrn" -c 53555 --after "2019-09-17 23:34" --before "2019-09-18"
|
||||
```
|
||||
|
||||
You can try different formats like `17-SEP-2019 11:34 PM` or even refine your ranges down to
|
||||
milliseconds `17-SEP-2019 23:45:30.6170`!
|
||||
Don't forget to quote (") the date if it has spaces!
|
||||
More info about .NET date
|
||||
formats [here](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings).
|
||||
|
||||
### Export channels from a specific guild (server)
|
||||
|
||||
To export all channels in a specific guild, use the `exportguild` command and provide the guild ID through the `-g|--guild` option:
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll exportguild -t "mfa.Ifrn" -g 21814
|
||||
```
|
||||
|
||||
#### Including threads
|
||||
|
||||
By default, threads are not included in the export. You can change this behavior by using `--include-threads` and
|
||||
specifying which threads should be included. It has possible values of `none`, `active`, or `all`, indicating which
|
||||
threads should be included. To include both active and archived threads, use `--include-threads all`.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll exportguild -t "mfa.Ifrn" -g 21814 --include-threads all
|
||||
```
|
||||
|
||||
#### Including voice channels
|
||||
|
||||
By default, voice channels are included in the export. You can change this behavior by using `--include-vc` and
|
||||
specifying whether to include voice channels in the export. It has possible values of `true` or `false`, to exclude
|
||||
voice channels, use `--include-vc false`.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll exportguild -t "mfa.Ifrn" -g 21814 --include-vc false
|
||||
```
|
||||
|
||||
### Export all channels
|
||||
|
||||
To export all accessible channels, use the `exportall` command:
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll exportall -t "mfa.Ifrn"
|
||||
```
|
||||
|
||||
#### Excluding DMs
|
||||
|
||||
To exclude DMs, add the `--include-dm false` option.
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll exportall -t "mfa.Ifrn" --include-dm false
|
||||
```
|
||||
|
||||
### List channels in a guild (server)
|
||||
|
||||
To list the channels available in a specific guild, use the `channels` command and provide the guild ID through the `-g|--guild` option:
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll channels -t "mfa.Ifrn" -g 21814
|
||||
```
|
||||
|
||||
### List direct message channels
|
||||
|
||||
To list all DM channels accessible to the current account, use the `dm` command:
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll dm -t "mfa.Ifrn"
|
||||
```
|
||||
|
||||
### List guilds (servers)
|
||||
|
||||
To list all guilds accessible by the current account, use the `guilds` command:
|
||||
|
||||
```console
|
||||
dotnet DiscordChatExporter.Cli.dll guilds -t "mfa.Ifrn" > C:\path\to\output.txt
|
||||
```
|
||||
@@ -0,0 +1,97 @@
|
||||
# Using the GUI
|
||||
|
||||
## Video tutorial
|
||||
|
||||
[](https://youtube.com/watch?v=jjtu0VQXV7I)
|
||||
|
||||
> Video by [NoIntro Tutorials](https://youtube.com/channel/UCFezKSxdNKJe77-hYiuXu3Q).
|
||||
|
||||
## Guide
|
||||
|
||||
### Step 1
|
||||
|
||||
After extracting the `.zip`, open `DiscordChatExporter.exe`
|
||||
|
||||
### Step 2
|
||||
|
||||
Please refer to the on-screen instructions to get your token, then paste your token in the upper text box and hit ENTER or click the arrow (→).
|
||||
|
||||
> **Warning**:
|
||||
> **Never share your token!**
|
||||
> A token gives full access to an account, treat it like a password.
|
||||
|
||||
<img src="https://i.imgur.com/SuLQ5tZ.png" height="400"/>
|
||||
|
||||
### Step 3
|
||||
|
||||
DCE will display your Direct Messages and a sidebar with your server list. Select the channel you would like to export, then click the  button to continue.
|
||||
|
||||
> **Note**:
|
||||
> You can export multiple channels at once by holding `CTRL` or `SHIFT` while selecting.
|
||||
> You can also double-click a channel to export it without clicking the  button.
|
||||
|
||||
<img src="https://i.imgur.com/JHMFRh2.png" height="400"/>
|
||||
|
||||
### Step 4
|
||||
|
||||
In this screen you can customize the following:
|
||||
|
||||
- **Output path** - The folder where the exported chat(s) will be saved.
|
||||
|
||||
- **Export format** - HTML (Dark), HTML (Light), TXT, CSV and JSON
|
||||
|
||||
- **Date range (after/before)** (Optional) - If set, only messages sent in the provided date range will be exported. Only one value (either after or before) is required if you want to use this option.
|
||||
> **Note**:
|
||||
> Please note that the time defaults to **12:00 AM** (midnight/00:00). This means that if you choose to export between Sep 17th and Sep 18th, messages from Sep 18th won't be exported.
|
||||
|
||||
- **Partition limit** (Optional) - Split output into partitions, each limited to this number of messages (e.g. 100) or file size (e.g. 10mb). For example, a channel with 36 messages set to be partitioned every 10 messages will output 4 files.
|
||||
|
||||
- **Message Filter** (Optional) - Special notation for filtering the messages that get included in the export. See [Message filters](Message-filters.md) for more info.
|
||||
|
||||
- **Format markdown** (Optional) - Disable markdown processing when exporting. You can use this to produce JSON or plain text exports without unwrapping mentions, custom emoji, and certain other special tokens.
|
||||
|
||||
- **Download assets** (Optional) - If this option is set, the export will include additional files such as user avatars, attached files, images, etc. Only files that are referenced by the export are downloaded, which means that, for example, user avatars will not be downloaded when using the plain text (TXT) export format. A folder containing the assets will be created along with the exported chat. They must be kept together.
|
||||
|
||||
- **Reuse assets** (Optional) - If this option is set, the export will reuse already downloaded assets to skip redundant requests. This option is only available when **Download assets** is enabled.
|
||||
|
||||
- **Assets directory path** (Optional) - If this option is set, the export will use the specified directory to store assets from all exported channels in the same place.
|
||||
|
||||
> **Note**:
|
||||
> You need to scroll down to see all available options.
|
||||
|
||||
## Settings
|
||||
|
||||
- **Auto-update**
|
||||
Perform automatic updates on every launch.
|
||||
Default: Enabled
|
||||
|
||||
> **Note**:
|
||||
> Keep this option enabled to receive the latest features and bug fixes!
|
||||
|
||||
- **Dark mode**
|
||||
Use darker colors in the UI (User Interface).
|
||||
Default: Disabled
|
||||
|
||||
- **Persist token**
|
||||
Persist last used token between sessions.
|
||||
Default: Enabled
|
||||
|
||||
- **Show threads**
|
||||
Controls whether threads are shown in the channel list.
|
||||
Default: none
|
||||
|
||||
- **Locale**
|
||||
Customize how dates are formatted in the exported files.
|
||||
|
||||
- **Date format**
|
||||
Customize how dates are formatted in the exported files in the settings menu ().
|
||||
|
||||
- **Parallel limit**
|
||||
The number of channels that will be exported at the same time.
|
||||
Default: 1
|
||||
|
||||
- **Normalize to UTC**
|
||||
Convert all dates to UTC before exporting.
|
||||
|
||||
> **Note**:
|
||||
> Try to keep this number low so that your account doesn't get flagged.
|
||||
@@ -1,9 +1,14 @@
|
||||
name: docker
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# Outputs from this job aren't really used, but it's here to verify that
|
||||
# the Dockerfile builds correctly on pull requests.
|
||||
pack:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
@@ -11,37 +16,38 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # 4.1.0
|
||||
|
||||
- name: Install Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # 3.0.0
|
||||
|
||||
- name: Build image
|
||||
run: >
|
||||
docker buildx build
|
||||
docker buildx build .
|
||||
--file DiscordChatExporter.Cli.dockerfile
|
||||
--platform linux/amd64,linux/arm64
|
||||
--output type=tar,dest=DiscordChatExporter.Cli.Docker.tar
|
||||
.
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # 3.1.3
|
||||
with:
|
||||
name: DiscordChatExporter.Cli.Docker
|
||||
path: DiscordChatExporter.Cli.Docker.tar
|
||||
if-no-files-found: error
|
||||
|
||||
deploy-latest:
|
||||
if: ${{ github.event_name == 'push' && github.ref_type == 'branch' && github.ref_name == 'master' }}
|
||||
deploy:
|
||||
# Deploy to DockerHub only on tag push or master branch push
|
||||
if: ${{ github.ref_type == 'tag' || github.ref_type == 'branch' && github.ref_name == 'master' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # 4.1.0
|
||||
|
||||
- name: Install Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # 3.0.0
|
||||
|
||||
- name: Login to DockerHub
|
||||
run: >
|
||||
@@ -50,37 +56,10 @@ jobs:
|
||||
|
||||
- name: Build & push image
|
||||
run: >
|
||||
docker buildx build
|
||||
docker buildx build .
|
||||
--file DiscordChatExporter.Cli.dockerfile
|
||||
--platform linux/amd64,linux/arm64
|
||||
--push
|
||||
--tag tyrrrz/discordchatexporter:latest
|
||||
.
|
||||
|
||||
deploy-stable:
|
||||
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
run: >
|
||||
echo ${{ secrets.DOCKER_TOKEN }} |
|
||||
docker login --username tyrrrz --password-stdin
|
||||
|
||||
- name: Build & push image
|
||||
run: >
|
||||
docker buildx build
|
||||
--file DiscordChatExporter.Cli.dockerfile
|
||||
--platform linux/amd64,linux/arm64
|
||||
--push
|
||||
--tag tyrrrz/discordchatexporter:stable
|
||||
--tag tyrrrz/discordchatexporter:${{ github.ref_name }}
|
||||
.
|
||||
${{ github.ref_type == 'tag' && '--tag tyrrrz/discordchatexporter:$GITHUB_REF_NAME' || '' }}
|
||||
${{ github.ref_type == 'tag' && '--tag tyrrrz/discordchatexporter:stable' || '' }}
|
||||
|
||||
+109
-80
@@ -1,139 +1,167 @@
|
||||
name: main
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
format:
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # 4.1.0
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # 3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Verify formatting
|
||||
run: >
|
||||
dotnet build
|
||||
-t:CSharpierFormat
|
||||
--configuration Release
|
||||
|
||||
test:
|
||||
# Tests need access to secrets, so we can't run them against PRs because of limited trust
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # 4.1.0
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # 3.2.0
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Run tests
|
||||
# Tests need access to secrets, so we can't run them against PRs because of limited trust
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
env:
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
run: >
|
||||
dotnet test
|
||||
-p:CSharpier_Bypass=true
|
||||
--configuration Release
|
||||
--logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true"
|
||||
--collect:"XPlat Code Coverage"
|
||||
--
|
||||
RunConfiguration.CollectSourceInformation=true
|
||||
DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
|
||||
env:
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # 3.1.4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
pack:
|
||||
needs: test
|
||||
strategy:
|
||||
matrix:
|
||||
app:
|
||||
- DiscordChatExporter.Cli
|
||||
- DiscordChatExporter.Gui
|
||||
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # 4.1.0
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # 3.2.0
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Publish (CLI)
|
||||
- name: Publish app
|
||||
run: >
|
||||
dotnet publish DiscordChatExporter.Cli
|
||||
--output DiscordChatExporter.Cli/publish/
|
||||
dotnet publish ${{ matrix.app }}
|
||||
-p:CSharpier_Bypass=true
|
||||
--output ${{ matrix.app }}/bin/publish/
|
||||
--configuration Release
|
||||
|
||||
- name: Publish (GUI)
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # 3.1.3
|
||||
with:
|
||||
name: ${{ matrix.app }}
|
||||
path: ${{ matrix.app }}/bin/publish/
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
|
||||
needs:
|
||||
- format
|
||||
- test
|
||||
- pack
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: >
|
||||
dotnet publish DiscordChatExporter.Gui
|
||||
--output DiscordChatExporter.Gui/publish/
|
||||
--configuration Release
|
||||
|
||||
- name: Upload artifacts (CLI)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: DiscordChatExporter.Cli
|
||||
path: DiscordChatExporter.Cli/publish/
|
||||
|
||||
- name: Upload artifacts (GUI)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: DiscordChatExporter
|
||||
path: DiscordChatExporter.Gui/publish/
|
||||
gh release create ${{ github.ref_name }}
|
||||
--repo ${{ github.event.repository.full_name }}
|
||||
--title ${{ github.ref_name }}
|
||||
--notes "[Changelog](${{ github.event.repository.html_url }}/blob/${{ github.ref_name }}/Changelog.md)"
|
||||
--verify-tag
|
||||
|
||||
deploy:
|
||||
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
|
||||
needs: pack
|
||||
needs: release
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
app:
|
||||
- DiscordChatExporter.Cli
|
||||
- DiscordChatExporter.Gui
|
||||
include:
|
||||
- app: DiscordChatExporter.Cli
|
||||
asset: DiscordChatExporter.Cli
|
||||
- app: DiscordChatExporter.Gui
|
||||
# GUI asset isn't suffixed, unlike the CLI asset
|
||||
asset: DiscordChatExporter
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download artifacts (CLI)
|
||||
uses: actions/download-artifact@v3
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # 3.0.2
|
||||
with:
|
||||
name: DiscordChatExporter.Cli
|
||||
path: DiscordChatExporter.Cli
|
||||
name: ${{ matrix.app }}
|
||||
path: ${{ matrix.app }}/
|
||||
|
||||
- name: Download artifacts (GUI)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: DiscordChatExporter
|
||||
path: DiscordChatExporter.Gui
|
||||
- name: Create package
|
||||
# Change into the artifacts directory to avoid including the directory itself in the zip archive
|
||||
working-directory: ${{ matrix.app }}/
|
||||
run: zip -r ../${{ matrix.asset }}.zip .
|
||||
|
||||
- name: Create package (CLI)
|
||||
shell: pwsh
|
||||
run: >
|
||||
Compress-Archive
|
||||
-Path DiscordChatExporter.Cli/*
|
||||
-DestinationPath DiscordChatExporter.Cli.zip
|
||||
-Force
|
||||
|
||||
- name: Create package (GUI)
|
||||
shell: pwsh
|
||||
run: >
|
||||
Compress-Archive
|
||||
-Path DiscordChatExporter.Gui/*
|
||||
-DestinationPath DiscordChatExporter.zip
|
||||
-Force
|
||||
|
||||
- name: Create release
|
||||
- name: Upload release asset
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: >
|
||||
gh release create "${{ github.ref_name }}"
|
||||
"DiscordChatExporter.Cli.zip"
|
||||
"DiscordChatExporter.zip"
|
||||
--repo "${{ github.event.repository.full_name }}"
|
||||
--title "${{ github.ref_name }}"
|
||||
--notes "[Changelog](${{ github.event.repository.html_url }}/blob/${{ github.ref_name }}/Changelog.md)"
|
||||
--verify-tag
|
||||
gh release upload ${{ github.ref_name }}
|
||||
${{ matrix.asset }}.zip
|
||||
--repo ${{ github.event.repository.full_name }}
|
||||
|
||||
notify:
|
||||
needs: deploy
|
||||
@@ -143,7 +171,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Notify Discord
|
||||
uses: tyrrrz/action-http-request@v1
|
||||
uses: tyrrrz/action-http-request@64c70c67f5ebc54d4c7ea09cbe3553322778afd5 # 1.1.2
|
||||
with:
|
||||
url: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
method: POST
|
||||
@@ -152,5 +180,6 @@ jobs:
|
||||
body: |
|
||||
{
|
||||
"avatar_url": "https://raw.githubusercontent.com/${{ github.event.repository.full_name }}/${{ github.ref_name }}/favicon.png",
|
||||
"content": "**${{ github.event.repository.name }}** new version released!\nVersion: `${{ github.ref_name }}`\nChangelog: <${{ github.event.repository.html_url }}/blob/${{ github.ref_name }}/Changelog.md>"
|
||||
}
|
||||
"content": "**${{ github.event.repository.name }}** v${{ github.ref_name }} has been released!\n🔗 [Download](<${{ github.event.repository.html_url }}/releases/tag/${{ github.ref_name }}>) • [Changelog](<${{ github.event.repository.html_url }}/blob/${{ github.ref_name }}/Changelog.md>)"
|
||||
}
|
||||
retry-count: 5
|
||||
|
||||
+6
-17
@@ -1,23 +1,12 @@
|
||||
# User-specific files
|
||||
.vs/
|
||||
.idea/
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
.idea/
|
||||
.vs/
|
||||
.vscode/
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
[Xx]64/
|
||||
[Xx]86/
|
||||
[Bb]uild/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Coverage
|
||||
*.opencover.xml
|
||||
# Test results
|
||||
TestResults/
|
||||
+53
-1
@@ -1,5 +1,57 @@
|
||||
# Changelog
|
||||
|
||||
## v2.42.5 (23-Nov-2023)
|
||||
|
||||
- General changes:
|
||||
- Fixed an issue where the application crashed when trying to export a channel that contained a message with an invalid timestamp (e.g. `<t:0:x>`).
|
||||
|
||||
## v2.42.4 (16-Nov-2023)
|
||||
|
||||
- General changes:
|
||||
- Switched from .NET 7.0 to .NET 8.0. If running on Windows, the application should update all required prerequisites automatically. Alternatively, you can download the latest version of the runtime for your system [here](https://dotnet.microsoft.com/download/dotnet/8.0).
|
||||
- CLI changes:
|
||||
- Added a message that is displayed when the application is running with the output stream redirected, informing the user that they will not get rich progress reporting in this mode.
|
||||
- GUI changes:
|
||||
- Changed the presentation of the "Reuse assets" and "Assets path" options, such that the corresponding controls are now disabled if the "Download assets" option is not enabled.
|
||||
- HTML changes:
|
||||
- Fixed an issue where exporting a channel with a custom assets path could result in an HTML output that did not render properly in Firefox.
|
||||
|
||||
## v2.42.3 (09-Nov-2023)
|
||||
|
||||
- JSON changes:
|
||||
- Added the `video` field to the embed object, which may contain additional information about an embedded video resource.
|
||||
- CSV changes:
|
||||
- Fixed an issue where the `Date` field was incorrectly formatted. Now, the dates follow the `ISO 8601` standard.
|
||||
|
||||
## v2.42.2 (26-Oct-2023)
|
||||
|
||||
- General changes:
|
||||
- Fixed an issue where trying to export a channel that contained application interactions with empty command names crashed the application.
|
||||
|
||||
## v2.42.1 (19-Oct-2023)
|
||||
|
||||
- General changes:
|
||||
- Improved the error message shown when trying to export a forum channel. Such channels cannot be exported directly, you need to fetch and export their individual threads instead.
|
||||
- CLI changes:
|
||||
- Fixed an issue where fetching threads with `[` or `]` in their name crashed the application.
|
||||
|
||||
## v2.42 (12-Oct-2023)
|
||||
|
||||
- General changes:
|
||||
- Fixed an issue where the "reuse assets" option did not work as intended due to Discord recently introducing signatures in the CDN URLs. (Thanks [@slatinsky](https://github.com/slatinsky))
|
||||
- CLI changes:
|
||||
- Added progress reporting for the "fetching channels" stage of the `exportguild` and `exportall` commands. (Thanks [@slatinsky](https://github.com/slatinsky))
|
||||
|
||||
## v2.41.2 (05-Oct-2023)
|
||||
|
||||
- General changes:
|
||||
- Added a check that will trigger an error if the provided bot account does not have the message content intent enabled. Unlike the previous attempt, this approach does not rely on heuristics.
|
||||
|
||||
## v2.41.1 (28-Sep-2023)
|
||||
|
||||
- CLI changes:
|
||||
- Fixed an issue where the export failed to export channels with an empty name.
|
||||
|
||||
## v2.41 (15-Sep-2023)
|
||||
|
||||
- General changes:
|
||||
@@ -143,7 +195,7 @@
|
||||
|
||||
## v2.36.1 (24-Sep-2022)
|
||||
|
||||
- Added a check which will trigger an error if the provided bot account does not have the message content intent enabled. Note, however, that this check is based on heuristics which may result in false negatives.
|
||||
- Added a check that will trigger an error if the provided bot account does not have the message content intent enabled. Note, however, that this check is based on heuristics which may result in false negatives.
|
||||
- Fixed an issue where certain transient HTTP errors were not retried.
|
||||
- Fixed an issue which caused the export process to fail with the `IndexOutOfRangeException` exception on certain automated messages.
|
||||
- Fixed an issue which caused the export process to fail on unrecognized embed types.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>2.41</Version>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Version>2.42.5</Version>
|
||||
<Company>Tyrrrz</Company>
|
||||
<Copyright>Copyright (c) Oleksii Holub</Copyright>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -11,19 +11,19 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.0.4" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.25.0" PrivateAssets="all" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.11.0" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.2" PrivateAssets="all" />
|
||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
|
||||
<PackageReference Include="ReflectionMagic" Version="5.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0" PrivateAssets="all" />
|
||||
<PackageReference Include="AngleSharp" Version="1.0.7" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.26.2" PrivateAssets="all" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
|
||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="ReflectionMagic" Version="5.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.6.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -105,7 +105,7 @@ public static class ExportWrapper
|
||||
if (message is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Message '{messageId}' does not exist in the export of channel '{channelId}'."
|
||||
$"Message #{messageId} not found in the export of channel #{channelId}."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ public static class ExportWrapper
|
||||
if (message.ValueKind == JsonValueKind.Undefined)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Message '{messageId}' does not exist in the export of channel '{channelId}'."
|
||||
$"Message #{messageId} not found in the export of channel #{channelId}."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using DiscordChatExporter.Cli.Tests.Infra;
|
||||
@@ -27,7 +28,11 @@ public class HtmlAttachmentSpecs
|
||||
.Select(e => e.GetAttribute("href"))
|
||||
.Should()
|
||||
.Contain(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885587844964417596/Test.txt"
|
||||
u =>
|
||||
u.StartsWith(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885587844964417596/Test.txt",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +53,11 @@ public class HtmlAttachmentSpecs
|
||||
.Select(e => e.GetAttribute("src"))
|
||||
.Should()
|
||||
.Contain(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885654862430359613/bird-thumbnail.png"
|
||||
u =>
|
||||
u.StartsWith(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885654862430359613/bird-thumbnail.png",
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,7 +78,7 @@ public class HtmlAttachmentSpecs
|
||||
var videoUrl = message.QuerySelector("video source")?.GetAttribute("src");
|
||||
videoUrl
|
||||
.Should()
|
||||
.Be(
|
||||
.StartWith(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885655761512968233/file_example_MP4_640_3MG.mp4"
|
||||
);
|
||||
}
|
||||
@@ -91,7 +100,7 @@ public class HtmlAttachmentSpecs
|
||||
var audioUrl = message.QuerySelector("audio source")?.GetAttribute("src");
|
||||
audioUrl
|
||||
.Should()
|
||||
.Be(
|
||||
.StartWith(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885656175348187146/file_example_MP3_1MG.mp3"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ public class HtmlEmbedSpecs
|
||||
|
||||
// Assert
|
||||
var iframeUrl = message.QuerySelector("iframe")?.GetAttribute("src");
|
||||
iframeUrl.Should().Be("https://open.spotify.com/embed/track/1LHZMWefF9502NPfArRfvP");
|
||||
iframeUrl.Should().StartWith("https://open.spotify.com/embed/track/1LHZMWefF9502NPfArRfvP");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -161,7 +161,7 @@ public class HtmlEmbedSpecs
|
||||
|
||||
// Assert
|
||||
var iframeUrl = message.QuerySelector("iframe")?.GetAttribute("src");
|
||||
iframeUrl.Should().Be("https://www.youtube.com/embed/qOWW4OlgbvE");
|
||||
iframeUrl.Should().StartWith("https://www.youtube.com/embed/qOWW4OlgbvE");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -19,7 +19,7 @@ public class HtmlStickerSpecs
|
||||
|
||||
// Assert
|
||||
var stickerUrl = message.QuerySelector("[title='rock'] img")?.GetAttribute("src");
|
||||
stickerUrl.Should().Be("https://cdn.discordapp.com/stickers/904215665597120572.png");
|
||||
stickerUrl.Should().StartWith("https://cdn.discordapp.com/stickers/904215665597120572.png");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -35,6 +35,9 @@ public class HtmlStickerSpecs
|
||||
var stickerUrl = message
|
||||
.QuerySelector("[title='Yikes'] [data-source]")
|
||||
?.GetAttribute("data-source");
|
||||
stickerUrl.Should().Be("https://cdn.discordapp.com/stickers/816087132447178774.json");
|
||||
|
||||
stickerUrl
|
||||
.Should()
|
||||
.StartWith("https://cdn.discordapp.com/stickers/816087132447178774.json");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,10 @@ public class JsonAttachmentSpecs
|
||||
.GetProperty("url")
|
||||
.GetString()
|
||||
.Should()
|
||||
.Be(
|
||||
.StartWith(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885587844964417596/Test.txt"
|
||||
);
|
||||
|
||||
attachments[0].GetProperty("fileName").GetString().Should().Be("Test.txt");
|
||||
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(11);
|
||||
}
|
||||
@@ -54,9 +55,10 @@ public class JsonAttachmentSpecs
|
||||
.GetProperty("url")
|
||||
.GetString()
|
||||
.Should()
|
||||
.Be(
|
||||
.StartWith(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885654862430359613/bird-thumbnail.png"
|
||||
);
|
||||
|
||||
attachments[0].GetProperty("fileName").GetString().Should().Be("bird-thumbnail.png");
|
||||
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(466335);
|
||||
}
|
||||
@@ -80,14 +82,16 @@ public class JsonAttachmentSpecs
|
||||
.GetProperty("url")
|
||||
.GetString()
|
||||
.Should()
|
||||
.Be(
|
||||
.StartWith(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885655761512968233/file_example_MP4_640_3MG.mp4"
|
||||
);
|
||||
|
||||
attachments[0]
|
||||
.GetProperty("fileName")
|
||||
.GetString()
|
||||
.Should()
|
||||
.Be("file_example_MP4_640_3MG.mp4");
|
||||
|
||||
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(3114374);
|
||||
}
|
||||
|
||||
@@ -110,9 +114,10 @@ public class JsonAttachmentSpecs
|
||||
.GetProperty("url")
|
||||
.GetString()
|
||||
.Should()
|
||||
.Be(
|
||||
.StartWith(
|
||||
"https://cdn.discordapp.com/attachments/885587741654536192/885656175348187146/file_example_MP3_1MG.mp3"
|
||||
);
|
||||
|
||||
attachments[0].GetProperty("fileName").GetString().Should().Be("file_example_MP3_1MG.mp3");
|
||||
attachments[0].GetProperty("fileSizeBytes").GetInt64().Should().Be(1087849);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class JsonStickerSpecs
|
||||
.GetProperty("sourceUrl")
|
||||
.GetString()
|
||||
.Should()
|
||||
.Be("https://cdn.discordapp.com/stickers/904215665597120572.png");
|
||||
.StartWith("https://cdn.discordapp.com/stickers/904215665597120572.png");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -50,6 +50,6 @@ public class JsonStickerSpecs
|
||||
.GetProperty("sourceUrl")
|
||||
.GetString()
|
||||
.Should()
|
||||
.Be("https://cdn.discordapp.com/stickers/816087132447178774.json");
|
||||
.StartWith("https://cdn.discordapp.com/stickers/816087132447178774.json");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
# -- Build
|
||||
# Specify the platform here so that we pull the SDK image matching the host platform,
|
||||
# instead of the target platform specified during build by the `--platform` option.
|
||||
# Use the .NET 8.0 preview because the `--arch` option is only available in that version.
|
||||
# https://github.com/dotnet/dotnet-docker/issues/4388#issuecomment-1459038566
|
||||
# TODO: switch images to Alpine once .NET 8.0 is released.
|
||||
# Currently, the correct preview version is only available on Debian.
|
||||
# https://github.com/dotnet/dotnet-docker/blob/main/samples/selecting-tags.md
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-preview AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
|
||||
|
||||
# Expose the target architecture set by the `docker build --platform` option, so that
|
||||
# we can build the assembly for the correct platform.
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /build
|
||||
WORKDIR /tmp/app
|
||||
|
||||
COPY favicon.ico .
|
||||
COPY NuGet.config .
|
||||
@@ -22,25 +17,40 @@ COPY DiscordChatExporter.Cli DiscordChatExporter.Cli
|
||||
|
||||
# Publish a self-contained assembly so we can use a slimmer runtime image
|
||||
RUN dotnet publish DiscordChatExporter.Cli \
|
||||
--configuration Release \
|
||||
-p:CSharpier_Bypass=true \
|
||||
--configuration Release \
|
||||
--self-contained \
|
||||
--use-current-runtime \
|
||||
--arch $TARGETARCH \
|
||||
--output publish/
|
||||
--output DiscordChatExporter.Cli/bin/publish/
|
||||
|
||||
# -- Run
|
||||
# Use `runtime-deps` instead of `runtime` because we have a self-contained assembly
|
||||
FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/runtime-deps:7.0 AS run
|
||||
FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine AS run
|
||||
|
||||
# Create a non-root user to run the app, so that the output files can be accessed by the host
|
||||
LABEL org.opencontainers.image.title="DiscordChatExporter.Cli"
|
||||
LABEL org.opencontainers.image.description="DiscordChatExporter is an application that can be used to export message history from any Discord channel to a file."
|
||||
LABEL org.opencontainers.image.authors="tyrrrz.me"
|
||||
LABEL org.opencontainers.image.url="https://github.com/Tyrrrz/DiscordChatExporter"
|
||||
LABEL org.opencontainers.image.source="https://github.com/Tyrrrz/DiscordChatExporter/blob/master/DiscordChatExporter.Cli.dockerfile"
|
||||
LABEL org.opencontainers.image.licenses="MIT"
|
||||
|
||||
# Alpine image doesn't come with the ICU libraries pre-installed, so we need to install them manually.
|
||||
# We need the full ICU data because we allow the user to specify any locale for formatting purposes.
|
||||
RUN apk add --no-cache icu-libs
|
||||
RUN apk add --no-cache icu-data-full
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
|
||||
# Use a non-root user to ensure that the files shared with the host are accessible by the host user.
|
||||
# We can use the default user provided by the base image for this purpose.
|
||||
# https://github.com/Tyrrrz/DiscordChatExporter/issues/851
|
||||
RUN adduser --disabled-password --no-create-home dce
|
||||
USER dce
|
||||
USER $APP_UID
|
||||
|
||||
# This directory is exposed to the user for mounting purposes, so it's important that it always
|
||||
# stays the same for backwards compatibility.
|
||||
WORKDIR /out
|
||||
|
||||
COPY --from=build /build/publish /opt/dce
|
||||
ENTRYPOINT ["/opt/dce/DiscordChatExporter.Cli"]
|
||||
COPY --from=build /tmp/app/DiscordChatExporter.Cli/bin/publish /opt/app
|
||||
ENTRYPOINT ["/opt/app/DiscordChatExporter.Cli"]
|
||||
@@ -37,15 +37,28 @@ public abstract class DiscordCommandBase : ICommand
|
||||
{
|
||||
using (console.WithForegroundColor(ConsoleColor.DarkYellow))
|
||||
{
|
||||
console.Error.WriteLine(
|
||||
"Warning: Option --bot is deprecated and should not be used. "
|
||||
+ "The type of the provided token is now inferred automatically. "
|
||||
+ "Please update your workflows as this option may be completely removed in a future version."
|
||||
);
|
||||
console
|
||||
.Error
|
||||
.WriteLine(
|
||||
"Warning: Option --bot is deprecated and should not be used. "
|
||||
+ "The type of the provided token is now inferred automatically. "
|
||||
+ "Please update your workflows as this option may be completely removed in a future version."
|
||||
);
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
// Note about interactivity
|
||||
if (console.IsOutputRedirected)
|
||||
{
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
"Note: Output streams are redirected, rich console interactions are disabled. "
|
||||
+ "If you are running this command in Docker, consider allocating a pseudo-terminal for better user experience (docker run -it ...)."
|
||||
);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ using DiscordChatExporter.Core.Exceptions;
|
||||
using DiscordChatExporter.Core.Exporting;
|
||||
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||
using DiscordChatExporter.Core.Utils;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using Gress;
|
||||
using Spectre.Console;
|
||||
@@ -163,13 +162,14 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||
|| OutputPath.Contains('%')
|
||||
// Otherwise, require an existing directory or an unambiguous directory path
|
||||
|| Directory.Exists(OutputPath)
|
||||
|| PathEx.IsDirectoryPath(OutputPath);
|
||||
|| Path.EndsInDirectorySeparator(OutputPath);
|
||||
|
||||
if (!isValidOutputPath)
|
||||
{
|
||||
throw new CommandException(
|
||||
"Attempted to export multiple channels, but the output path is neither a directory nor a template. "
|
||||
+ "If the provided output path is meant to be treated as a directory, make sure it ends with a slash."
|
||||
+ "If the provided output path is meant to be treated as a directory, make sure it ends with a slash. "
|
||||
+ $"Provided output path: '{OutputPath}'."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1124
|
||||
ParallelLimit > 1
|
||||
)
|
||||
.StartAsync(async progressContext =>
|
||||
.StartAsync(async ctx =>
|
||||
{
|
||||
await Parallel.ForEachAsync(
|
||||
channels,
|
||||
@@ -199,8 +199,8 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||
{
|
||||
try
|
||||
{
|
||||
await progressContext.StartTaskAsync(
|
||||
channel.GetHierarchicalName(),
|
||||
await ctx.StartTaskAsync(
|
||||
Markup.Escape(channel.GetHierarchicalName()),
|
||||
async progress =>
|
||||
{
|
||||
var guild = await Discord.GetGuildAsync(
|
||||
@@ -244,9 +244,11 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||
// Print the result
|
||||
using (console.WithForegroundColor(ConsoleColor.White))
|
||||
{
|
||||
await console.Output.WriteLineAsync(
|
||||
$"Successfully exported {channels.Count - errorsByChannel.Count} channel(s)."
|
||||
);
|
||||
await console
|
||||
.Output
|
||||
.WriteLineAsync(
|
||||
$"Successfully exported {channels.Count - errorsByChannel.Count} channel(s)."
|
||||
);
|
||||
}
|
||||
|
||||
// Print errors
|
||||
@@ -256,9 +258,11 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||
|
||||
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||
{
|
||||
await console.Error.WriteLineAsync(
|
||||
$"Failed to export {errorsByChannel.Count} channel(s):"
|
||||
);
|
||||
await console
|
||||
.Error
|
||||
.WriteLineAsync(
|
||||
$"Failed to export {errorsByChannel.Count} the following channel(s):"
|
||||
);
|
||||
}
|
||||
|
||||
foreach (var (channel, error) in errorsByChannel)
|
||||
@@ -320,33 +324,51 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||
// Support Ukraine callout
|
||||
if (!IsUkraineSupportMessageDisabled)
|
||||
{
|
||||
console.Output.WriteLine(
|
||||
"┌────────────────────────────────────────────────────────────────────┐"
|
||||
);
|
||||
console.Output.WriteLine(
|
||||
"│ Thank you for supporting Ukraine <3 │"
|
||||
);
|
||||
console.Output.WriteLine(
|
||||
"│ │"
|
||||
);
|
||||
console.Output.WriteLine(
|
||||
"│ As Russia wages a genocidal war against my country, │"
|
||||
);
|
||||
console.Output.WriteLine(
|
||||
"│ I'm grateful to everyone who continues to │"
|
||||
);
|
||||
console.Output.WriteLine(
|
||||
"│ stand with Ukraine in our fight for freedom. │"
|
||||
);
|
||||
console.Output.WriteLine(
|
||||
"│ │"
|
||||
);
|
||||
console.Output.WriteLine(
|
||||
"│ Learn more: https://tyrrrz.me/ukraine │"
|
||||
);
|
||||
console.Output.WriteLine(
|
||||
"└────────────────────────────────────────────────────────────────────┘"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
"┌────────────────────────────────────────────────────────────────────┐"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
"│ Thank you for supporting Ukraine <3 │"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
"│ │"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
"│ As Russia wages a genocidal war against my country, │"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
"│ I'm grateful to everyone who continues to │"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
"│ stand with Ukraine in our fight for freedom. │"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
"│ │"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
"│ Learn more: https://tyrrrz.me/ukraine │"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
"└────────────────────────────────────────────────────────────────────┘"
|
||||
);
|
||||
console.Output.WriteLine("");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Infrastructure;
|
||||
using DiscordChatExporter.Cli.Commands.Base;
|
||||
using DiscordChatExporter.Cli.Commands.Converters;
|
||||
using DiscordChatExporter.Cli.Commands.Shared;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Cli.Utils.Extensions;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Core.Discord.Dump;
|
||||
using DiscordChatExporter.Core.Exceptions;
|
||||
using JsonExtensions.Reading;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
|
||||
@@ -51,39 +51,84 @@ public class ExportAllCommand : ExportCommandBase
|
||||
// Pull from the API
|
||||
if (string.IsNullOrWhiteSpace(DataPackageFilePath))
|
||||
{
|
||||
await console.Output.WriteLineAsync("Fetching channels...");
|
||||
|
||||
await foreach (var guild in Discord.GetUserGuildsAsync(cancellationToken))
|
||||
{
|
||||
// Regular channels
|
||||
await foreach (
|
||||
var channel in Discord.GetGuildChannelsAsync(guild.Id, cancellationToken)
|
||||
)
|
||||
{
|
||||
if (channel.IsCategory)
|
||||
continue;
|
||||
await console
|
||||
.Output
|
||||
.WriteLineAsync($"Fetching channels for guild '{guild.Name}'...");
|
||||
|
||||
if (!IncludeVoiceChannels && channel.IsVoice)
|
||||
continue;
|
||||
var fetchedChannelsCount = 0;
|
||||
await console
|
||||
.CreateStatusTicker()
|
||||
.StartAsync(
|
||||
"...",
|
||||
async ctx =>
|
||||
{
|
||||
await foreach (
|
||||
var channel in Discord.GetGuildChannelsAsync(
|
||||
guild.Id,
|
||||
cancellationToken
|
||||
)
|
||||
)
|
||||
{
|
||||
if (channel.IsCategory)
|
||||
continue;
|
||||
|
||||
channels.Add(channel);
|
||||
}
|
||||
if (!IncludeVoiceChannels && channel.IsVoice)
|
||||
continue;
|
||||
|
||||
channels.Add(channel);
|
||||
|
||||
ctx.Status(
|
||||
Markup.Escape($"Fetched '{channel.GetHierarchicalName()}'.")
|
||||
);
|
||||
|
||||
fetchedChannelsCount++;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await console.Output.WriteLineAsync($"Fetched {fetchedChannelsCount} channel(s).");
|
||||
|
||||
// Threads
|
||||
if (ThreadInclusionMode != ThreadInclusionMode.None)
|
||||
{
|
||||
await foreach (
|
||||
var thread in Discord.GetGuildThreadsAsync(
|
||||
guild.Id,
|
||||
ThreadInclusionMode == ThreadInclusionMode.All,
|
||||
Before,
|
||||
After,
|
||||
cancellationToken
|
||||
)
|
||||
)
|
||||
{
|
||||
channels.Add(thread);
|
||||
}
|
||||
await console
|
||||
.Output
|
||||
.WriteLineAsync($"Fetching threads for guild '{guild.Name}'...");
|
||||
|
||||
var fetchedThreadsCount = 0;
|
||||
await console
|
||||
.CreateStatusTicker()
|
||||
.StartAsync(
|
||||
"...",
|
||||
async ctx =>
|
||||
{
|
||||
await foreach (
|
||||
var thread in Discord.GetGuildThreadsAsync(
|
||||
guild.Id,
|
||||
ThreadInclusionMode == ThreadInclusionMode.All,
|
||||
Before,
|
||||
After,
|
||||
cancellationToken
|
||||
)
|
||||
)
|
||||
{
|
||||
channels.Add(thread);
|
||||
|
||||
ctx.Status(
|
||||
Markup.Escape($"Fetched '{thread.GetHierarchicalName()}'.")
|
||||
);
|
||||
|
||||
fetchedThreadsCount++;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await console
|
||||
.Output
|
||||
.WriteLineAsync($"Fetched {fetchedThreadsCount} thread(s).");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,39 +136,59 @@ public class ExportAllCommand : ExportCommandBase
|
||||
else
|
||||
{
|
||||
await console.Output.WriteLineAsync("Extracting channels...");
|
||||
using var archive = ZipFile.OpenRead(DataPackageFilePath);
|
||||
|
||||
var entry = archive.GetEntry("messages/index.json");
|
||||
if (entry is null)
|
||||
throw new CommandException("Could not find channel index inside the data package.");
|
||||
var dump = await DataDump.LoadAsync(DataPackageFilePath, cancellationToken);
|
||||
var inaccessibleChannels = new List<DataDumpChannel>();
|
||||
|
||||
await using var stream = entry.Open();
|
||||
using var document = await JsonDocument.ParseAsync(stream, default, cancellationToken);
|
||||
await console
|
||||
.CreateStatusTicker()
|
||||
.StartAsync(
|
||||
"...",
|
||||
async ctx =>
|
||||
{
|
||||
foreach (var dumpChannel in dump.Channels)
|
||||
{
|
||||
ctx.Status(
|
||||
Markup.Escape(
|
||||
$"Fetching '{dumpChannel.Name}' ({dumpChannel.Id})..."
|
||||
)
|
||||
);
|
||||
|
||||
foreach (var property in document.RootElement.EnumerateObjectOrEmpty())
|
||||
{
|
||||
var channelId = Snowflake.Parse(property.Name);
|
||||
var channelName = property.Value.GetString();
|
||||
try
|
||||
{
|
||||
var channel = await Discord.GetChannelAsync(
|
||||
dumpChannel.Id,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
// Null items refer to deleted channels
|
||||
if (channelName is null)
|
||||
continue;
|
||||
|
||||
await console.Output.WriteLineAsync(
|
||||
$"Fetching channel '{channelName}' ({channelId})..."
|
||||
channels.Add(channel);
|
||||
}
|
||||
catch (DiscordChatExporterException)
|
||||
{
|
||||
inaccessibleChannels.Add(dumpChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
try
|
||||
await console.Output.WriteLineAsync($"Fetched {channels} channel(s).");
|
||||
|
||||
// Print inaccessible channels
|
||||
if (inaccessibleChannels.Any())
|
||||
{
|
||||
await console.Output.WriteLineAsync();
|
||||
|
||||
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||
{
|
||||
var channel = await Discord.GetChannelAsync(channelId, cancellationToken);
|
||||
channels.Add(channel);
|
||||
}
|
||||
catch (DiscordChatExporterException)
|
||||
{
|
||||
await console.Error.WriteLineAsync(
|
||||
$"Channel '{channelName}' ({channelId}) is inaccessible."
|
||||
);
|
||||
await console
|
||||
.Error
|
||||
.WriteLineAsync("Failed to access the following channel(s):");
|
||||
}
|
||||
|
||||
foreach (var dumpChannel in inaccessibleChannels)
|
||||
await console.Error.WriteLineAsync($"{dumpChannel.Name} ({dumpChannel.Id})");
|
||||
|
||||
await console.Error.WriteLineAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using DiscordChatExporter.Cli.Commands.Base;
|
||||
using DiscordChatExporter.Cli.Commands.Converters;
|
||||
using DiscordChatExporter.Cli.Commands.Shared;
|
||||
using DiscordChatExporter.Cli.Utils.Extensions;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
|
||||
@@ -33,35 +36,69 @@ public class ExportGuildCommand : ExportCommandBase
|
||||
var cancellationToken = console.RegisterCancellationHandler();
|
||||
var channels = new List<Channel>();
|
||||
|
||||
// Regular channels
|
||||
await console.Output.WriteLineAsync("Fetching channels...");
|
||||
|
||||
// Regular channels
|
||||
await foreach (var channel in Discord.GetGuildChannelsAsync(GuildId, cancellationToken))
|
||||
{
|
||||
if (channel.IsCategory)
|
||||
continue;
|
||||
var fetchedChannelsCount = 0;
|
||||
await console
|
||||
.CreateStatusTicker()
|
||||
.StartAsync(
|
||||
"...",
|
||||
async ctx =>
|
||||
{
|
||||
await foreach (
|
||||
var channel in Discord.GetGuildChannelsAsync(GuildId, cancellationToken)
|
||||
)
|
||||
{
|
||||
if (channel.IsCategory)
|
||||
continue;
|
||||
|
||||
if (!IncludeVoiceChannels && channel.IsVoice)
|
||||
continue;
|
||||
if (!IncludeVoiceChannels && channel.IsVoice)
|
||||
continue;
|
||||
|
||||
channels.Add(channel);
|
||||
}
|
||||
channels.Add(channel);
|
||||
|
||||
ctx.Status(Markup.Escape($"Fetched '{channel.GetHierarchicalName()}'."));
|
||||
|
||||
fetchedChannelsCount++;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await console.Output.WriteLineAsync($"Fetched {fetchedChannelsCount} channel(s).");
|
||||
|
||||
// Threads
|
||||
if (ThreadInclusionMode != ThreadInclusionMode.None)
|
||||
{
|
||||
await foreach (
|
||||
var thread in Discord.GetGuildThreadsAsync(
|
||||
GuildId,
|
||||
ThreadInclusionMode == ThreadInclusionMode.All,
|
||||
Before,
|
||||
After,
|
||||
cancellationToken
|
||||
)
|
||||
)
|
||||
{
|
||||
channels.Add(thread);
|
||||
}
|
||||
await console.Output.WriteLineAsync("Fetching threads...");
|
||||
|
||||
var fetchedThreadsCount = 0;
|
||||
await console
|
||||
.CreateStatusTicker()
|
||||
.StartAsync(
|
||||
"...",
|
||||
async ctx =>
|
||||
{
|
||||
await foreach (
|
||||
var thread in Discord.GetGuildThreadsAsync(
|
||||
GuildId,
|
||||
ThreadInclusionMode == ThreadInclusionMode.All,
|
||||
Before,
|
||||
After,
|
||||
cancellationToken
|
||||
)
|
||||
)
|
||||
{
|
||||
channels.Add(thread);
|
||||
|
||||
ctx.Status(Markup.Escape($"Fetched '{thread.GetHierarchicalName()}'."));
|
||||
|
||||
fetchedThreadsCount++;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await console.Output.WriteLineAsync($"Fetched {fetchedThreadsCount} thread(s).");
|
||||
}
|
||||
|
||||
await ExportAsync(console, channels);
|
||||
|
||||
@@ -64,9 +64,9 @@ public class GetChannelsCommand : DiscordCommandBase
|
||||
foreach (var channel in channels)
|
||||
{
|
||||
// Channel ID
|
||||
await console.Output.WriteAsync(
|
||||
channel.Id.ToString().PadRight(channelIdMaxLength, ' ')
|
||||
);
|
||||
await console
|
||||
.Output
|
||||
.WriteAsync(channel.Id.ToString().PadRight(channelIdMaxLength, ' '));
|
||||
|
||||
// Separator
|
||||
using (console.WithForegroundColor(ConsoleColor.DarkGray))
|
||||
@@ -88,9 +88,11 @@ public class GetChannelsCommand : DiscordCommandBase
|
||||
await console.Output.WriteAsync(" * ");
|
||||
|
||||
// Thread ID
|
||||
await console.Output.WriteAsync(
|
||||
channelThread.Id.ToString().PadRight(channelThreadIdMaxLength, ' ')
|
||||
);
|
||||
await console
|
||||
.Output
|
||||
.WriteAsync(
|
||||
channelThread.Id.ToString().PadRight(channelThreadIdMaxLength, ' ')
|
||||
);
|
||||
|
||||
// Separator
|
||||
using (console.WithForegroundColor(ConsoleColor.DarkGray))
|
||||
@@ -106,9 +108,9 @@ public class GetChannelsCommand : DiscordCommandBase
|
||||
|
||||
// Thread status
|
||||
using (console.WithForegroundColor(ConsoleColor.White))
|
||||
await console.Output.WriteLineAsync(
|
||||
channelThread.IsArchived ? "Archived" : "Active"
|
||||
);
|
||||
await console
|
||||
.Output
|
||||
.WriteLineAsync(channelThread.IsArchived ? "Archived" : "Active");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@ public class GetDirectChannelsCommand : DiscordCommandBase
|
||||
foreach (var channel in channels)
|
||||
{
|
||||
// Channel ID
|
||||
await console.Output.WriteAsync(
|
||||
channel.Id.ToString().PadRight(channelIdMaxLength, ' ')
|
||||
);
|
||||
await console
|
||||
.Output
|
||||
.WriteAsync(channel.Id.ToString().PadRight(channelIdMaxLength, ' '));
|
||||
|
||||
// Separator
|
||||
using (console.WithForegroundColor(ConsoleColor.DarkGray))
|
||||
|
||||
@@ -15,18 +15,18 @@ public class GuideCommand : ICommand
|
||||
using (console.WithForegroundColor(ConsoleColor.White))
|
||||
console.Output.WriteLine("To get user token:");
|
||||
|
||||
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(" 1. Open Discord in your web browser and login");
|
||||
console.Output.WriteLine(" 2. Open any server or direct message channel");
|
||||
console.Output.WriteLine(" 3. Press Ctrl+Shift+I to show developer tools");
|
||||
console.Output.WriteLine(" 4. Navigate to the Network tab");
|
||||
console.Output.WriteLine(" 5. Press Ctrl+R to reload");
|
||||
console.Output.WriteLine(" 6. Switch between random channels to trigger network requests");
|
||||
console.Output.WriteLine(
|
||||
" 7. Search for a request containing \"messages?limit=50\" or similar"
|
||||
);
|
||||
console.Output.WriteLine(" 7. Search for a request that starts with \"messages\"");
|
||||
console.Output.WriteLine(" 8. Select the Headers tab on the right");
|
||||
console.Output.WriteLine(" 9. Scroll down to the Request Headers section");
|
||||
console.Output.WriteLine(" 10. Copy the value of the \"authorization\" header");
|
||||
@@ -40,9 +40,11 @@ public class GuideCommand : ICommand
|
||||
console.Output.WriteLine(" 2. Open your application's settings");
|
||||
console.Output.WriteLine(" 3. Navigate to the Bot section on the left");
|
||||
console.Output.WriteLine(" 4. Under Token click Copy");
|
||||
console.Output.WriteLine(
|
||||
" * Your bot needs to have Message Content Intent enabled to read messages"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
" * Your bot needs to have Message Content Intent enabled to read messages"
|
||||
);
|
||||
console.Output.WriteLine();
|
||||
|
||||
// Guild or channel ID
|
||||
@@ -53,20 +55,22 @@ public class GuideCommand : ICommand
|
||||
console.Output.WriteLine(" 2. Open Settings");
|
||||
console.Output.WriteLine(" 3. Go to Advanced section");
|
||||
console.Output.WriteLine(" 4. Enable Developer Mode");
|
||||
console.Output.WriteLine(
|
||||
" 5. Right-click on the desired guild or channel and click Copy Server ID or Copy Channel ID"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine(
|
||||
" 5. Right-click on the desired guild or channel and click Copy Server ID or Copy Channel ID"
|
||||
);
|
||||
console.Output.WriteLine();
|
||||
|
||||
// Docs link
|
||||
using (console.WithForegroundColor(ConsoleColor.White))
|
||||
console.Output.WriteLine(
|
||||
"If you have questions or issues, please refer to the documentation:"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine("If you have questions or issues, please refer to the documentation:");
|
||||
using (console.WithForegroundColor(ConsoleColor.DarkCyan))
|
||||
console.Output.WriteLine(
|
||||
"https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs"
|
||||
);
|
||||
console
|
||||
.Output
|
||||
.WriteLine("https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs");
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliFx" Version="2.3.4" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.25.0" PrivateAssets="all" />
|
||||
<PackageReference Include="CliFx" Version="2.3.5" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.26.2" PrivateAssets="all" />
|
||||
<PackageReference Include="Deorcify" Version="1.0.2" PrivateAssets="all" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.5.1" PrivateAssets="all" />
|
||||
<PackageReference Include="Gress" Version="2.1.1" />
|
||||
|
||||
@@ -17,6 +17,9 @@ internal static class ConsoleExtensions
|
||||
}
|
||||
);
|
||||
|
||||
public static Status CreateStatusTicker(this IConsole console) =>
|
||||
console.CreateAnsiConsole().Status().AutoRefresh(true);
|
||||
|
||||
public static Progress CreateProgressTicker(this IConsole console) =>
|
||||
console
|
||||
.CreateAnsiConsole()
|
||||
@@ -31,14 +34,17 @@ internal static class ConsoleExtensions
|
||||
);
|
||||
|
||||
public static async ValueTask StartTaskAsync(
|
||||
this ProgressContext progressContext,
|
||||
this ProgressContext context,
|
||||
string description,
|
||||
Func<ProgressTask, ValueTask> performOperationAsync
|
||||
)
|
||||
{
|
||||
var progressTask = progressContext.AddTask(
|
||||
// Don't recognize random square brackets as style tags
|
||||
Markup.Escape(description),
|
||||
// Description cannot be empty
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1133
|
||||
var actualDescription = !string.IsNullOrWhiteSpace(description) ? description : "...";
|
||||
|
||||
var progressTask = context.AddTask(
|
||||
actualDescription,
|
||||
new ProgressTaskSettings { MaxValue = 1 }
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Text.Json;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using JsonExtensions.Reading;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
// https://discord.com/developers/docs/resources/application#application-object
|
||||
public partial record Application(Snowflake Id, string Name, ApplicationFlags Flags)
|
||||
{
|
||||
public bool IsMessageContentIntentEnabled =>
|
||||
Flags.HasFlag(ApplicationFlags.GatewayMessageContent)
|
||||
|| Flags.HasFlag(ApplicationFlags.GatewayMessageContentLimited);
|
||||
}
|
||||
|
||||
public partial record Application
|
||||
{
|
||||
public static Application Parse(JsonElement json)
|
||||
{
|
||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var name = json.GetProperty("name").GetNonWhiteSpaceString();
|
||||
|
||||
var flags =
|
||||
json.GetPropertyOrNull("flags")?.GetInt32OrNull()?.Pipe(x => (ApplicationFlags)x)
|
||||
?? ApplicationFlags.None;
|
||||
|
||||
return new Application(id, name, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
// https://discord.com/developers/docs/resources/application#application-object-application-flags
|
||||
[Flags]
|
||||
public enum ApplicationFlags
|
||||
{
|
||||
None = 0,
|
||||
ApplicationAutoModerationRuleCreateBadge = 64,
|
||||
GatewayPresence = 4096,
|
||||
GatewayPresenceLimited = 8192,
|
||||
GatewayGuildMembers = 16384,
|
||||
GatewayGuildMembersLimited = 32768,
|
||||
VerificationPendingGuildLimit = 65536,
|
||||
Embedded = 131072,
|
||||
GatewayMessageContent = 262144,
|
||||
GatewayMessageContentLimited = 524288,
|
||||
ApplicationCommandBadge = 8388608
|
||||
}
|
||||
@@ -62,7 +62,7 @@ public partial record Channel
|
||||
public static Channel Parse(JsonElement json, Channel? parent = null, int? positionHint = null)
|
||||
{
|
||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var kind = (ChannelKind)json.GetProperty("type").GetInt32();
|
||||
var kind = json.GetProperty("type").GetInt32().Pipe(t => (ChannelKind)t);
|
||||
|
||||
var guildId =
|
||||
json.GetPropertyOrNull("guild_id")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using JsonExtensions.Reading;
|
||||
using System.Text.Json;
|
||||
using JsonExtensions.Reading;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@ public partial record SpotifyTrackEmbedProjection
|
||||
private static string? TryParseTrackId(string embedUrl)
|
||||
{
|
||||
// https://open.spotify.com/track/1LHZMWefF9502NPfArRfvP?si=3efac6ce9be04f0a
|
||||
var trackId = Regex.Match(embedUrl, @"spotify\.com/track/(.*?)(?:\?|&|/|$)").Groups[
|
||||
1
|
||||
].Value;
|
||||
var trackId = Regex
|
||||
.Match(embedUrl, @"spotify\.com/track/(.*?)(?:\?|&|/|$)")
|
||||
.Groups[1]
|
||||
.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(trackId))
|
||||
return trackId;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ public record Interaction(Snowflake Id, string Name, User User)
|
||||
public static Interaction Parse(JsonElement json)
|
||||
{
|
||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var name = json.GetProperty("name").GetNonWhiteSpaceString();
|
||||
var name = json.GetProperty("name").GetNonNullString(); // may be empty, but not null
|
||||
var user = json.GetProperty("user").Pipe(User.Parse);
|
||||
|
||||
return new Interaction(id, name, user);
|
||||
|
||||
@@ -38,6 +38,12 @@ public partial record Message(
|
||||
// App interactions are rendered as replies in the Discord client, but they are not actually replies
|
||||
public bool IsReplyLike => IsReply || Interaction is not null;
|
||||
|
||||
public bool IsEmpty =>
|
||||
string.IsNullOrWhiteSpace(Content)
|
||||
&& !Attachments.Any()
|
||||
&& !Embeds.Any()
|
||||
&& !Stickers.Any();
|
||||
|
||||
public IEnumerable<User> GetReferencedUsers()
|
||||
{
|
||||
yield return Author;
|
||||
@@ -93,7 +99,8 @@ public partial record Message
|
||||
if (trailingEmbeds.Any())
|
||||
{
|
||||
// Concatenate all images into one embed
|
||||
var images = embed.Images
|
||||
var images = embed
|
||||
.Images
|
||||
.Concat(trailingEmbeds.SelectMany(e => e.Images))
|
||||
.ToArray();
|
||||
|
||||
@@ -118,14 +125,15 @@ public partial record Message
|
||||
public static Message Parse(JsonElement json)
|
||||
{
|
||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var kind = (MessageKind)json.GetProperty("type").GetInt32();
|
||||
var flags =
|
||||
(MessageFlags?)json.GetPropertyOrNull("flags")?.GetInt32OrNull() ?? MessageFlags.None;
|
||||
var author = json.GetProperty("author").Pipe(User.Parse);
|
||||
var kind = json.GetProperty("type").GetInt32().Pipe(t => (MessageKind)t);
|
||||
|
||||
var flags =
|
||||
json.GetPropertyOrNull("flags")?.GetInt32OrNull()?.Pipe(f => (MessageFlags)f)
|
||||
?? MessageFlags.None;
|
||||
|
||||
var author = json.GetProperty("author").Pipe(User.Parse);
|
||||
var timestamp = json.GetProperty("timestamp").GetDateTimeOffset();
|
||||
var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffsetOrNull();
|
||||
|
||||
var callEndedTimestamp = json.GetPropertyOrNull("call")
|
||||
?.GetPropertyOrNull("ended_timestamp")
|
||||
?.GetDateTimeOffsetOrNull();
|
||||
|
||||
@@ -13,7 +13,7 @@ public record Sticker(Snowflake Id, string Name, StickerFormat Format, string So
|
||||
{
|
||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||
var name = json.GetProperty("name").GetNonNullString();
|
||||
var format = (StickerFormat)json.GetProperty("format_type").GetInt32();
|
||||
var format = json.GetProperty("format_type").GetInt32().Pipe(t => (StickerFormat)t);
|
||||
|
||||
var sourceUrl = ImageCdn.GetStickerUrl(
|
||||
id,
|
||||
|
||||
@@ -33,17 +33,19 @@ public class DiscordClient
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
return await Http.ResponseResiliencePolicy.ExecuteAsync(
|
||||
return await Http.ResponseResiliencePipeline.ExecuteAsync(
|
||||
async innerCancellationToken =>
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
|
||||
|
||||
// Don't validate because the token can have special characters
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/828
|
||||
request.Headers.TryAddWithoutValidation(
|
||||
"Authorization",
|
||||
tokenKind == TokenKind.Bot ? $"Bot {_token}" : _token
|
||||
);
|
||||
request
|
||||
.Headers
|
||||
.TryAddWithoutValidation(
|
||||
"Authorization",
|
||||
tokenKind == TokenKind.Bot ? $"Bot {_token}" : _token
|
||||
);
|
||||
|
||||
var response = await Http.Client.SendAsync(
|
||||
request,
|
||||
@@ -58,11 +60,13 @@ public class DiscordClient
|
||||
// require properly keeping track of Discord's global/per-route/per-resource
|
||||
// rate limits and that's just way too much effort.
|
||||
// https://discord.com/developers/docs/topics/rate-limits
|
||||
var remainingRequestCount = response.Headers
|
||||
var remainingRequestCount = response
|
||||
.Headers
|
||||
.TryGetValue("X-RateLimit-Remaining")
|
||||
?.Pipe(s => int.Parse(s, CultureInfo.InvariantCulture));
|
||||
|
||||
var resetAfterDelay = response.Headers
|
||||
var resetAfterDelay = response
|
||||
.Headers
|
||||
.TryGetValue("X-RateLimit-Reset-After")
|
||||
?.Pipe(s => double.Parse(s, CultureInfo.InvariantCulture))
|
||||
.Pipe(TimeSpan.FromSeconds);
|
||||
@@ -86,10 +90,13 @@ public class DiscordClient
|
||||
);
|
||||
}
|
||||
|
||||
private async ValueTask<TokenKind> GetTokenKindAsync(
|
||||
private async ValueTask<TokenKind> ResolveTokenKindAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (_resolvedTokenKind is not null)
|
||||
return _resolvedTokenKind.Value;
|
||||
|
||||
// Try authenticating as a user
|
||||
using var userResponse = await GetResponseAsync(
|
||||
"users/@me",
|
||||
@@ -98,7 +105,7 @@ public class DiscordClient
|
||||
);
|
||||
|
||||
if (userResponse.StatusCode != HttpStatusCode.Unauthorized)
|
||||
return TokenKind.User;
|
||||
return (_resolvedTokenKind = TokenKind.User).Value;
|
||||
|
||||
// Try authenticating as a bot
|
||||
using var botResponse = await GetResponseAsync(
|
||||
@@ -108,7 +115,7 @@ public class DiscordClient
|
||||
);
|
||||
|
||||
if (botResponse.StatusCode != HttpStatusCode.Unauthorized)
|
||||
return TokenKind.Bot;
|
||||
return (_resolvedTokenKind = TokenKind.Bot).Value;
|
||||
|
||||
throw new DiscordChatExporterException("Authentication token is invalid.", true);
|
||||
}
|
||||
@@ -116,11 +123,12 @@ public class DiscordClient
|
||||
private async ValueTask<HttpResponseMessage> GetResponseAsync(
|
||||
string url,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var tokenKind = _resolvedTokenKind ??= await GetTokenKindAsync(cancellationToken);
|
||||
return await GetResponseAsync(url, tokenKind, cancellationToken);
|
||||
}
|
||||
) =>
|
||||
await GetResponseAsync(
|
||||
url,
|
||||
await ResolveTokenKindAsync(cancellationToken),
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
private async ValueTask<JsonElement> GetJsonResponseAsync(
|
||||
string url,
|
||||
@@ -152,9 +160,9 @@ public class DiscordClient
|
||||
_
|
||||
=> throw new DiscordChatExporterException(
|
||||
$"""
|
||||
Request to '{url}' failed: {response.StatusCode.ToString().ToSpaceSeparatedWords().ToLowerInvariant()}.
|
||||
Response content: {await response.Content.ReadAsStringAsync(cancellationToken)}
|
||||
""",
|
||||
Request to '{url}' failed: {response.StatusCode.ToString().ToSpaceSeparatedWords().ToLowerInvariant()}.
|
||||
Response content: {await response.Content.ReadAsStringAsync(cancellationToken)}
|
||||
""",
|
||||
true
|
||||
)
|
||||
};
|
||||
@@ -174,6 +182,14 @@ public class DiscordClient
|
||||
: null;
|
||||
}
|
||||
|
||||
public async ValueTask<Application> GetApplicationAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var response = await GetJsonResponseAsync("applications/@me", cancellationToken);
|
||||
return Application.Parse(response);
|
||||
}
|
||||
|
||||
public async ValueTask<User?> TryGetUserAsync(
|
||||
Snowflake userId,
|
||||
CancellationToken cancellationToken = default
|
||||
@@ -285,8 +301,6 @@ public class DiscordClient
|
||||
if (guildId == Guild.DirectMessages.Id)
|
||||
yield break;
|
||||
|
||||
var tokenKind = _resolvedTokenKind ??= await GetTokenKindAsync(cancellationToken);
|
||||
|
||||
var channels = (await GetGuildChannelsAsync(guildId, cancellationToken))
|
||||
// Categories cannot have threads
|
||||
.Where(c => !c.IsCategory)
|
||||
@@ -302,7 +316,7 @@ public class DiscordClient
|
||||
.ToArray();
|
||||
|
||||
// User accounts can only fetch threads using the search endpoint
|
||||
if (tokenKind == TokenKind.User)
|
||||
if (await ResolveTokenKindAsync(cancellationToken) == TokenKind.User)
|
||||
{
|
||||
// Active threads
|
||||
foreach (var channel in channels)
|
||||
@@ -592,6 +606,24 @@ public class DiscordClient
|
||||
if (!messages.Any())
|
||||
yield break;
|
||||
|
||||
// If all messages are empty, make sure that it's not because the bot account doesn't
|
||||
// have the Message Content Intent enabled.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1106#issuecomment-1741548959
|
||||
if (
|
||||
messages.All(m => m.IsEmpty)
|
||||
&& await ResolveTokenKindAsync(cancellationToken) == TokenKind.Bot
|
||||
)
|
||||
{
|
||||
var application = await GetApplicationAsync(cancellationToken);
|
||||
if (!application.IsMessageContentIntentEnabled)
|
||||
{
|
||||
throw new DiscordChatExporterException(
|
||||
"Provided bot account does not have the Message Content Intent enabled.",
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
firstMessage ??= message;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JsonExtensions.Reading;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord.Dump;
|
||||
|
||||
public partial class DataDump
|
||||
{
|
||||
public IReadOnlyList<DataDumpChannel> Channels { get; }
|
||||
|
||||
public DataDump(IReadOnlyList<DataDumpChannel> channels) => Channels = channels;
|
||||
}
|
||||
|
||||
public partial class DataDump
|
||||
{
|
||||
public static DataDump Parse(JsonElement json)
|
||||
{
|
||||
var channels = new List<DataDumpChannel>();
|
||||
|
||||
foreach (var property in json.EnumerateObjectOrEmpty())
|
||||
{
|
||||
var channelId = Snowflake.Parse(property.Name);
|
||||
var channelName = property.Value.GetString();
|
||||
|
||||
// Null items refer to deleted channels
|
||||
if (channelName is null)
|
||||
continue;
|
||||
|
||||
var channel = new DataDumpChannel(channelId, channelName);
|
||||
channels.Add(channel);
|
||||
}
|
||||
|
||||
return new DataDump(channels);
|
||||
}
|
||||
|
||||
public static async ValueTask<DataDump> LoadAsync(
|
||||
string zipFilePath,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var archive = ZipFile.OpenRead(zipFilePath);
|
||||
|
||||
var entry = archive.GetEntry("messages/index.json");
|
||||
if (entry is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Could not find the channel index inside the data package."
|
||||
);
|
||||
}
|
||||
|
||||
await using var stream = entry.Open();
|
||||
using var document = await JsonDocument.ParseAsync(stream, default, cancellationToken);
|
||||
|
||||
return Parse(document.RootElement);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace DiscordChatExporter.Core.Discord.Dump;
|
||||
|
||||
public record DataDumpChannel(Snowflake Id, string Name);
|
||||
@@ -22,26 +22,27 @@ public partial record struct Snowflake
|
||||
public static Snowflake FromDate(DateTimeOffset instant) =>
|
||||
new(((ulong)instant.ToUnixTimeMilliseconds() - 1420070400000UL) << 22);
|
||||
|
||||
public static Snowflake? TryParse(string? str, IFormatProvider? formatProvider = null)
|
||||
public static Snowflake? TryParse(string? value, IFormatProvider? formatProvider = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return null;
|
||||
|
||||
// As number
|
||||
if (ulong.TryParse(str, NumberStyles.None, formatProvider, out var value))
|
||||
return new Snowflake(value);
|
||||
if (ulong.TryParse(value, NumberStyles.None, formatProvider, out var number))
|
||||
return new Snowflake(number);
|
||||
|
||||
// As date
|
||||
if (DateTimeOffset.TryParse(str, formatProvider, DateTimeStyles.None, out var instant))
|
||||
if (DateTimeOffset.TryParse(value, formatProvider, DateTimeStyles.None, out var instant))
|
||||
return FromDate(instant);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Snowflake Parse(string str, IFormatProvider? formatProvider) =>
|
||||
TryParse(str, formatProvider) ?? throw new FormatException($"Invalid snowflake '{str}'.");
|
||||
public static Snowflake Parse(string value, IFormatProvider? formatProvider) =>
|
||||
TryParse(value, formatProvider)
|
||||
?? throw new FormatException($"Invalid snowflake '{value}'.");
|
||||
|
||||
public static Snowflake Parse(string str) => Parse(str, null);
|
||||
public static Snowflake Parse(string value) => Parse(value, null);
|
||||
}
|
||||
|
||||
public partial record struct Snowflake : IComparable<Snowflake>, IComparable
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AsyncKeyedLock" Version="6.2.1" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.25.0" PrivateAssets="all" />
|
||||
<PackageReference Include="AsyncKeyedLock" Version="6.2.2" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.26.2" PrivateAssets="all" />
|
||||
<PackageReference Include="Gress" Version="2.1.1" />
|
||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||
<PackageReference Include="Polly" Version="7.2.4" />
|
||||
<PackageReference Include="RazorBlade" Version="0.4.3" />
|
||||
<PackageReference Include="Polly" Version="8.2.0" />
|
||||
<PackageReference Include="RazorBlade" Version="0.4.4" />
|
||||
<PackageReference Include="Superpower" Version="3.0.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.14.0" />
|
||||
<PackageReference Include="YoutubeExplode" Version="6.3.1" />
|
||||
<PackageReference Include="YoutubeExplode" Version="6.3.7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -6,8 +6,12 @@ public class DiscordChatExporterException : Exception
|
||||
{
|
||||
public bool IsFatal { get; }
|
||||
|
||||
public DiscordChatExporterException(string message, bool isFatal = false)
|
||||
: base(message)
|
||||
public DiscordChatExporterException(
|
||||
string message,
|
||||
bool isFatal = false,
|
||||
Exception? innerException = null
|
||||
)
|
||||
: base(message, innerException)
|
||||
{
|
||||
IsFatal = isFatal;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,9 @@ using Gress;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
public class ChannelExporter
|
||||
public class ChannelExporter(DiscordClient discord)
|
||||
{
|
||||
private readonly DiscordClient _discord;
|
||||
|
||||
public ChannelExporter(DiscordClient discord) => _discord = discord;
|
||||
private readonly DiscordClient _discord = discord;
|
||||
|
||||
public async ValueTask ExportChannelAsync(
|
||||
ExportRequest request,
|
||||
@@ -22,17 +20,32 @@ public class ChannelExporter
|
||||
{
|
||||
// Forum channels don't have messages, they are just a list of threads
|
||||
if (request.Channel.Kind == ChannelKind.GuildForum)
|
||||
throw new DiscordChatExporterException("Channel is a forum.");
|
||||
{
|
||||
throw new DiscordChatExporterException(
|
||||
$"Channel '{request.Channel.Name}' (#{request.Channel.Id}) "
|
||||
+ $"of guild '{request.Guild.Name}' (#{request.Guild.Id}) "
|
||||
+ $"is a forum and cannot be exported directly. "
|
||||
+ "You need to pull its threads and export them individually."
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the channel is empty
|
||||
if (request.Channel.IsEmpty)
|
||||
throw new DiscordChatExporterException("Channel does not contain any messages.");
|
||||
{
|
||||
throw new DiscordChatExporterException(
|
||||
$"Channel '{request.Channel.Name}' (#{request.Channel.Id}) "
|
||||
+ $"of guild '{request.Guild.Name}' (#{request.Guild.Id}) "
|
||||
+ $"does not contain any messages."
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the 'after' boundary is valid
|
||||
if (request.After is not null && !request.Channel.MayHaveMessagesAfter(request.After.Value))
|
||||
{
|
||||
throw new DiscordChatExporterException(
|
||||
"Channel does not contain any messages within the specified period."
|
||||
$"Channel '{request.Channel.Name}' (#{request.Channel.Id}) "
|
||||
+ $"of guild '{request.Guild.Name}' (#{request.Guild.Id}) "
|
||||
+ $"does not contain any messages within the specified period."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +56,9 @@ public class ChannelExporter
|
||||
)
|
||||
{
|
||||
throw new DiscordChatExporterException(
|
||||
"Channel does not contain any messages within the specified period."
|
||||
$"Channel '{request.Channel.Name}' (#{request.Channel.Id}) "
|
||||
+ $"of guild '{request.Guild.Name}' (#{request.Guild.Id}) "
|
||||
+ $"does not contain any messages within the specified period."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,20 +78,36 @@ public class ChannelExporter
|
||||
)
|
||||
)
|
||||
{
|
||||
// Resolve members for referenced users
|
||||
foreach (var user in message.GetReferencedUsers())
|
||||
await context.PopulateMemberAsync(user, cancellationToken);
|
||||
try
|
||||
{
|
||||
// Resolve members for referenced users
|
||||
foreach (var user in message.GetReferencedUsers())
|
||||
await context.PopulateMemberAsync(user, cancellationToken);
|
||||
|
||||
// Export the message
|
||||
if (request.MessageFilter.IsMatch(message))
|
||||
await messageExporter.ExportMessageAsync(message, cancellationToken);
|
||||
// Export the message
|
||||
if (request.MessageFilter.IsMatch(message))
|
||||
await messageExporter.ExportMessageAsync(message, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Provide more context to the exception, to simplify debugging based on error messages
|
||||
throw new DiscordChatExporterException(
|
||||
$"Failed to export message #{message.Id} "
|
||||
+ $"in channel '{request.Channel.Name}' (#{request.Channel.Id}) "
|
||||
+ $"of guild '{request.Guild.Name} (#{request.Guild.Id})'.",
|
||||
ex is not DiscordChatExporterException dex || dex.IsFatal,
|
||||
ex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Throw if no messages were exported
|
||||
if (messageExporter.MessagesExported <= 0)
|
||||
{
|
||||
throw new DiscordChatExporterException(
|
||||
"Channel does not contain any matching messages within the specified period."
|
||||
$"Channel '{request.Channel.Name}' (#{request.Channel.Id}) "
|
||||
+ $"of guild '{request.Guild.Name}' (#{request.Guild.Id}) "
|
||||
+ $"does not contain any matching messages within the specified period."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,10 @@ using DiscordChatExporter.Core.Utils.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
internal partial class CsvMessageWriter : MessageWriter
|
||||
internal partial class CsvMessageWriter(Stream stream, ExportContext context)
|
||||
: MessageWriter(stream, context)
|
||||
{
|
||||
private readonly TextWriter _writer;
|
||||
|
||||
public CsvMessageWriter(Stream stream, ExportContext context)
|
||||
: base(stream, context)
|
||||
{
|
||||
_writer = new StreamWriter(stream);
|
||||
}
|
||||
private readonly TextWriter _writer = new StreamWriter(stream);
|
||||
|
||||
private async ValueTask<string> FormatMarkdownAsync(
|
||||
string markdown,
|
||||
@@ -88,7 +83,7 @@ internal partial class CsvMessageWriter : MessageWriter
|
||||
await _writer.WriteAsync(',');
|
||||
|
||||
// Message timestamp
|
||||
await _writer.WriteAsync(CsvEncode(Context.FormatDate(message.Timestamp)));
|
||||
await _writer.WriteAsync(CsvEncode(Context.FormatDate(message.Timestamp, "o")));
|
||||
await _writer.WriteAsync(',');
|
||||
|
||||
// Message content
|
||||
|
||||
@@ -7,13 +7,14 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using AsyncKeyedLock;
|
||||
using DiscordChatExporter.Core.Utils;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
internal partial class ExportAssetDownloader
|
||||
internal partial class ExportAssetDownloader(string workingDirPath, bool reuse)
|
||||
{
|
||||
private static readonly AsyncKeyedLocker<string> Locker =
|
||||
new(o =>
|
||||
@@ -22,18 +23,12 @@ internal partial class ExportAssetDownloader
|
||||
o.PoolInitialFill = 1;
|
||||
});
|
||||
|
||||
private readonly string _workingDirPath;
|
||||
private readonly bool _reuse;
|
||||
private readonly string _workingDirPath = workingDirPath;
|
||||
private readonly bool _reuse = reuse;
|
||||
|
||||
// File paths of the previously downloaded assets
|
||||
private readonly Dictionary<string, string> _previousPathsByUrl = new(StringComparer.Ordinal);
|
||||
|
||||
public ExportAssetDownloader(string workingDirPath, bool reuse)
|
||||
{
|
||||
_workingDirPath = workingDirPath;
|
||||
_reuse = reuse;
|
||||
}
|
||||
|
||||
public async ValueTask<string> DownloadAsync(
|
||||
string url,
|
||||
CancellationToken cancellationToken = default
|
||||
@@ -53,44 +48,49 @@ internal partial class ExportAssetDownloader
|
||||
|
||||
Directory.CreateDirectory(_workingDirPath);
|
||||
|
||||
await Http.ResiliencePolicy.ExecuteAsync(async () =>
|
||||
{
|
||||
// Download the file
|
||||
using var response = await Http.Client.GetAsync(url, cancellationToken);
|
||||
await using (var output = File.Create(filePath))
|
||||
await response.Content.CopyToAsync(output, cancellationToken);
|
||||
|
||||
// Try to set the file date according to the last-modified header
|
||||
try
|
||||
await Http.ResiliencePipeline.ExecuteAsync(
|
||||
async innerCancellationToken =>
|
||||
{
|
||||
var lastModified = response.Content.Headers
|
||||
.TryGetValue("Last-Modified")
|
||||
?.Pipe(
|
||||
s =>
|
||||
DateTimeOffset.TryParse(
|
||||
s,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out var instant
|
||||
)
|
||||
? instant
|
||||
: (DateTimeOffset?)null
|
||||
);
|
||||
// Download the file
|
||||
using var response = await Http.Client.GetAsync(url, innerCancellationToken);
|
||||
await using (var output = File.Create(filePath))
|
||||
await response.Content.CopyToAsync(output, innerCancellationToken);
|
||||
|
||||
if (lastModified is not null)
|
||||
// Try to set the file date according to the last-modified header
|
||||
try
|
||||
{
|
||||
File.SetCreationTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
||||
File.SetLastWriteTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
||||
File.SetLastAccessTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
||||
var lastModified = response
|
||||
.Content
|
||||
.Headers
|
||||
.TryGetValue("Last-Modified")
|
||||
?.Pipe(
|
||||
s =>
|
||||
DateTimeOffset.TryParse(
|
||||
s,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out var instant
|
||||
)
|
||||
? instant
|
||||
: (DateTimeOffset?)null
|
||||
);
|
||||
|
||||
if (lastModified is not null)
|
||||
{
|
||||
File.SetCreationTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
||||
File.SetLastWriteTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
||||
File.SetLastAccessTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// This can apparently fail for some reason.
|
||||
// Updating the file date is not a critical task, so we'll just ignore exceptions thrown here.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/585
|
||||
}
|
||||
});
|
||||
catch
|
||||
{
|
||||
// This can apparently fail for some reason.
|
||||
// Updating the file date is not a critical task, so we'll just ignore exceptions thrown here.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/585
|
||||
}
|
||||
},
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
return _previousPathsByUrl[url] = filePath;
|
||||
}
|
||||
@@ -98,12 +98,29 @@ internal partial class ExportAssetDownloader
|
||||
|
||||
internal partial class ExportAssetDownloader
|
||||
{
|
||||
private static string GetUrlHash(string url) =>
|
||||
SHA256
|
||||
.HashData(Encoding.UTF8.GetBytes(url))
|
||||
private static string GetUrlHash(string url)
|
||||
{
|
||||
// Remove signature parameters from Discord CDN URLs to normalize them
|
||||
static string NormalizeUrl(string url)
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
if (!string.Equals(uri.Host, "cdn.discordapp.com", StringComparison.OrdinalIgnoreCase))
|
||||
return url;
|
||||
|
||||
var query = HttpUtility.ParseQueryString(uri.Query);
|
||||
query.Remove("ex");
|
||||
query.Remove("is");
|
||||
query.Remove("hm");
|
||||
|
||||
return uri.GetLeftPart(UriPartial.Path) + query;
|
||||
}
|
||||
|
||||
return SHA256
|
||||
.HashData(Encoding.UTF8.GetBytes(NormalizeUrl(url)))
|
||||
.ToHex()
|
||||
// 5 chars ought to be enough for anybody
|
||||
.Truncate(5);
|
||||
}
|
||||
|
||||
private static string GetFileNameFromUrl(string url)
|
||||
{
|
||||
|
||||
@@ -12,27 +12,18 @@ using DiscordChatExporter.Core.Utils.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
internal class ExportContext
|
||||
internal class ExportContext(DiscordClient discord, ExportRequest request)
|
||||
{
|
||||
private readonly Dictionary<Snowflake, Member?> _membersById = new();
|
||||
private readonly Dictionary<Snowflake, Channel> _channelsById = new();
|
||||
private readonly Dictionary<Snowflake, Role> _rolesById = new();
|
||||
private readonly ExportAssetDownloader _assetDownloader;
|
||||
|
||||
public DiscordClient Discord { get; }
|
||||
private readonly ExportAssetDownloader _assetDownloader =
|
||||
new(request.AssetsDirPath, request.ShouldReuseAssets);
|
||||
|
||||
public ExportRequest Request { get; }
|
||||
public DiscordClient Discord { get; } = discord;
|
||||
|
||||
public ExportContext(DiscordClient discord, ExportRequest request)
|
||||
{
|
||||
Discord = discord;
|
||||
Request = request;
|
||||
|
||||
_assetDownloader = new ExportAssetDownloader(
|
||||
request.AssetsDirPath,
|
||||
request.ShouldReuseAssets
|
||||
);
|
||||
}
|
||||
public ExportRequest Request { get; } = request;
|
||||
|
||||
public DateTimeOffset NormalizeDate(DateTimeOffset instant) =>
|
||||
Request.IsUtcNormalizationEnabled ? instant.ToUniversalTime() : instant.ToLocalTime();
|
||||
@@ -101,7 +92,8 @@ internal class ExportContext
|
||||
public Role? TryGetRole(Snowflake id) => _rolesById.GetValueOrDefault(id);
|
||||
|
||||
public IReadOnlyList<Role> GetUserRoles(Snowflake id) =>
|
||||
TryGetMember(id)?.RoleIds
|
||||
TryGetMember(id)
|
||||
?.RoleIds
|
||||
.Select(TryGetRole)
|
||||
.WhereNotNull()
|
||||
.OrderByDescending(r => r.Position)
|
||||
@@ -123,9 +115,9 @@ internal class ExportContext
|
||||
var filePath = await _assetDownloader.DownloadAsync(url, cancellationToken);
|
||||
var relativeFilePath = Path.GetRelativePath(Request.OutputDirPath, filePath);
|
||||
|
||||
// Prefer relative paths so that the output files can be copied around without breaking references.
|
||||
// If the asset directory is outside of the export directory, use an absolute path instead.
|
||||
var optimalFilePath =
|
||||
// Prefer the relative path so that the export package can be copied around without breaking references.
|
||||
// However, if the assets directory lies outside of the export directory, use the absolute path instead.
|
||||
var shouldUseAbsoluteFilePath =
|
||||
relativeFilePath.StartsWith(
|
||||
".." + Path.DirectorySeparatorChar,
|
||||
StringComparison.Ordinal
|
||||
@@ -133,15 +125,19 @@ internal class ExportContext
|
||||
|| relativeFilePath.StartsWith(
|
||||
".." + Path.AltDirectorySeparatorChar,
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
? filePath
|
||||
: relativeFilePath;
|
||||
);
|
||||
|
||||
var optimalFilePath = shouldUseAbsoluteFilePath ? filePath : relativeFilePath;
|
||||
|
||||
// For HTML, the path needs to be properly formatted
|
||||
if (Request.Format is ExportFormat.HtmlDark or ExportFormat.HtmlLight)
|
||||
{
|
||||
// Create a 'file:///' URI and then strip the 'file:///' prefix to allow for relative paths
|
||||
return new Uri(new Uri("file:///"), optimalFilePath).ToString()[8..];
|
||||
// Format the path into a valid file URI
|
||||
var href = new Uri(new Uri("file:///"), optimalFilePath).ToString();
|
||||
|
||||
// File schema does not support relative paths, so strip it if that's the case
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1155
|
||||
return shouldUseAbsoluteFilePath ? href : href[8..];
|
||||
}
|
||||
|
||||
return optimalFilePath;
|
||||
|
||||
@@ -182,10 +182,9 @@ public partial class ExportRequest
|
||||
=> before?.ToDate().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
|
||||
?? "",
|
||||
"%d"
|
||||
=> DateTimeOffset.Now.ToString(
|
||||
"yyyy-MM-dd",
|
||||
CultureInfo.InvariantCulture
|
||||
),
|
||||
=> DateTimeOffset
|
||||
.Now
|
||||
.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
|
||||
"%%" => "%",
|
||||
_ => m.Value
|
||||
|
||||
@@ -26,12 +26,14 @@ internal class ContainsMessageFilter : MessageFilter
|
||||
|
||||
public override bool IsMatch(Message message) =>
|
||||
IsMatch(message.Content)
|
||||
|| message.Embeds.Any(
|
||||
e =>
|
||||
IsMatch(e.Title)
|
||||
|| IsMatch(e.Author?.Name)
|
||||
|| IsMatch(e.Description)
|
||||
|| IsMatch(e.Footer?.Text)
|
||||
|| e.Fields.Any(f => IsMatch(f.Name) || IsMatch(f.Value))
|
||||
);
|
||||
|| message
|
||||
.Embeds
|
||||
.Any(
|
||||
e =>
|
||||
IsMatch(e.Title)
|
||||
|| IsMatch(e.Author?.Name)
|
||||
|| IsMatch(e.Description)
|
||||
|| IsMatch(e.Footer?.Text)
|
||||
|| e.Fields.Any(f => IsMatch(f.Name) || IsMatch(f.Value))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,13 @@ internal class MentionsMessageFilter : MessageFilter
|
||||
public MentionsMessageFilter(string value) => _value = value;
|
||||
|
||||
public override bool IsMatch(Message message) =>
|
||||
message.MentionedUsers.Any(
|
||||
user =>
|
||||
string.Equals(_value, user.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(_value, user.DisplayName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(_value, user.FullName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(_value, user.Id.ToString(), StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
message
|
||||
.MentionedUsers
|
||||
.Any(
|
||||
user =>
|
||||
string.Equals(_value, user.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(_value, user.DisplayName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(_value, user.FullName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(_value, user.Id.ToString(), StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,16 @@ internal class ReactionMessageFilter : MessageFilter
|
||||
public ReactionMessageFilter(string value) => _value = value;
|
||||
|
||||
public override bool IsMatch(Message message) =>
|
||||
message.Reactions.Any(
|
||||
r =>
|
||||
string.Equals(_value, r.Emoji.Id?.ToString(), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(_value, r.Emoji.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(_value, r.Emoji.Code, StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
message
|
||||
.Reactions
|
||||
.Any(
|
||||
r =>
|
||||
string.Equals(
|
||||
_value,
|
||||
r.Emoji.Id?.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
|| string.Equals(_value, r.Emoji.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(_value, r.Emoji.Code, StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,18 +12,15 @@ using DiscordChatExporter.Core.Utils.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||
internal partial class HtmlMarkdownVisitor(
|
||||
ExportContext context,
|
||||
StringBuilder buffer,
|
||||
bool isJumbo
|
||||
) : MarkdownVisitor
|
||||
{
|
||||
private readonly ExportContext _context;
|
||||
private readonly StringBuilder _buffer;
|
||||
private readonly bool _isJumbo;
|
||||
|
||||
public HtmlMarkdownVisitor(ExportContext context, StringBuilder buffer, bool isJumbo)
|
||||
{
|
||||
_context = context;
|
||||
_buffer = buffer;
|
||||
_isJumbo = isJumbo;
|
||||
}
|
||||
private readonly ExportContext _context = context;
|
||||
private readonly StringBuilder _buffer = buffer;
|
||||
private readonly bool _isJumbo = isJumbo;
|
||||
|
||||
protected override ValueTask VisitTextAsync(
|
||||
TextNode text,
|
||||
@@ -196,7 +193,8 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||
// Try to extract the message ID if the link points to a Discord message
|
||||
var linkedMessageId = Regex
|
||||
.Match(link.Url, @"^https?://(?:discord|discordapp)\.com/channels/.*?/(\d+)/?$")
|
||||
.Groups[1].Value;
|
||||
.Groups[1]
|
||||
.Value;
|
||||
|
||||
_buffer.Append(
|
||||
!string.IsNullOrWhiteSpace(linkedMessageId)
|
||||
|
||||
@@ -9,21 +9,15 @@ using WebMarkupMin.Core;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
internal class HtmlMessageWriter : MessageWriter
|
||||
internal class HtmlMessageWriter(Stream stream, ExportContext context, string themeName)
|
||||
: MessageWriter(stream, context)
|
||||
{
|
||||
private readonly TextWriter _writer;
|
||||
private readonly string _themeName;
|
||||
private readonly TextWriter _writer = new StreamWriter(stream);
|
||||
private readonly string _themeName = themeName;
|
||||
|
||||
private readonly HtmlMinifier _minifier = new();
|
||||
private readonly List<Message> _messageGroup = new();
|
||||
|
||||
public HtmlMessageWriter(Stream stream, ExportContext context, string themeName)
|
||||
: base(stream, context)
|
||||
{
|
||||
_writer = new StreamWriter(stream);
|
||||
_themeName = themeName;
|
||||
}
|
||||
|
||||
private bool CanJoinGroup(Message message)
|
||||
{
|
||||
// If the group is empty, any message can join it
|
||||
|
||||
@@ -12,26 +12,21 @@ using JsonExtensions.Writing;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
internal class JsonMessageWriter : MessageWriter
|
||||
internal class JsonMessageWriter(Stream stream, ExportContext context)
|
||||
: MessageWriter(stream, context)
|
||||
{
|
||||
private readonly Utf8JsonWriter _writer;
|
||||
|
||||
public JsonMessageWriter(Stream stream, ExportContext context)
|
||||
: base(stream, context)
|
||||
{
|
||||
_writer = new Utf8JsonWriter(
|
||||
stream,
|
||||
new JsonWriterOptions
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/450
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
Indented = true,
|
||||
// Validation errors may mask actual failures
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/413
|
||||
SkipValidation = true
|
||||
}
|
||||
);
|
||||
}
|
||||
private readonly Utf8JsonWriter _writer = new Utf8JsonWriter(
|
||||
stream,
|
||||
new JsonWriterOptions
|
||||
{
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/450
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
Indented = true,
|
||||
// Validation errors may mask actual failures
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/413
|
||||
SkipValidation = true
|
||||
}
|
||||
);
|
||||
|
||||
private async ValueTask<string> FormatMarkdownAsync(
|
||||
string markdown,
|
||||
@@ -143,6 +138,31 @@ internal class JsonMessageWriter : MessageWriter
|
||||
await _writer.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async ValueTask WriteEmbedVideoAsync(
|
||||
EmbedVideo embedVideo,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(embedVideo.Url))
|
||||
{
|
||||
_writer.WriteString(
|
||||
"url",
|
||||
await Context.ResolveAssetUrlAsync(
|
||||
embedVideo.ProxyUrl ?? embedVideo.Url,
|
||||
cancellationToken
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_writer.WriteNumber("width", embedVideo.Width);
|
||||
_writer.WriteNumber("height", embedVideo.Height);
|
||||
|
||||
_writer.WriteEndObject();
|
||||
await _writer.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async ValueTask WriteEmbedFooterAsync(
|
||||
EmbedFooter embedFooter,
|
||||
CancellationToken cancellationToken = default
|
||||
@@ -224,6 +244,12 @@ internal class JsonMessageWriter : MessageWriter
|
||||
await WriteEmbedImageAsync(embed.Image, cancellationToken);
|
||||
}
|
||||
|
||||
if (embed.Video is not null)
|
||||
{
|
||||
_writer.WritePropertyName("video");
|
||||
await WriteEmbedVideoAsync(embed.Video, cancellationToken);
|
||||
}
|
||||
|
||||
if (embed.Footer is not null)
|
||||
{
|
||||
_writer.WritePropertyName("footer");
|
||||
@@ -419,12 +445,14 @@ internal class JsonMessageWriter : MessageWriter
|
||||
|
||||
_writer.WriteStartArray("users");
|
||||
await foreach (
|
||||
var user in Context.Discord.GetMessageReactionsAsync(
|
||||
Context.Request.Channel.Id,
|
||||
message.Id,
|
||||
reaction.Emoji,
|
||||
cancellationToken
|
||||
)
|
||||
var user in Context
|
||||
.Discord
|
||||
.GetMessageReactionsAsync(
|
||||
Context.Request.Channel.Id,
|
||||
message.Id,
|
||||
reaction.Emoji,
|
||||
cancellationToken
|
||||
)
|
||||
)
|
||||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
@@ -6,20 +6,15 @@ using DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
internal partial class MessageExporter : IAsyncDisposable
|
||||
internal partial class MessageExporter(ExportContext context) : IAsyncDisposable
|
||||
{
|
||||
private readonly ExportContext _context;
|
||||
private readonly ExportContext _context = context;
|
||||
|
||||
private int _partitionIndex;
|
||||
private MessageWriter? _writer;
|
||||
|
||||
public long MessagesExported { get; private set; }
|
||||
|
||||
public MessageExporter(ExportContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private async ValueTask ResetWriterAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_writer is not null)
|
||||
@@ -44,10 +39,10 @@ internal partial class MessageExporter : IAsyncDisposable
|
||||
// Ensure that the partition limit has not been reached
|
||||
if (
|
||||
_writer is not null
|
||||
&& _context.Request.PartitionLimit.IsReached(
|
||||
_writer.MessagesWritten,
|
||||
_writer.BytesWritten
|
||||
)
|
||||
&& _context
|
||||
.Request
|
||||
.PartitionLimit
|
||||
.IsReached(_writer.MessagesWritten, _writer.BytesWritten)
|
||||
)
|
||||
{
|
||||
await ResetWriterAsync(cancellationToken);
|
||||
|
||||
@@ -6,22 +6,16 @@ using DiscordChatExporter.Core.Discord.Data;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
internal abstract class MessageWriter : IAsyncDisposable
|
||||
internal abstract class MessageWriter(Stream stream, ExportContext context) : IAsyncDisposable
|
||||
{
|
||||
protected Stream Stream { get; }
|
||||
protected Stream Stream { get; } = stream;
|
||||
|
||||
protected ExportContext Context { get; }
|
||||
protected ExportContext Context { get; } = context;
|
||||
|
||||
public long MessagesWritten { get; private set; }
|
||||
|
||||
public long BytesWritten => Stream.Length;
|
||||
|
||||
protected MessageWriter(Stream stream, ExportContext context)
|
||||
{
|
||||
Stream = stream;
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public virtual ValueTask WritePreambleAsync(CancellationToken cancellationToken = default) =>
|
||||
default;
|
||||
|
||||
|
||||
@@ -8,16 +8,11 @@ using DiscordChatExporter.Core.Utils.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
||||
internal partial class PlainTextMarkdownVisitor(ExportContext context, StringBuilder buffer)
|
||||
: MarkdownVisitor
|
||||
{
|
||||
private readonly ExportContext _context;
|
||||
private readonly StringBuilder _buffer;
|
||||
|
||||
public PlainTextMarkdownVisitor(ExportContext context, StringBuilder buffer)
|
||||
{
|
||||
_context = context;
|
||||
_buffer = buffer;
|
||||
}
|
||||
private readonly ExportContext _context = context;
|
||||
private readonly StringBuilder _buffer = buffer;
|
||||
|
||||
protected override ValueTask VisitTextAsync(
|
||||
TextNode text,
|
||||
|
||||
@@ -8,15 +8,10 @@ using DiscordChatExporter.Core.Discord.Data.Embeds;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
internal class PlainTextMessageWriter : MessageWriter
|
||||
internal class PlainTextMessageWriter(Stream stream, ExportContext context)
|
||||
: MessageWriter(stream, context)
|
||||
{
|
||||
private readonly TextWriter _writer;
|
||||
|
||||
public PlainTextMessageWriter(Stream stream, ExportContext context)
|
||||
: base(stream, context)
|
||||
{
|
||||
_writer = new StreamWriter(stream);
|
||||
}
|
||||
private readonly TextWriter _writer = new StreamWriter(stream);
|
||||
|
||||
private async ValueTask<string> FormatMarkdownAsync(
|
||||
string markdown,
|
||||
|
||||
@@ -30,7 +30,8 @@ internal class AggregateMatcher<T> : IMatcher<T>
|
||||
|
||||
// If this match is earlier than previous earliest - replace
|
||||
if (
|
||||
earliestMatch is null || match.Segment.StartIndex < earliestMatch.Segment.StartIndex
|
||||
earliestMatch is null
|
||||
|| match.Segment.StartIndex < earliestMatch.Segment.StartIndex
|
||||
)
|
||||
earliestMatch = match;
|
||||
|
||||
|
||||
@@ -340,13 +340,27 @@ internal static partial class MarkdownParser
|
||||
)
|
||||
);
|
||||
|
||||
// https://discord.com/developers/docs/reference#message-formatting-timestamp-styles
|
||||
var format = m.Groups[2].Value.NullIfWhiteSpace() switch
|
||||
{
|
||||
// Ignore the 'relative' format because it doesn't make sense in a static export
|
||||
// Known formats
|
||||
"t" => "t",
|
||||
"T" => "T",
|
||||
"d" => "d",
|
||||
"D" => "D",
|
||||
"f" => "f",
|
||||
"F" => "F",
|
||||
// Relative format: ignore because it doesn't make sense in a static export
|
||||
"r" => null,
|
||||
"R" => null,
|
||||
// Discord's date formats are (mostly) compatible with .NET's date formats
|
||||
var f => f
|
||||
// Unspecified format: will be mapped to the default format
|
||||
null => null,
|
||||
// Unknown format: throw an exception to consider this timestamp invalid
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1156
|
||||
var f
|
||||
=> throw new InvalidOperationException(
|
||||
$"Unknown timestamp format '{f}'."
|
||||
)
|
||||
};
|
||||
|
||||
return new TimestampNode(instant, format);
|
||||
@@ -354,7 +368,12 @@ internal static partial class MarkdownParser
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/681
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/766
|
||||
catch (Exception ex)
|
||||
when (ex is FormatException or ArgumentOutOfRangeException or OverflowException)
|
||||
when (ex
|
||||
is FormatException
|
||||
or ArgumentOutOfRangeException
|
||||
or OverflowException
|
||||
or InvalidOperationException
|
||||
)
|
||||
{
|
||||
// For invalid timestamps, Discord renders "Invalid Date" instead of ignoring the markdown
|
||||
return TimestampNode.Invalid;
|
||||
|
||||
@@ -24,12 +24,10 @@ internal class StringMatcher<T> : IMatcher<T>
|
||||
|
||||
public ParsedMatch<T>? TryMatch(StringSegment segment)
|
||||
{
|
||||
var index = segment.Source.IndexOf(
|
||||
_needle,
|
||||
segment.StartIndex,
|
||||
segment.Length,
|
||||
_comparison
|
||||
);
|
||||
var index = segment
|
||||
.Source
|
||||
.IndexOf(_needle, segment.StartIndex, segment.Length, _comparison);
|
||||
|
||||
if (index < 0)
|
||||
return null;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Security.Authentication;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace DiscordChatExporter.Core.Utils;
|
||||
|
||||
@@ -31,29 +32,45 @@ public static class Http
|
||||
&& IsRetryableStatusCode(hrex.StatusCode ?? HttpStatusCode.OK)
|
||||
);
|
||||
|
||||
public static IAsyncPolicy ResiliencePolicy { get; } =
|
||||
Policy
|
||||
.Handle<Exception>(IsRetryableException)
|
||||
.WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1));
|
||||
|
||||
public static IAsyncPolicy<HttpResponseMessage> ResponseResiliencePolicy { get; } =
|
||||
Policy
|
||||
.Handle<Exception>(IsRetryableException)
|
||||
.OrResult<HttpResponseMessage>(m => IsRetryableStatusCode(m.StatusCode))
|
||||
.WaitAndRetryAsync(
|
||||
8,
|
||||
(i, result, _) =>
|
||||
public static ResiliencePipeline ResiliencePipeline { get; } =
|
||||
new ResiliencePipelineBuilder()
|
||||
.AddRetry(
|
||||
new RetryStrategyOptions
|
||||
{
|
||||
// If rate-limited, use retry-after header as the guide.
|
||||
// The response can be null here if an exception was thrown.
|
||||
if (result.Result?.Headers.RetryAfter?.Delta is { } retryAfter)
|
||||
{
|
||||
// Add some buffer just in case
|
||||
return retryAfter + TimeSpan.FromSeconds(1);
|
||||
}
|
||||
ShouldHandle = new PredicateBuilder().Handle<Exception>(IsRetryableException),
|
||||
MaxRetryAttempts = 4,
|
||||
BackoffType = DelayBackoffType.Exponential,
|
||||
Delay = TimeSpan.FromSeconds(1)
|
||||
}
|
||||
)
|
||||
.Build();
|
||||
|
||||
return TimeSpan.FromSeconds(Math.Pow(2, i) + 1);
|
||||
},
|
||||
(_, _, _, _) => Task.CompletedTask
|
||||
);
|
||||
public static ResiliencePipeline<HttpResponseMessage> ResponseResiliencePipeline { get; } =
|
||||
new ResiliencePipelineBuilder<HttpResponseMessage>()
|
||||
.AddRetry(
|
||||
new RetryStrategyOptions<HttpResponseMessage>
|
||||
{
|
||||
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
|
||||
.Handle<Exception>(IsRetryableException)
|
||||
.HandleResult(m => IsRetryableStatusCode(m.StatusCode)),
|
||||
MaxRetryAttempts = 8,
|
||||
DelayGenerator = args =>
|
||||
{
|
||||
// If rate-limited, use retry-after header as the guide.
|
||||
// The response can be null here if an exception was thrown.
|
||||
if (args.Outcome.Result?.Headers.RetryAfter?.Delta is { } retryAfter)
|
||||
{
|
||||
// Add some buffer just in case
|
||||
return ValueTask.FromResult<TimeSpan?>(
|
||||
retryAfter + TimeSpan.FromSeconds(1)
|
||||
);
|
||||
}
|
||||
|
||||
return ValueTask.FromResult<TimeSpan?>(
|
||||
TimeSpan.FromSeconds(Math.Pow(2, args.AttemptNumber) + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,4 @@ public static class PathEx
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
public static bool IsDirectoryPath(string path) =>
|
||||
path.EndsWith(Path.DirectorySeparatorChar) || path.EndsWith(Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using DiscordChatExporter.Gui.ViewModels;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
using Stylet;
|
||||
using StyletIoC;
|
||||
|
||||
#if !DEBUG
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
@@ -13,15 +13,14 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Cogwheel" Version="2.0.3" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.25.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Cogwheel" Version="2.0.4" />
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.26.2" PrivateAssets="all" />
|
||||
<PackageReference Include="Deorcify" Version="1.0.2" PrivateAssets="all" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.5.1" PrivateAssets="all" />
|
||||
<PackageReference Include="Gress" Version="2.1.1" />
|
||||
<PackageReference Include="MaterialDesignColors" Version="2.1.4" />
|
||||
<PackageReference Include="MaterialDesignThemes" Version="4.9.0" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.77" />
|
||||
<PackageReference Include="Onova" Version="2.6.10" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Stylet" Version="1.3.6" />
|
||||
|
||||
@@ -66,7 +66,8 @@ public partial class SettingsService
|
||||
{
|
||||
try
|
||||
{
|
||||
return Registry.CurrentUser
|
||||
return Registry
|
||||
.CurrentUser
|
||||
.OpenSubKey(
|
||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
false
|
||||
|
||||
@@ -12,8 +12,8 @@ using DiscordChatExporter.Gui.Models;
|
||||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.Utils;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
using DiscordChatExporter.Gui.ViewModels.Messages;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
using DiscordChatExporter.Gui.ViewModels.Messages;
|
||||
using Gress;
|
||||
using Gress.Completable;
|
||||
using Stylet;
|
||||
@@ -234,7 +234,8 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
|
||||
var exporter = new ChannelExporter(_discord);
|
||||
|
||||
var channelProgressPairs = dialog.Channels!
|
||||
var channelProgressPairs = dialog
|
||||
.Channels!
|
||||
.Select(c => new { Channel = c, Progress = _progressMuxer.CreateInput() })
|
||||
.ToArray();
|
||||
|
||||
@@ -276,9 +277,7 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
_eventAggregator.Publish(
|
||||
new NotificationMessage(ex.Message.TrimEnd('.') + $" ({channel.Name})")
|
||||
);
|
||||
_eventAggregator.Publish(new NotificationMessage(ex.Message.TrimEnd('.')));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Microsoft.Win32;
|
||||
using Ookii.Dialogs.Wpf;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
@@ -68,9 +67,8 @@ public class DialogManager : IDisposable
|
||||
|
||||
public string? PromptDirectoryPath(string defaultDirPath = "")
|
||||
{
|
||||
var dialog = new VistaFolderBrowserDialog { SelectedPath = defaultDirPath };
|
||||
|
||||
return dialog.ShowDialog() == true ? dialog.SelectedPath : null;
|
||||
var dialog = new OpenFolderDialog { InitialDirectory = defaultDirPath };
|
||||
return dialog.ShowDialog() == true ? dialog.FolderName : null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -4,8 +4,8 @@ using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.Utils;
|
||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
using DiscordChatExporter.Gui.ViewModels.Messages;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
using DiscordChatExporter.Gui.ViewModels.Messages;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Stylet;
|
||||
|
||||
|
||||
@@ -193,9 +193,8 @@
|
||||
<LineBreak />
|
||||
<Run Text="6. Switch between random channels to trigger network requests" />
|
||||
<LineBreak />
|
||||
<Run Text="7. Search for a request containing" />
|
||||
<Run FontWeight="SemiBold" Text="messages?limit=50" />
|
||||
<Run Text="or similar" />
|
||||
<Run Text="7. Search for a request that starts with" />
|
||||
<Run FontWeight="SemiBold" Text="messages" />
|
||||
<LineBreak />
|
||||
<Run Text="8. Select the" />
|
||||
<Run FontWeight="SemiBold" Text="Headers" />
|
||||
|
||||
@@ -271,7 +271,10 @@
|
||||
</Grid>
|
||||
|
||||
<!-- Reuse assets -->
|
||||
<Grid Margin="16,8" ToolTip="Reuse previously downloaded assets to avoid redundant requests">
|
||||
<Grid
|
||||
Margin="16,8"
|
||||
IsEnabled="{Binding ShouldDownloadAssets}"
|
||||
ToolTip="Reuse previously downloaded assets to avoid redundant requests">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -289,14 +292,16 @@
|
||||
</Grid>
|
||||
|
||||
<!-- Assets path -->
|
||||
<Grid Margin="16,8">
|
||||
<Grid
|
||||
Margin="16,8"
|
||||
IsEnabled="{Binding ShouldDownloadAssets}"
|
||||
ToolTip="Download assets to this directory. If not specified, the asset directory path will be derived from the output path.">
|
||||
<TextBox
|
||||
Padding="16,16,42,16"
|
||||
materialDesign:HintAssist.Hint="Assets directory path"
|
||||
materialDesign:HintAssist.IsFloating="True"
|
||||
Style="{DynamicResource MaterialDesignOutlinedTextBox}"
|
||||
Text="{Binding AssetsDirPath}"
|
||||
ToolTip="Download assets to this directory. If not specified, the asset directory path will be derived from the output path." />
|
||||
Text="{Binding AssetsDirPath}" />
|
||||
<Button
|
||||
Width="24"
|
||||
Height="24"
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
**DiscordChatExporter** is an application that can be used to export message history from any [Discord](https://discord.com) channel to a file.
|
||||
It works with direct messages, group messages, and server channels, and supports Discord's dialect of markdown as well as most other rich media features.
|
||||
|
||||
> ❔ If you have questions or issues, **please refer to the [docs](.docs)**.
|
||||
<!-- Can't use a relative link here due to a bug in markdown parsing -->
|
||||
> ❔ If you have questions or issues, **please refer to the [docs](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)**.
|
||||
|
||||
> 💬 If you want to chat, **join my [Discord server](https://discord.gg/2SUWKFnHSm)**.
|
||||
|
||||
@@ -57,7 +58,7 @@ The following table lists all available download options:
|
||||
<td>
|
||||
<ul>
|
||||
<li>🟢 <b><a href="https://github.com/Tyrrrz/DiscordChatExporter/releases/latest">Stable release</a></b> (<code>DiscordChatExporter.zip</code>)</li>
|
||||
<li>🟠 <a href="https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/main.yml">CI build</a> (<code>DiscordChatExporter.zip</code>)</li>
|
||||
<li>🟠 <a href="https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/main.yml">CI build</a> (<code>DiscordChatExporter.Gui.zip</code>)</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
@@ -70,8 +71,8 @@ The following table lists all available download options:
|
||||
<td><b>CLI</b></td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>🟢 <b><a href="https://github.com/Tyrrrz/DiscordChatExporter/releases/latest">Stable release</a></b> (<code>DiscordChatExporter.CLI.zip</code></li>
|
||||
<li>🟠 <a href="https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/main.yml">CI build</a> (<code>DiscordChatExporter.CLI.zip</code>)</li>
|
||||
<li>🟢 <b><a href="https://github.com/Tyrrrz/DiscordChatExporter/releases/latest">Stable release</a></b> (<code>DiscordChatExporter.Cli.zip</code></li>
|
||||
<li>🟠 <a href="https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/main.yml">CI build</a> (<code>DiscordChatExporter.Cli.zip</code>)</li>
|
||||
<li>🐋 <a href="https://hub.docker.com/r/tyrrrz/discordchatexporter">Docker</a> (<code>tyrrrz/discordchatexporter</code>)</li>
|
||||
<li>📦 <a href="https://aur.archlinux.org/packages/discord-chat-exporter-cli">AUR</a> (<code>discord-chat-exporter-cli</code>)</li>
|
||||
<li>📦 <a href="https://search.nixos.org/packages?query=discordchatexporter-cli">Nix</a> (<code>discordchatexporter-cli</code>)</li>
|
||||
@@ -88,21 +89,21 @@ The following table lists all available download options:
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
> **Note**:
|
||||
> AUR and Nix packages linked above are maintained by the community.
|
||||
> If you have any issues with them, please contact the corresponding maintainers.
|
||||
|
||||
> **Warning**:
|
||||
> To run **DiscordChatExporter** on macOS and Linux, you need to make sure that **.NET 7.0 Runtime** is installed.
|
||||
> **Important**:
|
||||
> To run **DiscordChatExporter** on macOS and Linux, you need to make sure that **.NET 8.0 Runtime** is installed.
|
||||
> You can download it here:
|
||||
>
|
||||
> - [.NET 7.0 Runtime for **macOS x64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/sdk-7.0.101-macos-x64-installer)
|
||||
> - [.NET 7.0 Runtime for **macOS Arm64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/sdk-7.0.101-macos-arm64-installer)
|
||||
> - [.NET 7.0 Runtime for **Linux**](https://docs.microsoft.com/en-us/dotnet/core/install/linux) (find the correct download for your distro)
|
||||
> - [.NET 8.0 Runtime for **macOS x64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-macos-x64-installer)
|
||||
> - [.NET 8.0 Runtime for **macOS arm64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-8.0.0-macos-arm64-installer)
|
||||
> - [.NET 8.0 Runtime for **Linux**](https://learn.microsoft.com/dotnet/core/install/linux) (find the correct download for your distro)
|
||||
>
|
||||
> This should not be necessary if you install **DiscordChatExporter** using a package manager, as it should take care of the dependencies for you.
|
||||
> This is also not necessary if you are running **DiscordChatExporter** via Docker, because the image already contains the runtime.
|
||||
|
||||
> **Note**:
|
||||
> AUR and Nix packages linked above are maintained by the community.
|
||||
> If you have any issues with them, please contact the corresponding maintainers.
|
||||
|
||||
## Features
|
||||
|
||||
- Graphical user interface (Windows)
|
||||
@@ -118,7 +119,7 @@ The following table lists all available download options:
|
||||

|
||||

|
||||
|
||||
## Related projects
|
||||
## See also
|
||||
|
||||
- [**Chat Analytics**](https://github.com/mlomb/chat-analytics) — solution for analyzing chat patterns of Discord users, using exports produced by **DiscordChatExporter**.
|
||||
- [**DiscordChatExporter-frontend**](https://github.com/slatinsky/DiscordChatExporter-frontend) — convenient viewer for exports produced by **DiscordChatExporter**.
|
||||
Reference in New Issue
Block a user