mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-07 14:44:39 +02:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e2c78eabf | |||
| 7df7614cd0 | |||
| 5b65e4ce5d | |||
| 597ec1e7b0 | |||
| b9fa38d2b2 | |||
| 91ba79e6ec | |||
| 56df93305e | |||
| c69e39800d | |||
| f505ff3f1e | |||
| b844e48016 | |||
| 36fc6490f3 | |||
| 013a03a4f2 | |||
| e216c07580 | |||
| d1244208b6 | |||
| 6e56f29404 | |||
| 4edcdf0955 | |||
| 7d458cd3fd | |||
| 57eb32d9c0 | |||
| 506bc0176b | |||
| 67b31c5f68 | |||
| d9c06bacda | |||
| db45b49af7 | |||
| fc07f8da6c | |||
| b138908eb3 | |||
| 2c4f812d4f | |||
| 324d6bab20 | |||
| e5d5f1c85f | |||
| 49c4b12512 | |||
| 57e2dc9313 | |||
| 07b72b9023 | |||
| f3830247fe | |||
| a8f89ec292 | |||
| 0bdbd44492 | |||
| db23971b08 | |||
| 90bf4975b9 | |||
| c2f9868746 | |||
| ec840e76ae | |||
| b672c30071 | |||
| e752269467 | |||
| 5b1b720503 | |||
| 3dc3e3fa01 | |||
| 001ea7e495 |
+108
@@ -0,0 +1,108 @@
|
||||
# Docker usage instructions
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
```
|
||||
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 various automation scenarios.
|
||||
|
||||
You can see all available tags [here](https://hub.docker.com/r/tyrrrz/discordchatexporter/tags?page=1&ordering=name).
|
||||
|
||||
### Usage
|
||||
|
||||
To run the CLI in Docker and render help text:
|
||||
|
||||
```
|
||||
docker run --rm tyrrrz/discordchatexporter:stable
|
||||
```
|
||||
|
||||
To export a channel:
|
||||
|
||||
```
|
||||
docker run --rm -v /path/on/machine:/out tyrrrz/discordchatexporter:stable export -t TOKEN -c CHANNELID
|
||||
```
|
||||
|
||||
If you want colored output, real-time progress reporting, and some other stuff, pass `-it` (interactive) option:
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
**If you are running SELinux, you will need to add the `:z` option after `/out`, e.g.:**
|
||||
|
||||
```
|
||||
docker run --rm -v /path/on/machine:/out:z tyrrrz/discordchatexporter:stable export -t TOKEN -c CHANNELID
|
||||
```
|
||||
|
||||
For more information, refer to the [Docker docs SELinux labels for bind mounts page](https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label).
|
||||
|
||||
You can also use the current working directory as the output directory by specifying:
|
||||
|
||||
- `-v $PWD:/out` in Mac/Linux
|
||||
- `-v $pwd.Path:/out` in PowerShell
|
||||
|
||||
For more information, please refer to the [Dockerfile](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/Dockerfile) and [Docker documentation](https://docs.docker.com/engine/reference/run/).<br>
|
||||
To get your Token and Channel IDs, please refer to [this page](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md).
|
||||
|
||||
### Environment variables
|
||||
|
||||
DiscordChatExpoter CLI accepts environment variables as fallbacks for the `--token` and `--bot` options.
|
||||
|
||||
- `DISCORD_TOKEN` to set the token
|
||||
- `DISCORD_TOKEN_BOT` to set whether it's a bot token (`true` or `false`)
|
||||
|
||||
You can use these variables 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 for more information:
|
||||
|
||||
- [Docker run - Set environment variables](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file)
|
||||
|
||||
### 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.
|
||||
|
||||
### Build Docker images for arm on Raspberry Pi
|
||||
|
||||
If you use the Docker image on arm (for example Raspberry Pi), you'd build the image as below.
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/Tyrrrz/DiscordChatExporter.git
|
||||
$ cd DiscordChatExporter
|
||||
$ docker build -f DiscordChatExporter.Cli.dockerfile -t tyrrrz/discordchatexporter:latest .
|
||||
```
|
||||
|
||||
After a while, some images are built like below.
|
||||
|
||||
```bash
|
||||
$ docker image ls
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
tyrrrz/discordchatexporter latest 7c9ead725177 49 seconds ago 199MB
|
||||
<none> <none> 57f9f49d05af About a minute ago 678MB
|
||||
mcr.microsoft.com/dotnet/sdk 5.0 72af6071941e 4 days ago 641MB
|
||||
mcr.microsoft.com/dotnet/runtime 3.1 6229bebd5a11 4 days ago 197MB
|
||||
```
|
||||
|
||||
Among these, two images (`tyrrrz/discordchatexporter:latest` and `mcr.microsoft.com/dotnet/runtime:3.1`) are required, so you may delete the other two images.
|
||||
|
||||
Now you can execute DiscordChatExporter:
|
||||
|
||||
```bash
|
||||
$ docker run --rm tyrrrz/discordchatexporter:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Special thanks to [@simnalamburt](https://github.com/simnalamburt) (dockerize) and [@Nimja](https://github.com/nimja) (better instructions)
|
||||
@@ -0,0 +1,66 @@
|
||||
# Install .NET Core runtime
|
||||
|
||||
**.NET v7.0 Runtime** is required by **DiscordChatExporter v2.37 and newer**.
|
||||
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.
|
||||
|
||||
- Install
|
||||
- [Windows and macOS](#windows-and-macos)
|
||||
- [Linux](#linux)
|
||||
- [Docker](#docker)
|
||||
- [Verify installation](#Verify-installation-optional)
|
||||
|
||||
## Windows and macOS
|
||||
|
||||
**Download from the direct links:**
|
||||
|
||||
- [.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)
|
||||
|
||||
> 💡 [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)
|
||||
> 💡 [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)
|
||||
|
||||
## Linux
|
||||
|
||||
**You only need to install .NET Core <u>Runtime</u> v7.0**
|
||||
|
||||
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)
|
||||
|
||||
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 Core** 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 Core Runtime** is correctly installed, the command will output the following:
|
||||
|
||||
```
|
||||
|
||||
.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]
|
||||
```
|
||||
@@ -0,0 +1,493 @@
|
||||
# GUI, CLI and Formats explained
|
||||
|
||||
# Getting started
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
You can get it by [downloading](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest) the `DiscordChatExporter.zip` file.
|
||||
- **Command Line Interface** (**CLI**) - offers greater flexibility and more features for advanced users, such as export scheduling, ID lists, and more specific date ranges.
|
||||
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.
|
||||
|
||||
**(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))**
|
||||
|
||||
# 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 (→).
|
||||
**NEVER SHARE YOUR TOKEN**. A token gives <u>full access</u> 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.
|
||||
|
||||
> 💡 Tip: 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.
|
||||
> 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
|
||||
|
||||
> 💡 Tip: 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
|
||||
|
||||
> 💡 Tip: 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
|
||||
|
||||
> 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 |
|
||||
|
||||
> 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 |
|
||||
|
||||
> 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 |
|
||||
|
||||
> 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 |
|
||||
|
||||
> 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 |
|
||||
|
||||
> 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 |
|
||||
|
||||
> 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 |
|
||||
|
||||
> 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).
|
||||
|
||||
# File formats
|
||||
|
||||
## HTML
|
||||
|
||||

|
||||
The HTML format replicates Discord's interface, making it the most user-friendly option.
|
||||
It's the best format for attachment preview and sharing.
|
||||
You can open `.html` files with a web browser, such as Google Chrome.
|
||||
|
||||
> Please note that if a picture is deleted, or if a user changes its avatar, the respective images will no longer be displayed.
|
||||
> Export using the "Download referenced assets" (`--media`) option to avoid this.
|
||||
|
||||
## Plain Text
|
||||
|
||||
<img src="https://i.imgur.com/PbUyRXD.png" height="400"/>
|
||||
|
||||
The Plain Text format is the best option for archiving due to its small size.
|
||||
You can open `.txt` files with a text editor, such as Notepad.
|
||||
|
||||
## JSON
|
||||
|
||||
<img src="https://i.imgur.com/FAeSA4O.png" height="400"/>
|
||||
|
||||
The JSON format contains more technical information and is easily parsable.
|
||||
You can open `.json` files with a text editor, such as Notepad.
|
||||
|
||||
## CSV
|
||||
|
||||

|
||||

|
||||
|
||||
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)
|
||||
@@ -0,0 +1,20 @@
|
||||
# Linux usage instructions
|
||||
|
||||
### Step 1: Install .NET Core Runtime
|
||||
|
||||
Please follow the [instructions provided here](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Dotnet.md).
|
||||
|
||||
### Step 2: Downloading and using DiscordChatExporter.Cli
|
||||
|
||||
**1.** Download [DiscordChatExporter.CLI.zip](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest) and extract it to a folder.<br/>
|
||||
**2.** Open Terminal.<br/>
|
||||
**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.<br/>
|
||||
**4.** Replace `TOKEN` and `CHANNEL`, then execute this command to export:
|
||||
|
||||
```
|
||||
dotnet DiscordChatExporter.Cli.dll export -t TOKEN -c CHANNEL
|
||||
```
|
||||
|
||||
> 💡 Tip: [How to get Token and Channel IDs](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/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.
|
||||
@@ -0,0 +1,20 @@
|
||||
# macOS usage instructions
|
||||
|
||||

|
||||
|
||||
### Step 1: Install .NET Core Runtime
|
||||
|
||||
Please follow the [instructions provided here](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Dotnet.md).
|
||||
|
||||
### Step 2: Downloading and using DiscordChatExporter.Cli
|
||||
|
||||
**1.** Download [DiscordChatExporter.CLI.zip](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest) and extract it to a folder.<br/>
|
||||
**2.** Search for `Terminal.app` in Spotlight (⌘+SPACE), then open it.<br/>
|
||||
**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.<br/>
|
||||
**4.** Execute the following command to export, replacing `TOKEN` and `CHANNEL` with your own values:
|
||||
|
||||
dotnet DiscordChatExporter.Cli.dll export -t TOKEN -c CHANNEL
|
||||
|
||||
> 💡 Tip: [How to get Token and Channel IDs](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/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.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Message filters
|
||||
|
||||
You can use a special notation to filter messages that you want to have included in an export. The notation syntax is designed to mimic Discord's search query syntax, but with additional capabilities.
|
||||
|
||||
To configure a filter, specify it in advanced export parameters when using the GUI or by passing the `--filter` option when using the CLI.
|
||||
|
||||
## Examples
|
||||
|
||||
- Filter by user
|
||||
|
||||
```
|
||||
from:Tyrrrz
|
||||
```
|
||||
|
||||
- Filter by user (with discriminator)
|
||||
|
||||
```
|
||||
from:Tyrrrz#1234
|
||||
```
|
||||
|
||||
- Filter by message content (allowed values: `link`, `embed`, `file`, `video`, `image`, `sound`)
|
||||
|
||||
```
|
||||
has:image
|
||||
```
|
||||
|
||||
- Filter by mentioned user (same rules apply as with `from:` filter)
|
||||
|
||||
```
|
||||
mentions:Tyrrrz#1234
|
||||
```
|
||||
|
||||
- Filter by contained text (has word "hello" and word "world" somewhere in the message text):
|
||||
|
||||
```
|
||||
hello world
|
||||
```
|
||||
|
||||
- Filter by contained text (has the string "hello world" somewhere in the message text):
|
||||
|
||||
```
|
||||
"hello world"
|
||||
```
|
||||
|
||||
- Combine multiple filters ('and'):
|
||||
|
||||
```
|
||||
from:Tyrrrz has:image
|
||||
```
|
||||
|
||||
- Same thing but with an explicit operator:
|
||||
|
||||
```
|
||||
from:Tyrrrz & has:image
|
||||
```
|
||||
|
||||
- Combine multiple filters ('or'):
|
||||
|
||||
```
|
||||
from:Tyrrrz | from:"96-LB"
|
||||
```
|
||||
|
||||
- Combine multiple filters using groups:
|
||||
|
||||
```
|
||||
(from:Tyrrrz | from:"96-LB") has:image
|
||||
```
|
||||
|
||||
- Negate a filter:
|
||||
|
||||
```
|
||||
-from:Tyrrrz | -has:image
|
||||
```
|
||||
|
||||
- Negate a grouped filter:
|
||||
|
||||
```
|
||||
-(from:Tyrrrz has:image)
|
||||
```
|
||||
|
||||
- Escape special characters (`-` is escaped below so it's not parsed as negation operator):
|
||||
|
||||
```
|
||||
from:96\-LB
|
||||
```
|
||||
|
||||
## CLI Caveat
|
||||
|
||||
When using the CLI and the specified filter starts with a negation (for example `-from:Tyrrrz`), you need to quote the value and prepend it with a space (i.e. `" -from:Tyrrrz"`). This prevents the beginning of the input from being incorrectly treated as a value passed to the `-f|--format` option.
|
||||
|
||||
In other words, this breaks:
|
||||
|
||||
```
|
||||
DiscordChatExporter.Cli export [...] --filter "-from:Tyrrrz"
|
||||
```
|
||||
|
||||
This works (note the space):
|
||||
|
||||
```
|
||||
DiscordChatExporter.Cli export [...] --filter " -from:Tyrrrz"
|
||||
```
|
||||
|
||||
Additionally, when using the CLI, it's recommended to use single quotes for contingent string of characters rather than double quotes (so it doesn't collide with the double quotes used as escaping mechanism in terminals). E.g.:
|
||||
|
||||
This breaks:
|
||||
|
||||
```
|
||||
DiscordChatExporter.Cli export [...] --filter "from:"Some name with spaces""
|
||||
```
|
||||
|
||||
This works:
|
||||
|
||||
```
|
||||
DiscordChatExporter.Cli export [...] --filter "from:'Some name with spaces'"
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
# Home
|
||||
|
||||
<p align="center">
|
||||
<img src="https://i.imgur.com/IkrFoA0.png" height="120" />
|
||||
<br>
|
||||
<b>DiscordChatExporter</b> can be used to export message history from a Discord channel to a file.
|
||||
</p>
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
**Step 1:** [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)
|
||||
|
||||
**Step 2:** [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)
|
||||
|
||||
**Step 3:** 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)
|
||||
|
||||
## 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)
|
||||
- **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)
|
||||
|
||||
## Video tutorial
|
||||
|
||||
- Video by [NoIntro Tutorials](https://youtube.com/channel/UCFezKSxdNKJe77-hYiuXu3Q) (using DiscordChatExporter GUI)
|
||||
|
||||
[](https://youtube.com/watch?v=jjtu0VQXV7I)
|
||||
|
||||
## 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)
|
||||
@@ -0,0 +1,126 @@
|
||||
# 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)).
|
||||
|
||||
You can use Cron on macOS, but [this method](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-MacOS.md) is preferred.
|
||||
|
||||
#
|
||||
|
||||
**1.** Open Terminal and create a new text file with `nano /path/to/DiscordChatExporter/cron.sh`<br/>
|
||||
|
||||
> For beginners: You can't use your mouse in nano, use the arrow keys to control the cursor (caret).
|
||||
|
||||
**2.** Paste the following into the text file:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs
|
||||
|
||||
TOKEN=tokenhere
|
||||
CHANNELID=channelhere
|
||||
DLLFOLDER=dceFOLDERpathhere
|
||||
FILENAME=filenamehere
|
||||
EXPORTDIRECTORY=dirhere
|
||||
EXPORTFORMAT=formathere
|
||||
# Available export formats: plaintext, htmldark, htmllight, json, csv
|
||||
# /\ CaSe-SeNsItIvE /\
|
||||
# You can edit the export command on line 40 if you'd like to include more options like date ranges and date format. You can't use partitioning (-p) with this script.
|
||||
|
||||
# This will verify if EXPORTFORMAT is valid and will set the final file extension according to it. If the format is invalid, the script will display a message and exit.
|
||||
if [[ "$EXPORTFORMAT" == "plaintext" ]]; then
|
||||
FORMATEXT=.txt
|
||||
elif [[ "$EXPORTFORMAT" == "htmldark" ]] || [[ "$EXPORTFORMAT" == "htmllight" ]]; then
|
||||
FORMATEXT=.html
|
||||
elif [[ "$EXPORTFORMAT" == "json" ]]; then
|
||||
FORMATEXT=.json
|
||||
elif [[ "$EXPORTFORMAT" == "csv" ]]; then
|
||||
FORMATEXT=.csv
|
||||
else
|
||||
echo "$EXPORTFORMAT - Unknown export format"
|
||||
echo "Available export formats: plaintext, htmldark, htmllight, csv, json"
|
||||
echo "/\ CaSe-SeNsItIvE /\\"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# This will change the script's directory to DLLPATH, if unable to do so, the script will exit.
|
||||
cd $DLLFOLDER || exit 1
|
||||
|
||||
# This will export your chat
|
||||
dotnet DiscordChatExporter.Cli.dll export -t $TOKEN -c $CHANNELID -f $EXPORTFORMAT -o $FILENAME.tmp
|
||||
|
||||
# This sets the current time to a variable
|
||||
CURRENTTIME=$(date +"%Y-%m-%d-%H-%M-%S")
|
||||
|
||||
# This will move the .tmp file to the desired export location, if unable to do so, it will attempt to delete the .tmp file.
|
||||
if ! mv "$FILENAME.tmp" "${EXPORTDIRECTORY//\"}/$FILENAME-$CURRENTTIME$FORMATEXT" ; then
|
||||
echo "Unable to move $FILENAME.tmp to $EXPORTDIRECTORY/$FILENAME-$CURRENTTIME$FORMATEXT."
|
||||
echo "Cleaning up..."
|
||||
if ! rm -Rf "$FILENAME.tmp" ; then
|
||||
echo "Unable to remove $FILENAME.tmp."
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
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).
|
||||
|
||||
`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).
|
||||
|
||||
`formathere` with one of the available export formats.
|
||||
|
||||
**Remember to escape spaces** (add `\` before them) or to quote (") the paths (`"/home/my user"`)!<br/>
|
||||
|
||||
> For beginners: To save, hold down CTRL and then press O, if asked for a filename, type it and press ENTER. Hit CTRL+X to exit the text editor.<br/> > [Check out this page](https://wiki.gentoo.org/wiki/Nano/Basics_Guide) if you want to know more about nano.
|
||||
|
||||
**4.** Make your script executable with `chmod +x /path/to/DiscordChatExporter/cron.sh`
|
||||
|
||||
**5.** Let's edit the cron file. If you want to run the script with your user privileges, edit it by running `crontab -e`. If you want to run the script as root, edit it with `sudo crontab -e`.<br/>
|
||||
If this is your first time running this command, you might be asked to select a text editor. Nano is easier for beginners.
|
||||
|
||||
**6.** Add the following to the end of the file `* * * * * /path/to/DiscordChatExporter/cron.sh >/tmp/discordchatexporter.log 2>/tmp/discordchatexportererror.log`<br/>
|
||||
Don't forget to replace the `/path/to/DiscordChatExporter/cron.sh`!
|
||||
|
||||
> 💡 Tip: If you don't want logs to be created, replace both `/tmp/discordchatexporter.log` with `/dev/null`
|
||||
|
||||
Then replace the \*s according to:
|
||||
|
||||

|
||||
|
||||
#
|
||||
|
||||
**Examples**<br/>
|
||||
If you want to execute the script at minute 15 of every hour: `15 * * * *`<br/>
|
||||
Every 30 minutes `*/30 * * * *`<br/>
|
||||
Everyday at midnight `0 0 * * *`<br/>
|
||||
Everyday at noon `0 12 * * *`<br/>
|
||||
Everyday at 3, 4 and 6 PM `0 15,16,18 * * *`<br/>
|
||||
Every Wednesday at 9 AM `0 9 * * 3`
|
||||
|
||||
Verify your cron time [here](https://crontab.guru).
|
||||
|
||||
#
|
||||
|
||||
**Extra information**
|
||||
|
||||
The week starts on Sunday. 0 = SUN, 1 = MON ... 7 = SUN.
|
||||
|
||||
Be aware that if you set the day to '31', the script will only run on months that have the 31st day.
|
||||
|
||||
> 💡 Tip: [Learn more about running a cron job on the last day of the month here](https://stackoverflow.com/questions/6139189/cron-job-to-run-on-the-last-day-of-the-month) (expert).
|
||||
|
||||
The default filename for the exported channel is `YYYY-MM-DD-hh-mm-ss-yourfilename`. You can change it if you'd like.
|
||||
|
||||
Don't forget to update your token in the script after it has been reset!
|
||||
|
||||
#
|
||||
|
||||
Special thanks to [@Yudi](https://github.com/Yudi)
|
||||
@@ -0,0 +1,276 @@
|
||||
# Scheduling exports on macOS
|
||||
|
||||
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)).
|
||||
|
||||
- [**1.** Creating the script](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-MacOS.md#1-creating-the-script)
|
||||
- [**2.** Creating the .plist file](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-MacOS.md#2-creating-the-plist-file)
|
||||
- [Export on System Boot/User Login](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-MacOS.md#export-on-system-bootuser-login)
|
||||
- [Export every _n_ seconds](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-MacOS.md#export-every-n-seconds)
|
||||
- [Export at a specific time and date](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-MacOS.md#export-at-a-specific-time-and-date)
|
||||
- [**3.** Loading the .plist into launchctl](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/scheduling-MacOS.md#3-loading-the-plist-into-launchctl)
|
||||
|
||||
## 1. Creating the script
|
||||
|
||||
**1.** Open TextEdit.app and create a new file
|
||||
|
||||
**2.** Convert the file to a plain text one in 'Format > Make Plain Text' (⇧⌘T)
|
||||
|
||||

|
||||
|
||||
**3.** Paste the following into the text editor:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs
|
||||
|
||||
TOKEN=tokenhere
|
||||
CHANNELID=channelhere
|
||||
DLLFOLDER=dceFOLDERpathhere
|
||||
FILENAME=filenamehere
|
||||
EXPORTDIRECTORY=dirhere
|
||||
EXPORTFORMAT=formathere
|
||||
# Available export formats: plaintext, htmldark, htmllight, json, csv
|
||||
# /\ CaSe-SeNsItIvE /\
|
||||
# You can edit the export command on line 43 if you'd like to include more options like date ranges and date format. You can't use partitioning (-p) with this script.
|
||||
|
||||
# This variable specifies in which directories the executable programs are located. Don't change it.
|
||||
PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/share/dotnet
|
||||
|
||||
# This will verify if EXPORTFORMAT is valid and will set the final file extension according to it. If the format is invalid, the script will display a message and exit.
|
||||
if [[ "$EXPORTFORMAT" == "plaintext" ]]; then
|
||||
FORMATEXT=.txt
|
||||
elif [[ "$EXPORTFORMAT" == "htmldark" ]] || [[ "$EXPORTFORMAT" == "htmllight" ]]; then
|
||||
FORMATEXT=.html
|
||||
elif [[ "$EXPORTFORMAT" == "json" ]]; then
|
||||
FORMATEXT=.json
|
||||
elif [[ "$EXPORTFORMAT" == "csv" ]]; then
|
||||
FORMATEXT=.csv
|
||||
else
|
||||
echo "$EXPORTFORMAT - Unknown export format"
|
||||
echo "Available export formats: plaintext, htmldark, htmllight, csv, json"
|
||||
echo "/\ CaSe-SeNsItIvE /\\"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# This will change the script's directory to DLLPATH, if unable to do so, the script will exit.
|
||||
cd $DLLFOLDER || exit 1
|
||||
|
||||
# This will export your chat
|
||||
dotnet DiscordChatExporter.Cli.dll export -t $TOKEN -c $CHANNELID -f $EXPORTFORMAT -o $FILENAME.tmp
|
||||
|
||||
# This sets the current time to a variable
|
||||
CURRENTTIME=$(date +"%Y-%m-%d-%H-%M-%S")
|
||||
|
||||
# This will move the .tmp file to the desired export location. If unable to do so, it will attempt to delete the .tmp file.
|
||||
if ! mv "$FILENAME.tmp" "${EXPORTDIRECTORY//\"}/$FILENAME-$CURRENTTIME$FORMATEXT" ; then
|
||||
echo "Unable to move $FILENAME.tmp to $EXPORTDIRECTORY/$FILENAME-$CURRENTTIME$FORMATEXT."
|
||||
echo "Cleaning up..."
|
||||
if ! rm -Rf "$FILENAME.tmp" ; then
|
||||
echo "Unable to remove $FILENAME.tmp."
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
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)
|
||||
|
||||
`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`)
|
||||
|
||||
`formathere` with one of the available export formats
|
||||
|
||||
To quickly get file or folder paths, select the file/folder, then hit Command+I (⌘I) and copy what's after `Where:`.
|
||||
After copying and pasting, make sure the file/folder name is not missing. If a folder has spaces in its name, add `\` before the spaces, like in the example below:
|
||||
|
||||
- `Discord\ Exports` - Wrong ✗
|
||||
- `/Users/user/Documents` - Wrong ✗
|
||||
- `/Users/user/Documents/Discord Exports` - Wrong ✗
|
||||
- `/Users/user/Documents/Discord\ Exports/DCE.Cli.dll` - Wrong ✗
|
||||
- `/Users/user/Documents/Discord \Exports` - Wrong ✗
|
||||
- `/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.
|
||||
|
||||
## 2. Creating the .plist file
|
||||
|
||||
Open TextEdit, make a Plain Text (⇧⌘T) and then paste the following into it:
|
||||
|
||||
```xml
|
||||
<?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>
|
||||
</plist>
|
||||
```
|
||||
|
||||
- The `Label` string is the name of the export job, it must be something unique. Replace the `local.discordchatexporter` between the `<string>` with another name if you'd like to run more than one script.
|
||||
|
||||
- The `Program` string is the path to the script. Replace `/path/to/filename.sh` between the `<string>` with the path of the previously created script.
|
||||
|
||||
- Replace the `REPLACEME` with the content presented in the following sections according to <u>when</u> you want to export.
|
||||
|
||||
When you're done, save the file with the same name as the `Label` and with the `.plist` extension (not `.txt`), like `local.discordchatexporter.plist`.
|
||||
|
||||
### Exporting on System Boot/User Login
|
||||
|
||||
```xml
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
```
|
||||
|
||||
### Export every _n_ seconds
|
||||
|
||||
The following example is to export every 3600 seconds (1 hour), replace the integer value with your desired time:
|
||||
|
||||
```xml
|
||||
<key>StartInterval</key>
|
||||
<integer>3600</integer>
|
||||
```
|
||||
|
||||
### Export at a specific time and date
|
||||
|
||||
```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>
|
||||
</dict>
|
||||
```
|
||||
|
||||
| Key | Integer |
|
||||
| ----------- | ----------------- |
|
||||
| **Month** | 1-12 |
|
||||
| **Day** | 1-31 |
|
||||
| **Weekday** | 0-6 (0 is Sunday) |
|
||||
| **Hour** | 0-23 |
|
||||
| **Minute** | 0-59 |
|
||||
|
||||
**Sunday** - 0; **Monday** - 1; **Tuesday** - 2; **Wednesday** - 3; **Thursday** - 4; **Friday** - 5; **Saturday** - 6
|
||||
|
||||
Replace the template's `0`s according to the desired times.
|
||||
|
||||
You can delete the `<key>`s you don't need, don't forget to remove the `<integer>0</integer>` under it.
|
||||
Omitted keys are interpreted as wildcards, for example, if you delete the Minute key, the script will run at every minute, delete the Weekday key and it'll run at every weekday, and so on.
|
||||
|
||||
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)):**
|
||||
|
||||
Export everyday at 5:15 PM:
|
||||
|
||||
```xml
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>17</integer>
|
||||
<key>Minute</key>
|
||||
<integer>15</integer>
|
||||
</dict>
|
||||
|
||||
```
|
||||
|
||||
Every 15 minutes of an hour (xx:15):
|
||||
|
||||
```xml
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Minute</key>
|
||||
<integer>15</integer>
|
||||
</dict>
|
||||
|
||||
```
|
||||
|
||||
Every Sunday at midnight and every Wednesday full hour (xx:00). Notice the inclusion of `<array>` and `</array>` to allow multiple values:
|
||||
|
||||
```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>
|
||||
</array>
|
||||
```
|
||||
|
||||
## 3. Loading the .plist into launchctl
|
||||
|
||||
**1.** Copy your `filename.plist` file to one of these folders according to how you want it to run:
|
||||
|
||||
- `~/Library/LaunchAgents` runs as the current logged-in user.
|
||||
|
||||
- `/Library/LaunchDaemons` runs as the system "_administrator_" (root).
|
||||
|
||||
- If macOS has a single user:
|
||||
- If you want to export only when the user is logged in, choose the first one.
|
||||
- If you want the script to always run on System Startup, choose the second one.
|
||||
- If macOS has multiple users:
|
||||
- If you want the script to run only when a certain user is logged in, choose the first one.
|
||||
- If you want the script to always run on System Startup, choose the second one.
|
||||
|
||||
To quickly go to these directories, open Finder and press Command+Shift+G (⌘⇧G), then paste the path into the text box.
|
||||
|
||||
**2.** To load the job into launchctl, in Terminal, type `launchctl load`, press SPACE, drag and drop the `.plist` into the Terminal window, then hit RETURN. It won't output anything if it was successfully loaded.
|
||||
|
||||
### Extra launchctl commands
|
||||
|
||||
**Unloading a job**
|
||||
|
||||
```
|
||||
launchctl unload /path/to/Library/LaunchAgents/local.discordchatexporter.plist
|
||||
```
|
||||
|
||||
**List every loaded job**
|
||||
|
||||
```
|
||||
launchctl list
|
||||
```
|
||||
|
||||
**Check if a specific job is enabled**
|
||||
You can also see error codes (2nd number) by running this command.
|
||||
|
||||
```
|
||||
launchctl list | grep local.discordchatexporter
|
||||
```
|
||||
|
||||
#
|
||||
|
||||
Further reading: [Script management with launchd in Terminal on Mac](https://support.apple.com/guide/terminal/script-management-with-launchd-apdc6c1077b-5d5d-4d35-9c19-60f2397b2369/mac) and [launchd.info](https://launchd.info/).
|
||||
Special thanks to [@Yudi](https://github.com/Yudi)
|
||||
@@ -0,0 +1,82 @@
|
||||
# Scheduling exports on Windows
|
||||
|
||||
We'll be using [DiscordChatExporter CLI](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest), PowerShell, and Task Scheduler.
|
||||
|
||||
**1.** Open a text editor such as Notepad and paste:
|
||||
|
||||
```powershell
|
||||
# Info: https://github.com/Tyrrrz/DiscordChatExporter/wiki
|
||||
|
||||
$TOKEN = "tokenhere"
|
||||
$CHANNEL = "channelhere"
|
||||
$EXEPATH = "exefolderhere"
|
||||
$FILENAME = "filenamehere"
|
||||
$EXPORTDIRECTORY = "dirhere"
|
||||
$EXPORTFORMAT = "formathere"
|
||||
# Available export formats: PlainText, HtmlDark, HtmlLight, Json, Csv
|
||||
|
||||
cd $EXEPATH
|
||||
|
||||
.\DiscordChatExporter.Cli.exe export -t $TOKEN -c $CHANNEL -f $EXPORTFORMAT -o "$FILENAME.tmp"
|
||||
|
||||
$Date = Get-Date -Format "yyyy-MM-dd-HH-mm"
|
||||
|
||||
If($EXPORTFORMAT -match "PlainText"){mv "$FILENAME.tmp" -Destination "$EXPORTDIRECTORY\$FILENAME-$Date.txt"}
|
||||
ElseIf($EXPORTFORMAT -match "HtmlDark"){mv "$FILENAME.tmp" -Destination "$EXPORTDIRECTORY\$FILENAME-$Date.html"}
|
||||
ElseIf($EXPORTFORMAT -match "HtmlLight"){mv "$FILENAME.tmp" -Destination "$EXPORTDIRECTORY\$FILENAME-$Date.html"}
|
||||
ElseIf($EXPORTFORMAT -match "Json"){mv "$FILENAME.tmp" -Destination "$EXPORTDIRECTORY\$FILENAME-$Date.json"}
|
||||
ElseIf($EXPORTFORMAT -match "Csv"){mv "$FILENAME.tmp" -Destination "$EXPORTDIRECTORY\$FILENAME-$Date.csv"}
|
||||
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)
|
||||
|
||||
`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)
|
||||
|
||||
`formathere` with one of the available export formats
|
||||
|
||||
Make sure not to delete the quotes (")
|
||||
|
||||
**3.** Save the file as `filename.ps1` not `.txt`
|
||||
|
||||
## Export at Startup
|
||||
|
||||
**1.** Press Windows + R, type `shell:startup` and press ENTER
|
||||
|
||||
**2.** Paste `filename.ps1` or a shortcut into this folder
|
||||
|
||||
## Scheduling with Task Scheduler
|
||||
|
||||
Please notice your computer must be turned on so the exportation can occur.
|
||||
|
||||
**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
|
||||
|
||||

|
||||
|
||||
**4.** Click 'Yes'
|
||||
|
||||

|
||||
|
||||
**5.** Click 'Finish'
|
||||
|
||||

|
||||
|
||||
#
|
||||
|
||||
Special thanks to [@Yudi](https://github.com/Yudi)
|
||||
@@ -0,0 +1,261 @@
|
||||
# Obtaining Token and Channel IDs
|
||||
|
||||
> **Warning**:
|
||||
> **DO NOT SHARE YOUR TOKEN!**.
|
||||
> A token gives full access to an account. To reset a user token, change your account password. To reset a bot token, click on "[Regenerate](#how-to-get-a-bot-token)"
|
||||
|
||||
### How to get a User Token
|
||||
|
||||
**Caution:** [Automating user accounts violates Discord's terms of service](https://support.discord.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-) and may result in account termination. Use at your own risk.
|
||||
|
||||
#### Through your web browser
|
||||
|
||||
Prerequisite step: Navigate to [discord.com](https://discord.com) and login.
|
||||
|
||||
##### In Chrome
|
||||
|
||||
###### 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.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
2. <img width="500" align="right" src="https://i.imgur.com/BDeG0zg.png" />Click the `Network` tab. The [network panel](https://developer.chrome.com/docs/devtools/overview/#network) will open
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
3. <img width="500" align="right" src="https://i.imgur.com/0Lgj0vk.png" />Press <kbd>F5</kbd>. The page will reload, and the network log (the lower half of the network panel) will display several entries.
|
||||
|
||||
<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`.
|
||||
|
||||
<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.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
6. <img width="500" align="right" src="https://i.imgur.com/u7CxXAt.png" />Scroll through the contents of the `Headers` tab until you find an entry beginning with `authorization:`.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
7. <img width="500" align="right" src="https://i.imgur.com/dXcXzma.png" />Right-click the entry and click `copy value`.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
###### Using the storage inspector
|
||||
|
||||
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/) will display.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
2. <img width="500" align="right" src="https://i.imgur.com/biAUIop.png" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>M</kbd> (<kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>M</kbd>). Chrome will enter [Device Mode](https://developer.chrome.com/docs/devtools/device-mode/), and the webpage will display as if on a mobile device.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
3. <img width="500" align="right" src="https://i.imgur.com/oUDRZoy.png" />If necessary, click the `»` at the right end of the tab bar, and click `Application`. The [application panel](https://developer.chrome.com/docs/devtools/overview/#application) will display.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
4. <img width="500" align="right" src="https://i.imgur.com/sydNPia.png" />In the menu to the right, under `Storage`, expand `Local Storage` if necessary, then click `https://discord.com`. The pane to the right will display a list of key-value pairs.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
5. <img width="500" align="right" src="https://i.imgur.com/qKo0ny9.png" />In the text box marked `Filter`, type `token`. The entries will filter down to those containing the string `token`.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
6. <img width="500" align="right" src="https://i.imgur.com/caj3lQq.png" />Click the `token` entry. (Note: if the token doesn't display, try refreshing by pressing <kbd>F5</kbd> or <kbd>⌘</kbd>+<kbd>R</kbd> on macOS)
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
7. <img width="500" align="right" src="https://i.imgur.com/SwWFIH4.png" />Click the text box at the bottom, press <kbd>Ctrl</kbd>+<kbd>A</kbd> (<kbd>⌘</kbd>+<kbd>A</kbd> on macOS) then <kbd>Ctrl</kbd>+<kbd>C</kbd> (<kbd>⌘</kbd>+<kbd>C</kbd> on macOS) to copy the value to your clipboard.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
##### In Firefox
|
||||
|
||||
###### 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.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
2. <img width="500" align="right" src="https://i.imgur.com/j00QzhU.png" />Press <kbd>F5</kbd>. The page will reload, and the [network request list](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/request_list/index.html) will populate with entries.
|
||||
|
||||
<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`.
|
||||
|
||||
<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.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
5. <img width="500" align="right" src="https://i.imgur.com/zBmq1JW.png" />Type `authorization` into the text box labelled `Filter Headers`.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
6. <img width="500" align="right" src="https://i.imgur.com/O3blcIS.png" />Scroll down until you see an entry labeled [authorization](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) under `Request Headers`.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
7. <img width="500" align="right" src="https://i.imgur.com/zHYEYoZ.png" />Right-click the entry labeled `authorization` and select `copy value`.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
###### Using the storage inspector
|
||||
|
||||
1. <img width="500" align="right" src="https://i.imgur.com/A8jqpkm.png" />Press <kbd>Shift</kbd>+<kbd>F9</kbd>. Firefox’s [web developer tools](https://firefox-source-docs.mozilla.org/devtools-user/) will display at the bottom of the window, and the [storage](https://firefox-source-docs.mozilla.org/devtools-user/storage_inspector/index.html) panel will be selected.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
2. <img width="500" align="right" src="https://i.imgur.com/TGcbB7f.png" />Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>M</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>M</kbd> on macOS). Firefox will toggle [responsive design mode](https://firefox-source-docs.mozilla.org/devtools-user/responsive_design_mode/), and the web page will display as if on a mobile device. (Note: Discord may steal focus and respond to the command by toggling mute. If this happens, return focus to Firefox’s web developer tools by clicking somewhere in it, then try the command again.)
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
3. <img width="500" align="right" src="https://i.imgur.com/2xWkep9.png" />In the [storage tree](https://firefox-source-docs.mozilla.org/devtools-user/storage_inspector/index.html#storage-inspector-storage-tree) (the list on the left side of the web developer tools panel), click [Local Storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). The entry will expand, and the entry `https://discord.com` will display beneath it.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
4. <img width="500" align="right" src="https://i.imgur.com/tGlGuOL.png" />In the storage tree, click `https://discord.com`. The [table widget](https://firefox-source-docs.mozilla.org/devtools-user/storage_inspector/index.html#storage-inspector-table-widget) to the right of the storage tree will display several key-value pairs.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
5. <img width="500" align="right" src="https://i.imgur.com/hDNsnZ5.png" />In the text box labelled `Filter items` at the top of the table widget, enter `token`. The table will now only display entries containing the string `token`.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
6. <img width="500" align="right" src="https://i.imgur.com/8fKId1W.png" />Click the entry `token`. The [sidebar](https://firefox-source-docs.mozilla.org/devtools-user/storage_inspector/index.html#storage-inspector-sidebar) will display. (Note: If the token doesn’t display, try refreshing by pressing <kbd>F5</kbd>.)
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
7. <img width="500" align="right" src="https://i.imgur.com/yD1ZuR9.png" />Right-click the single entry in the sidebar and select `copy`.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
#### Through the desktop app / Enabling web developer tools
|
||||
|
||||
##### By editing the settings file
|
||||
|
||||
1. If Discord is running, exit the application by right-clicking the icon in your taskbar tray and clicking `Quit Discord`.
|
||||
|
||||
2. Open Discord's settings file in your preferred text editor. See the following table for help finding it:
|
||||
|
||||
| OS | Stable | Canary | Public Test Build (PTB) |
|
||||
| ------- | ----------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| Windows | `%APPDATA%\discord\settings.json` | `%APPDATA%\discordcanary\settings.json` | `%APPDATA%\discordptb\settings.json` |
|
||||
| macOS | `~/Library/Application Support/discord/settings.json` | `~/Library/Application Support/discordcanary/settings.json` | `~/Library/Application Support/discordptb/settings.json` |
|
||||
| Linux | `~/.config/discord/settings.json` | `~/.config/discordcanary/settings.json` | `~/.config/discordptb/settings.json` |
|
||||
|
||||
If you use BetterDiscord, use the following table instead:
|
||||
|
||||
| OS | Stable | Canary | Public Test Build (PTB) |
|
||||
| ------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| Windows | `%APPDATA%\BetterDiscord\data\stable\settings.json` | `%APPDATA%\BetterDiscord\data\canary\settings.json` | `%APPDATA%\BetterDiscord\data\ptb\settings.json` |
|
||||
| macOS | `~/Library/Application Support/BetterDiscord/data/stable/settings.json` | `~/Library/Application Support/BetterDiscord/data/canary/settings.json` | `~/Library/Application Support/BetterDiscord/data/ptb/settings.json` |
|
||||
| Linux | `~/.config/BetterDiscord/data/stable/settings.json` | `~/.config/BetterDiscord/data/canary/settings.json` | `~/.config/BetterDiscord/data/ptb/settings.json` |
|
||||
|
||||
3. Insert a blank line after the first curly bracket (`{`), add the text `"DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING": true,` to it, and save the file. Your file should resemble the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING": true,
|
||||
"BACKGROUND_COLOR": "#202225",
|
||||
"IS_MAXIMIZED": true
|
||||
}
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
##### Via settings menu (BetterDiscord only)
|
||||
|
||||
1. <img width="500" align="right" src="https://i.imgur.com/mu1g4OF.png" />Click the User Settings button (the gear icon to the right of your username). Discord’s settings page will open.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
2. <img width="500" align="right" src="https://i.imgur.com/qFrIKON.png" />In the sidebar to the left, click `Settings` under the `BetterDiscord` group. BetterDiscord’s settings page will display.
|
||||
|
||||
<br clear="right" />
|
||||
<br />
|
||||
|
||||
3. <img width="500" align="right" src="https://i.imgur.com/48Re5kj.png" />In the main panel to the right, expand the `Developer Settings` group if necessary, and toggle `DevTools` to enabled.
|
||||
|
||||
<br clear="right" />
|
||||
<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).
|
||||
|
||||
### How to get a Bot Token
|
||||
|
||||
**1.** Go to [Discord developer portal](https://discord.com/developers/applications)
|
||||
**2.** Open your Application's settings
|
||||
**3.** Navigate to the **Bot** section on the left
|
||||
**4.** Under **Token** click **Copy**
|
||||
|
||||
**Important**: your bot needs to have **Message Content Intent** enabled for it to be able to read messages!
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### How to get a Server ID or a Server Channel ID
|
||||
|
||||
**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 ID
|
||||
|
||||
### How to get a Direct Message Channel ID
|
||||
|
||||
**1.** Click the three dots next to any message in the channel you want to export.
|
||||
**2.** Paste the link into a text editor.
|
||||
**3.** Copy the text in the highlighted part of the link.
|
||||
|
||||
> E.g. in https꞉//discord.com/channels/@me/**189716987098470**/0985709387059874, you should copy **189716987098470**
|
||||
|
||||
Make sure you're not copying the user's ID.
|
||||
|
||||
**Alternatively, you can also:**
|
||||
**1.** Open the desired direct message channel
|
||||
**2.** Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> (<kbd>⌥</kbd>+<kbd>⌘</kbd>+<kbd>I</kbd> on macOS) on Discord to show developer tools
|
||||
**3.** Navigate to the `Console` tab
|
||||
**4.** Type `window.location.href` and press Enter
|
||||
**5.** Copy the first long sequence of numbers inside the URL
|
||||
|
||||
> E.g. in https꞉//discord.com/channels/@me/**84289740160**, you should copy **84289740160**
|
||||
@@ -0,0 +1,171 @@
|
||||
# Troubleshooting
|
||||
|
||||
Welcome to the Frequently Asked Questions (FAQ) and Troubleshooting page!
|
||||
Here you'll find the answers to most of the questions related to **DiscordChatExporter** (DCE for short) and its core features.
|
||||
|
||||
❓ 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
|
||||
|
||||
### Token stealer?
|
||||
|
||||
No. That's why this kind of software needs to be open-source, so the code can be audited by anyone.
|
||||
Your token is only used to connect to Discord's API, it's not sent anywhere else.
|
||||
If you're using the GUI, be aware that your token will be saved to a plain text file unless you disable it in the settings menu.
|
||||
|
||||
### Why should I be worried about the safety of my token?
|
||||
|
||||
A token can be used to log into your account, so treat it like a password and never share it.
|
||||
|
||||
### How can I reset my token?
|
||||
|
||||
Follow the [instructions here](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Token-and-IDs.md).
|
||||
|
||||
### Will I get banned if I use this?
|
||||
|
||||
Automating user accounts is technically against [TOS](https://discord.com/terms), use at your discretion. [Bot accounts](https://discord.com/developers/docs/topics/oauth2#bots) don't have this restriction.
|
||||
|
||||
### Will the messages disappear from the exported file if I delete a message, delete my account or block a person?
|
||||
|
||||
Text messages will not be removed from the exported file, but if media, such as images and user avatars, is changed or deleted, it will no longer be displayed. To avoid this, export using the "Download media" (`--media`) option.
|
||||
|
||||
### Can DCE export messages that have already been deleted?
|
||||
|
||||
No, DCE cannot access them since they have been permanently deleted from Discord's servers.
|
||||
|
||||
### Can DCE export private chats?
|
||||
|
||||
Yes, if your account has access to them.
|
||||
|
||||
### Can DCE download images?
|
||||
|
||||
Yes, and other media too. Export using the "Download media" (`--media`) option.
|
||||
|
||||
### Can the exported chats be shared?
|
||||
|
||||
Yes.
|
||||
|
||||
### Can DCE recreate the exported chats in Discord?
|
||||
|
||||
No, DCE is an exporter.
|
||||
|
||||
### Can DCE reupload exported messages to another channel?
|
||||
|
||||
No, DCE is an exporter.
|
||||
|
||||
# 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)
|
||||
|
||||
### 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)
|
||||
|
||||
### 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)
|
||||
|
||||
### 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)
|
||||
|
||||
### 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.
|
||||
|
||||
Check the following page: [Installing .NET Core Runtime](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/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.
|
||||
|
||||
Check the following page: [Installing .NET Core Runtime](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Dotnet.md)
|
||||
|
||||
# CLI
|
||||
|
||||
### How do I use the CLI?
|
||||
|
||||
Check the following page:
|
||||
|
||||
- [Using the CLI](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Getting-started.md#using-the-cli)
|
||||
|
||||
If you're using **Docker**, please refer to the [Docker Usage Instructions](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/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)
|
||||
|
||||
### I can't find Docker exported chats
|
||||
|
||||
Check the following page:
|
||||
|
||||
- [Docker usage instructions](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/docker)
|
||||
|
||||
### 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.
|
||||
|
||||
### Errors
|
||||
|
||||
```
|
||||
DiscordChatExporter.Domain.Exceptions.DiscordChatExporterException: Authentication token is invalid.
|
||||
...
|
||||
```
|
||||
|
||||
↳ Make sure the provided token is correct.
|
||||
|
||||
```
|
||||
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).
|
||||
|
||||
```
|
||||
DiscordChatExporter.Domain.Exceptions.DiscordChatExporterException: Access is forbidden.
|
||||
```
|
||||
|
||||
↳ This means you don't have access to the channel.
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
```
|
||||
System.Net.WebException: Error: TrustFailure ... Invalid certificate received from server.
|
||||
...
|
||||
```
|
||||
|
||||
↳ Try running cert-sync.
|
||||
|
||||
Debian/Ubuntu: `cert-sync /etc/ssl/certs/ca-certificates.crt`
|
||||
|
||||
Red Hat: `cert-sync --user /etc/pki/tls/certs/ca-bundle.crt`
|
||||
|
||||
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).
|
||||
@@ -6,8 +6,8 @@ contact_links:
|
||||
- name: 💬 Discord server
|
||||
url: https://discord.gg/2SUWKFnHSm
|
||||
about: Chat with the project community.
|
||||
- name: 📖 Wiki
|
||||
url: https://github.com/Tyrrrz/DiscordChatExporter/wiki
|
||||
- name: 📖 Documentation
|
||||
url: https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs
|
||||
about: Find usage guides and frequently asked questions.
|
||||
- name: 🗨 Discussions
|
||||
url: https://github.com/Tyrrrz/DiscordChatExporter/discussions/new
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Run tests
|
||||
# Tests need access to secrets, so we can't run them against PRs because of limited trust
|
||||
@@ -26,7 +26,6 @@ jobs:
|
||||
run: dotnet test --configuration Release --logger GitHubActions --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
|
||||
env:
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
DISCORD_TOKEN_BOT: true
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
@@ -49,7 +48,7 @@ jobs:
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Publish (CLI)
|
||||
run: dotnet publish DiscordChatExporter.Cli/ -o DiscordChatExporter.Cli/bin/Publish/ --configuration Release
|
||||
@@ -92,41 +91,19 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Create package (GUI)
|
||||
run: Compress-Archive -Path DiscordChatExporter.Gui/* -DestinationPath DiscordChatExporter.Gui.zip -Force
|
||||
run: Compress-Archive -Path DiscordChatExporter.Gui/* -DestinationPath DiscordChatExporter.zip -Force
|
||||
shell: pwsh
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
files: |
|
||||
DiscordChatExporter.Cli.zip
|
||||
DiscordChatExporter.zip
|
||||
body: |
|
||||
[Changelog](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/Changelog.md)
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Upload release asset (CLI)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_name: DiscordChatExporter.Cli.zip
|
||||
asset_path: DiscordChatExporter.Cli.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload release asset (GUI)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_name: DiscordChatExporter.zip
|
||||
asset_path: DiscordChatExporter.Gui.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
notify:
|
||||
needs: deploy
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
### v2.37.2 (27-Jan-2023)
|
||||
|
||||
- Updated the links inside the application to point to the new documentation site. (Thanks [@CanePlayz](https://github.com/CanePlayz))
|
||||
- [CLI] Fixed an issue that there was no visual indication that an export task has failed before the whole process has completed.
|
||||
- Fixed an issue where the application sometimes crashed when it couldn't establish an HTTP connection to the target server.
|
||||
|
||||
### v2.37.1 (10-Jan-2023)
|
||||
|
||||
- [HTML] Fixed an issue where the export failed because it failed to resolve standard emoji image assets. Switched to a different CDN provider to mitigate this. (Thanks [@Lucas LaBuff](https://github.com/96-LB))
|
||||
- [GUI] Fixed an application crash that happened when the user attempted to close the same dialog multiple times in a very short time.
|
||||
|
||||
### v2.37 (08-Jan-2023)
|
||||
|
||||
- Switched from .NET 6.0 to .NET 7.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/7.0).
|
||||
- Updated rate limit handling to adjust automatically based on the response headers. This may result in exports taking longer than before on bot accounts, but it makes it less likely for Discord to throttle you. Exports with user accounts shouldn't be affected too much by this change.
|
||||
- Optimized snowflake parsing by removing unnecessary checks. (Thanks [@Kuba_Z2](https://github.com/KubaZ2))
|
||||
- [Docker] Optimized the Docker image size by using the Alpine base image and publishing a self-contained distribution. (Thanks [@Velithris](https://github.com/Zireael-N))
|
||||
- Fixed an issue where exporting a channel whose name ends with a dot would fail on Windows with default output path. The dots are now trimmed in file and directory names to match the behavior of Windows Explorer.
|
||||
|
||||
### v2.36.4 (29-Oct-2022)
|
||||
|
||||
- Changed all mentions of "media" in the context of "download media" or "reuse media" to "assets". CLI options will retain their existing names for backwards compatibility.
|
||||
|
||||
+2
-10
@@ -1,8 +1,8 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Version>2.36.4</Version>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>2.37.2</Version>
|
||||
<Company>Tyrrrz</Company>
|
||||
<Copyright>Copyright (c) Oleksii Holub</Copyright>
|
||||
<LangVersion>preview</LangVersion>
|
||||
@@ -10,12 +10,4 @@
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Even though the project builds against .NET 6, some dependencies
|
||||
apparently rely on a specific version of the runtime.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<RuntimeFrameworkVersion>6.0.9</RuntimeFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -11,15 +11,15 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="0.17.1" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.8.0" />
|
||||
<PackageReference Include="AngleSharp" Version="1.0.1" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.1" PrivateAssets="all" />
|
||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" PrivateAssets="all" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
||||
# -- Build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY favicon.ico ./
|
||||
COPY NuGet.config ./
|
||||
@@ -7,16 +9,36 @@ COPY Directory.Build.props ./
|
||||
COPY DiscordChatExporter.Core ./DiscordChatExporter.Core
|
||||
COPY DiscordChatExporter.Cli ./DiscordChatExporter.Cli
|
||||
|
||||
RUN dotnet publish DiscordChatExporter.Cli --configuration Release --output ./publish
|
||||
RUN dotnet publish DiscordChatExporter.Cli \
|
||||
--self-contained \
|
||||
--use-current-runtime \
|
||||
--configuration Release \
|
||||
--output ./publish
|
||||
|
||||
# Run
|
||||
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS run
|
||||
# -- Run
|
||||
FROM mcr.microsoft.com/dotnet/runtime-deps:7.0-alpine
|
||||
|
||||
# Alpine dotnet image doesn't include timezone data, which is needed
|
||||
# for certain date/time operations.
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
# Create a non-root user to run the app, so that the output files
|
||||
# can be accessed by the host.
|
||||
# https://github.com/Tyrrrz/DiscordChatExporter/issues/851
|
||||
RUN adduser \
|
||||
--disabled-password \
|
||||
--no-create-home \
|
||||
dce
|
||||
|
||||
RUN useradd dce
|
||||
USER dce
|
||||
|
||||
COPY --from=build ./publish ./
|
||||
COPY --from=build /build/publish /opt/dce
|
||||
|
||||
WORKDIR ./out
|
||||
# Need to keep this as /out for backwards compatibility with documentation.
|
||||
# A lot of people have this directory mounted in their scripts files, so
|
||||
# changing it would break existing workflows.
|
||||
WORKDIR /out
|
||||
|
||||
ENTRYPOINT ["dotnet", "../DiscordChatExporter.Cli.dll"]
|
||||
# Add the app directory to PATH so that it's easier to debug using a shell
|
||||
ENV PATH="$PATH:/opt/dce"
|
||||
ENTRYPOINT ["DiscordChatExporter.Cli"]
|
||||
|
||||
@@ -31,7 +31,7 @@ public abstract class ExportCommandBase : TokenCommandBase
|
||||
public string OutputPath
|
||||
{
|
||||
get => _outputPath;
|
||||
// Handle ~/ in paths on *nix systems
|
||||
// Handle ~/ in paths on Unix systems
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/pull/903
|
||||
init => _outputPath = Path.GetFullPath(value);
|
||||
}
|
||||
@@ -92,6 +92,12 @@ public abstract class ExportCommandBase : TokenCommandBase
|
||||
)]
|
||||
public string DateFormat { get; init; } = "dd-MMM-yy hh:mm tt";
|
||||
|
||||
[CommandOption(
|
||||
"fuck-russia",
|
||||
Description = "Don't print the Support Ukraine message to the console."
|
||||
)]
|
||||
public bool IsUkraineSupportMessageDisabled { get; init; }
|
||||
|
||||
private ChannelExporter? _channelExporter;
|
||||
protected ChannelExporter Exporter => _channelExporter ??= new ChannelExporter(Discord);
|
||||
|
||||
@@ -144,7 +150,8 @@ public abstract class ExportCommandBase : TokenCommandBase
|
||||
{
|
||||
try
|
||||
{
|
||||
await progressContext.StartTaskAsync($"{channel.Category.Name} / {channel.Name}",
|
||||
await progressContext.StartTaskAsync(
|
||||
$"{channel.Category.Name} / {channel.Name}",
|
||||
async progress =>
|
||||
{
|
||||
var guild = await Discord.GetGuildAsync(channel.GuildId, innerCancellationToken);
|
||||
@@ -234,12 +241,20 @@ public abstract class ExportCommandBase : TokenCommandBase
|
||||
|
||||
public override ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
// War in Ukraine message
|
||||
console.Output.WriteLine("========================================================================");
|
||||
console.Output.WriteLine("|| Ukraine is at war! Support my country in its fight for freedom~ ||");
|
||||
console.Output.WriteLine("|| Learn more on my website: https://tyrrrz.me ||");
|
||||
console.Output.WriteLine("========================================================================");
|
||||
console.Output.WriteLine("");
|
||||
// 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("");
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
@@ -12,11 +12,10 @@ public abstract class TokenCommandBase : ICommand
|
||||
[CommandOption(
|
||||
"token",
|
||||
't',
|
||||
IsRequired = true,
|
||||
EnvironmentVariable = "DISCORD_TOKEN",
|
||||
Description = "Authentication token."
|
||||
)]
|
||||
public string Token { get; init; } = "";
|
||||
public required string Token { get; init; }
|
||||
|
||||
[Obsolete("This option doesn't do anything. Kept for backwards compatibility.")]
|
||||
[CommandOption(
|
||||
|
||||
@@ -15,10 +15,9 @@ public class ExportChannelsCommand : ExportCommandBase
|
||||
[CommandOption(
|
||||
"channel",
|
||||
'c',
|
||||
IsRequired = true,
|
||||
Description = "Channel ID(s)."
|
||||
)]
|
||||
public IReadOnlyList<Snowflake> ChannelIds { get; init; } = Array.Empty<Snowflake>();
|
||||
public required IReadOnlyList<Snowflake> ChannelIds { get; init; }
|
||||
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
|
||||
@@ -15,10 +15,9 @@ public class ExportGuildCommand : ExportCommandBase
|
||||
[CommandOption(
|
||||
"guild",
|
||||
'g',
|
||||
IsRequired = true,
|
||||
Description = "Guild ID."
|
||||
)]
|
||||
public Snowflake GuildId { get; init; }
|
||||
public required Snowflake GuildId { get; init; }
|
||||
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
|
||||
@@ -16,10 +16,9 @@ public class GetChannelsCommand : TokenCommandBase
|
||||
[CommandOption(
|
||||
"guild",
|
||||
'g',
|
||||
IsRequired = true,
|
||||
Description = "Guild ID."
|
||||
)]
|
||||
public Snowflake GuildId { get; init; }
|
||||
public required Snowflake GuildId { get; init; }
|
||||
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
|
||||
@@ -61,11 +61,11 @@ public class GuideCommand : ICommand
|
||||
console.Output.WriteLine(" 6. Copy the first long sequence of numbers inside the URL");
|
||||
console.Output.WriteLine();
|
||||
|
||||
// Wiki link
|
||||
// Docs link
|
||||
using (console.WithForegroundColor(ConsoleColor.White))
|
||||
console.Output.WriteLine("If you have questions or issues, please refer to the wiki:");
|
||||
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/wiki");
|
||||
console.Output.WriteLine("https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs");
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliFx" Version="2.3.0" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.45.0" />
|
||||
<PackageReference Include="CliFx" Version="2.3.1" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.46.0" />
|
||||
<PackageReference Include="Gress" Version="2.0.1" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.3.1" PrivateAssets="all" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.4.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace DiscordChatExporter.Cli;
|
||||
|
||||
public static class Sanctions
|
||||
{
|
||||
[ModuleInitializer]
|
||||
internal static void Verify()
|
||||
{
|
||||
var isSkipped = string.Equals(
|
||||
Environment.GetEnvironmentVariable("RUSNI"),
|
||||
"PYZDA",
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
if (isSkipped)
|
||||
return;
|
||||
|
||||
var locale = CultureInfo.CurrentCulture.Name;
|
||||
|
||||
var region =
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? Registry.CurrentUser
|
||||
.OpenSubKey(@"Control Panel\International\Geo", false)?
|
||||
.GetValue("Name") as string
|
||||
: null;
|
||||
|
||||
var isSanctioned =
|
||||
locale.EndsWith("-ru", StringComparison.OrdinalIgnoreCase) ||
|
||||
locale.EndsWith("-by", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(region, "ru", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(region, "by", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!isSanctioned)
|
||||
return;
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.Error.WriteLine(
|
||||
"You cannot use this software on the territory of a terrorist state. " +
|
||||
"Set the environment variable `RUSNI=PYZDA` if you wish to override this check."
|
||||
);
|
||||
|
||||
Console.ResetColor();
|
||||
|
||||
Environment.Exit(0xFACC);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ internal static class ConsoleExtensions
|
||||
}
|
||||
finally
|
||||
{
|
||||
progressTask.Value = progressTask.MaxValue;
|
||||
progressTask.StopTask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord.Data.Common;
|
||||
|
||||
@@ -39,7 +40,8 @@ public readonly partial record struct FileSize(long TotalBytes)
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public override string ToString() => $"{GetLargestWholeNumberValue():0.##} {GetLargestWholeNumberSymbol()}";
|
||||
public override string ToString() =>
|
||||
string.Create(CultureInfo.InvariantCulture, $"{GetLargestWholeNumberValue():0.##} {GetLargestWholeNumberSymbol()}");
|
||||
}
|
||||
|
||||
public partial record struct FileSize
|
||||
|
||||
@@ -45,7 +45,7 @@ public partial record Emoji
|
||||
: $"https://cdn.discordapp.com/emojis/{id}.png";
|
||||
|
||||
private static string GetImageUrl(string name) =>
|
||||
$"https://twemoji.maxcdn.com/v/latest/svg/{GetTwemojiId(name)}.svg";
|
||||
$"https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/{GetTwemojiId(name)}.svg";
|
||||
|
||||
public static string GetImageUrl(Snowflake? id, string? name, bool isAnimated)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
@@ -35,7 +36,7 @@ public class DiscordClient
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
|
||||
|
||||
// Don't validate because token can have invalid characters
|
||||
// Don't validate because the token can have special characters
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/828
|
||||
request.Headers.TryAddWithoutValidation(
|
||||
"Authorization",
|
||||
@@ -44,11 +45,43 @@ public class DiscordClient
|
||||
: _token
|
||||
);
|
||||
|
||||
return await Http.Client.SendAsync(
|
||||
var response = await Http.Client.SendAsync(
|
||||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
innerCancellationToken
|
||||
);
|
||||
|
||||
// If this was the last request available before hitting the rate limit,
|
||||
// wait out the reset time so that future requests can succeed.
|
||||
// This may add an unnecessary delay in case the user doesn't intend to
|
||||
// make any more requests, but implementing a smarter solution would
|
||||
// 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
|
||||
.TryGetValue("X-RateLimit-Remaining")?
|
||||
.Pipe(s => int.Parse(s, CultureInfo.InvariantCulture));
|
||||
|
||||
var resetAfterDelay = response
|
||||
.Headers
|
||||
.TryGetValue("X-RateLimit-Reset-After")?
|
||||
.Pipe(s => TimeSpan.FromSeconds(double.Parse(s, CultureInfo.InvariantCulture)));
|
||||
|
||||
if (remainingRequestCount <= 0 && resetAfterDelay is not null)
|
||||
{
|
||||
var delay =
|
||||
// Adding a small buffer to the reset time reduces the chance of getting
|
||||
// rate limited again, because it allows for more requests to be released.
|
||||
(resetAfterDelay.Value + TimeSpan.FromSeconds(1))
|
||||
// Sometimes Discord returns an absurdly high value for the reset time, which
|
||||
// is not actually enforced by the server. So we cap it at a reasonable value.
|
||||
.Clamp(TimeSpan.Zero, TimeSpan.FromSeconds(60));
|
||||
|
||||
await Task.Delay(delay, innerCancellationToken);
|
||||
}
|
||||
|
||||
return response;
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Discord;
|
||||
|
||||
@@ -29,7 +28,7 @@ public partial record struct Snowflake
|
||||
return null;
|
||||
|
||||
// As number
|
||||
if (Regex.IsMatch(str, @"^\d+$") && ulong.TryParse(str, NumberStyles.Number, formatProvider, out var value))
|
||||
if (ulong.TryParse(str, NumberStyles.None, formatProvider, out var value))
|
||||
{
|
||||
return new Snowflake(value);
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Gress" Version="2.0.1" />
|
||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||
<PackageReference Include="MiniRazor.CodeGen" Version="2.2.2" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
<PackageReference Include="RazorBlade" Version="0.4.2" />
|
||||
<PackageReference Include="Superpower" Version="3.0.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.12.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.13.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Exporting\Writers\Html\*.cshtml" IsRazorTemplate="true" />
|
||||
<RazorBlade Include="Exporting\Writers\Html\*.cshtml" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -40,45 +40,6 @@ public partial record ExportRequest(
|
||||
|
||||
public partial record ExportRequest
|
||||
{
|
||||
private static string GetOutputBaseFilePath(
|
||||
Guild guild,
|
||||
Channel channel,
|
||||
string outputPath,
|
||||
ExportFormat format,
|
||||
Snowflake? after = null,
|
||||
Snowflake? before = null)
|
||||
{
|
||||
// Formats path
|
||||
outputPath = Regex.Replace(outputPath, "%.", m =>
|
||||
PathEx.EscapeFileName(m.Value switch
|
||||
{
|
||||
"%g" => guild.Id.ToString(),
|
||||
"%G" => guild.Name,
|
||||
"%t" => channel.Category.Id.ToString(),
|
||||
"%T" => channel.Category.Name,
|
||||
"%c" => channel.Id.ToString(),
|
||||
"%C" => channel.Name,
|
||||
"%p" => channel.Position?.ToString() ?? "0",
|
||||
"%P" => channel.Category.Position?.ToString() ?? "0",
|
||||
"%a" => after?.ToDate().ToString("yyyy-MM-dd") ?? "",
|
||||
"%b" => before?.ToDate().ToString("yyyy-MM-dd") ?? "",
|
||||
"%d" => DateTimeOffset.Now.ToString("yyyy-MM-dd"),
|
||||
"%%" => "%",
|
||||
_ => m.Value
|
||||
})
|
||||
);
|
||||
|
||||
// Output is a directory
|
||||
if (Directory.Exists(outputPath) || string.IsNullOrWhiteSpace(Path.GetExtension(outputPath)))
|
||||
{
|
||||
var fileName = GetDefaultOutputFileName(guild, channel, format, after, before);
|
||||
return Path.Combine(outputPath, fileName);
|
||||
}
|
||||
|
||||
// Output is a file
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
public static string GetDefaultOutputFileName(
|
||||
Guild guild,
|
||||
Channel channel,
|
||||
@@ -120,4 +81,45 @@ public partial record ExportRequest
|
||||
|
||||
return PathEx.EscapeFileName(buffer.ToString());
|
||||
}
|
||||
|
||||
private static string GetOutputBaseFilePath(
|
||||
Guild guild,
|
||||
Channel channel,
|
||||
string outputPath,
|
||||
ExportFormat format,
|
||||
Snowflake? after = null,
|
||||
Snowflake? before = null)
|
||||
{
|
||||
// Format path
|
||||
var actualOutputPath = Regex.Replace(
|
||||
outputPath,
|
||||
"%.",
|
||||
m => PathEx.EscapeFileName(m.Value switch
|
||||
{
|
||||
"%g" => guild.Id.ToString(),
|
||||
"%G" => guild.Name,
|
||||
"%t" => channel.Category.Id.ToString(),
|
||||
"%T" => channel.Category.Name,
|
||||
"%c" => channel.Id.ToString(),
|
||||
"%C" => channel.Name,
|
||||
"%p" => channel.Position?.ToString() ?? "0",
|
||||
"%P" => channel.Category.Position?.ToString() ?? "0",
|
||||
"%a" => after?.ToDate().ToString("yyyy-MM-dd") ?? "",
|
||||
"%b" => before?.ToDate().ToString("yyyy-MM-dd") ?? "",
|
||||
"%d" => DateTimeOffset.Now.ToString("yyyy-MM-dd"),
|
||||
"%%" => "%",
|
||||
_ => m.Value
|
||||
})
|
||||
);
|
||||
|
||||
// Output is a directory
|
||||
if (Directory.Exists(actualOutputPath) || string.IsNullOrWhiteSpace(Path.GetExtension(actualOutputPath)))
|
||||
{
|
||||
var fileName = GetDefaultOutputFileName(guild, channel, format, after, before);
|
||||
return Path.Combine(actualOutputPath, fileName);
|
||||
}
|
||||
|
||||
// Output is a file
|
||||
return actualOutputPath;
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,10 @@ internal partial class CsvMessageWriter : MessageWriter
|
||||
_writer = new StreamWriter(stream);
|
||||
}
|
||||
|
||||
private ValueTask<string> FormatMarkdownAsync(string? markdown) =>
|
||||
PlainTextMarkdownVisitor.FormatAsync(Context, markdown ?? "");
|
||||
private ValueTask<string> FormatMarkdownAsync(
|
||||
string markdown,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken);
|
||||
|
||||
public override async ValueTask WritePreambleAsync(CancellationToken cancellationToken = default) =>
|
||||
await _writer.WriteLineAsync("AuthorID,Author,Date,Content,Attachments,Reactions");
|
||||
@@ -84,7 +86,7 @@ internal partial class CsvMessageWriter : MessageWriter
|
||||
await _writer.WriteAsync(',');
|
||||
|
||||
// Message content
|
||||
await _writer.WriteAsync(CsvEncode(await FormatMarkdownAsync(message.Content)));
|
||||
await _writer.WriteAsync(CsvEncode(await FormatMarkdownAsync(message.Content, cancellationToken)));
|
||||
await _writer.WriteAsync(',');
|
||||
|
||||
// Attachments
|
||||
|
||||
@@ -1,43 +1,52 @@
|
||||
@using System
|
||||
@using System.Collections.Generic
|
||||
@using System.Linq
|
||||
@using System.Threading
|
||||
@using System.Threading.Tasks
|
||||
@using DiscordChatExporter.Core.Discord.Data
|
||||
@using DiscordChatExporter.Core.Discord.Data.Embeds
|
||||
@using DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||
@using DiscordChatExporter.Core.Exporting
|
||||
@using DiscordChatExporter.Core.Exporting.Writers.Html
|
||||
@using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
||||
@using DiscordChatExporter.Core.Utils.Extensions
|
||||
|
||||
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||
@inherits MiniRazor.TemplateBase<MessageGroupTemplateContext>
|
||||
@inherits RazorBlade.HtmlTemplate
|
||||
|
||||
@functions {
|
||||
public required ExportContext ExportContext { get; init; }
|
||||
|
||||
public required IReadOnlyList<Message> Messages { get; init; }
|
||||
}
|
||||
|
||||
@{
|
||||
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
||||
Model.ExportContext.ResolveAssetUrlAsync(url);
|
||||
ExportContext.ResolveAssetUrlAsync(url, CancellationToken);
|
||||
|
||||
string FormatDate(DateTimeOffset date) =>
|
||||
Model.ExportContext.FormatDate(date);
|
||||
ExportContext.FormatDate(date);
|
||||
|
||||
ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
||||
Model.FormatMarkdownAsync(markdown);
|
||||
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken);
|
||||
|
||||
ValueTask<string> FormatEmbedMarkdownAsync(string markdown) =>
|
||||
Model.FormatMarkdownAsync(markdown, false);
|
||||
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, false, CancellationToken);
|
||||
|
||||
var firstMessage = Model.Messages.First();
|
||||
var firstMessage = Messages.First();
|
||||
|
||||
var userMember = Model.ExportContext.TryGetMember(firstMessage.Author.Id);
|
||||
var userMember = ExportContext.TryGetMember(firstMessage.Author.Id);
|
||||
|
||||
var userColor = Model.ExportContext.TryGetUserColor(firstMessage.Author.Id);
|
||||
var userColor = ExportContext.TryGetUserColor(firstMessage.Author.Id);
|
||||
|
||||
var userNick = firstMessage.Author.IsBot
|
||||
? firstMessage.Author.Name
|
||||
: userMember?.Nick ?? firstMessage.Author.Name;
|
||||
|
||||
var referencedUserMember = firstMessage.ReferencedMessage is not null
|
||||
? Model.ExportContext.TryGetMember(firstMessage.ReferencedMessage.Author.Id)
|
||||
? ExportContext.TryGetMember(firstMessage.ReferencedMessage.Author.Id)
|
||||
: null;
|
||||
|
||||
var referencedUserColor = firstMessage.ReferencedMessage is not null
|
||||
? Model.ExportContext.TryGetUserColor(firstMessage.ReferencedMessage.Author.Id)
|
||||
? ExportContext.TryGetUserColor(firstMessage.ReferencedMessage.Author.Id)
|
||||
: null;
|
||||
|
||||
var referencedUserNick = firstMessage.ReferencedMessage is not null
|
||||
@@ -48,7 +57,7 @@
|
||||
}
|
||||
|
||||
<div class="chatlog__message-group">
|
||||
@foreach (var (message, i) in Model.Messages.WithIndex())
|
||||
@foreach (var (message, i) in Messages.WithIndex())
|
||||
{
|
||||
var isFirst = i == 0;
|
||||
|
||||
@@ -59,8 +68,8 @@
|
||||
{
|
||||
// System notifications are grouped even if the message author is different.
|
||||
// That's why we have to update the user values with the author of the current message.
|
||||
userMember = Model.ExportContext.TryGetMember(message.Author.Id);
|
||||
userColor = Model.ExportContext.TryGetUserColor(message.Author.Id);
|
||||
userMember = ExportContext.TryGetMember(message.Author.Id);
|
||||
userColor = ExportContext.TryGetUserColor(message.Author.Id);
|
||||
userNick = message.Author.IsBot
|
||||
? message.Author.Name
|
||||
: userMember?.Nick ?? message.Author.Name;
|
||||
@@ -129,7 +138,7 @@
|
||||
<span class="chatlog__reference-link" onclick="scrollToMessage(event, '@message.ReferencedMessage.Id')">
|
||||
@if (!string.IsNullOrWhiteSpace(message.ReferencedMessage.Content) && !message.ReferencedMessage.IsContentHidden())
|
||||
{
|
||||
<!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(message.ReferencedMessage.Content))<!--/wmm:ignore-->
|
||||
<!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(message.ReferencedMessage.Content))<!--/wmm:ignore-->
|
||||
}
|
||||
else if (message.ReferencedMessage.Attachments.Any() || message.ReferencedMessage.Embeds.Any())
|
||||
{
|
||||
@@ -180,7 +189,7 @@
|
||||
@{/* Text */}
|
||||
@if (!string.IsNullOrWhiteSpace(message.Content) && !message.IsContentHidden())
|
||||
{
|
||||
<span class="chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatMarkdownAsync(message.Content))<!--/wmm:ignore--></span>
|
||||
<span class="chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatMarkdownAsync(message.Content))<!--/wmm:ignore--></span>
|
||||
}
|
||||
|
||||
@{/* Edited timestamp */}
|
||||
@@ -300,12 +309,12 @@
|
||||
@if (!string.IsNullOrWhiteSpace(embed.Url))
|
||||
{
|
||||
<a class="chatlog__embed-title-link" href="@embed.Url">
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -386,12 +395,12 @@
|
||||
@if (!string.IsNullOrWhiteSpace(embed.Url))
|
||||
{
|
||||
<a class="chatlog__embed-title-link" href="@embed.Url">
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -400,7 +409,7 @@
|
||||
@if (!string.IsNullOrWhiteSpace(embed.Description))
|
||||
{
|
||||
<div class="chatlog__embed-description">
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(embed.Description))<!--/wmm:ignore--></div>
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Description))<!--/wmm:ignore--></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -414,14 +423,14 @@
|
||||
@if (!string.IsNullOrWhiteSpace(field.Name))
|
||||
{
|
||||
<div class="chatlog__embed-field-name">
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(field.Name))<!--/wmm:ignore--></div>
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(field.Name))<!--/wmm:ignore--></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(field.Value))
|
||||
{
|
||||
<div class="chatlog__embed-field-value">
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(field.Value))<!--/wmm:ignore--></div>
|
||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(field.Value))<!--/wmm:ignore--></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||
|
||||
internal class MessageGroupTemplateContext
|
||||
{
|
||||
public ExportContext ExportContext { get; }
|
||||
|
||||
public IReadOnlyList<Message> Messages { get; }
|
||||
|
||||
public MessageGroupTemplateContext(ExportContext exportContext, IReadOnlyList<Message> messages)
|
||||
{
|
||||
ExportContext = exportContext;
|
||||
Messages = messages;
|
||||
}
|
||||
|
||||
public ValueTask<string> FormatMarkdownAsync(string? markdown, bool isJumboAllowed = true) =>
|
||||
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown ?? "", isJumboAllowed);
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
@using DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||
@using System.Threading
|
||||
@using DiscordChatExporter.Core.Exporting
|
||||
|
||||
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||
@inherits MiniRazor.TemplateBase<PostambleTemplateContext>
|
||||
@inherits RazorBlade.HtmlTemplate
|
||||
|
||||
@{/* Close elements opened by preamble */}
|
||||
@functions {
|
||||
public required ExportContext ExportContext { get; init; }
|
||||
|
||||
public required long MessagesWritten { get; init; }
|
||||
}
|
||||
|
||||
@{
|
||||
/* Close elements opened by preamble */
|
||||
}
|
||||
<!--wmm:ignore-->
|
||||
</div>
|
||||
<!--/wmm:ignore-->
|
||||
|
||||
<div class="postamble">
|
||||
<div class="postamble__entry">Exported @Model.MessagesWritten.ToString("N0") message(s)</div>
|
||||
<div class="postamble__entry">Exported @MessagesWritten.ToString("N0") message(s)</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||
|
||||
internal class PostambleTemplateContext
|
||||
{
|
||||
public ExportContext ExportContext { get; }
|
||||
|
||||
public long MessagesWritten { get; }
|
||||
|
||||
public PostambleTemplateContext(ExportContext exportContext, long messagesWritten)
|
||||
{
|
||||
ExportContext = exportContext;
|
||||
MessagesWritten = messagesWritten;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,20 @@
|
||||
@using System
|
||||
@using System.Threading
|
||||
@using System.Threading.Tasks
|
||||
@using DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||
@using DiscordChatExporter.Core.Exporting
|
||||
@using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors
|
||||
|
||||
@namespace DiscordChatExporter.Core.Exporting.Writers.Html
|
||||
@inherits MiniRazor.TemplateBase<PreambleTemplateContext>
|
||||
@inherits RazorBlade.HtmlTemplate
|
||||
|
||||
@functions {
|
||||
public required ExportContext ExportContext { get; init; }
|
||||
|
||||
public required string ThemeName { get; init; }
|
||||
}
|
||||
|
||||
@{
|
||||
string Themed(string darkVariant, string lightVariant) =>
|
||||
string.Equals(Model.ThemeName, "Dark", StringComparison.OrdinalIgnoreCase)
|
||||
string.Equals(ThemeName, "Dark", StringComparison.OrdinalIgnoreCase)
|
||||
? darkVariant
|
||||
: lightVariant;
|
||||
|
||||
@@ -15,20 +22,20 @@
|
||||
$"https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-{weight}.woff";
|
||||
|
||||
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
||||
Model.ExportContext.ResolveAssetUrlAsync(url, CancellationToken);
|
||||
ExportContext.ResolveAssetUrlAsync(url, CancellationToken);
|
||||
|
||||
string FormatDate(DateTimeOffset date) =>
|
||||
Model.ExportContext.FormatDate(date);
|
||||
ExportContext.FormatDate(date);
|
||||
|
||||
ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
||||
Model.FormatMarkdownAsync(markdown);
|
||||
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown, true, CancellationToken);
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>@Model.ExportContext.Request.Guild.Name - @Model.ExportContext.Request.Channel.Name</title>
|
||||
<title>@ExportContext.Request.Guild.Name - @ExportContext.Request.Channel.Name</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
@@ -755,7 +762,7 @@
|
||||
</style>
|
||||
|
||||
@{/* Syntax highlighting */}
|
||||
<link rel="stylesheet" href="@await ResolveAssetUrlAsync($"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/solarized-{Model.ThemeName.ToLowerInvariant()}.min.css")">
|
||||
<link rel="stylesheet" href="@await ResolveAssetUrlAsync($"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/solarized-{ThemeName.ToLowerInvariant()}.min.css")">
|
||||
<script src="@await ResolveAssetUrlAsync("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js")"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -846,31 +853,31 @@
|
||||
|
||||
<div class="preamble">
|
||||
<div class="preamble__guild-icon-container">
|
||||
<img class="preamble__guild-icon" src="@await ResolveAssetUrlAsync(Model.ExportContext.Request.Guild.IconUrl)" alt="Guild icon" loading="lazy">
|
||||
<img class="preamble__guild-icon" src="@await ResolveAssetUrlAsync(ExportContext.Request.Guild.IconUrl)" alt="Guild icon" loading="lazy">
|
||||
</div>
|
||||
<div class="preamble__entries-container">
|
||||
<div class="preamble__entry">@Model.ExportContext.Request.Guild.Name</div>
|
||||
<div class="preamble__entry">@Model.ExportContext.Request.Channel.Category.Name / @Model.ExportContext.Request.Channel.Name</div>
|
||||
<div class="preamble__entry">@ExportContext.Request.Guild.Name</div>
|
||||
<div class="preamble__entry">@ExportContext.Request.Channel.Category.Name / @ExportContext.Request.Channel.Name</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ExportContext.Request.Channel.Topic))
|
||||
@if (!string.IsNullOrWhiteSpace(ExportContext.Request.Channel.Topic))
|
||||
{
|
||||
<div class="preamble__entry preamble__entry--small">@Raw(await FormatMarkdownAsync(Model.ExportContext.Request.Channel.Topic))</div>
|
||||
<div class="preamble__entry preamble__entry--small">@Html.Raw(await FormatMarkdownAsync(ExportContext.Request.Channel.Topic))</div>
|
||||
}
|
||||
|
||||
@if (Model.ExportContext.Request.After is not null || Model.ExportContext.Request.Before is not null)
|
||||
@if (ExportContext.Request.After is not null || ExportContext.Request.Before is not null)
|
||||
{
|
||||
<div class="preamble__entry preamble__entry--small">
|
||||
@if (Model.ExportContext.Request.After is not null && Model.ExportContext.Request.Before is not null)
|
||||
@if (ExportContext.Request.After is not null && ExportContext.Request.Before is not null)
|
||||
{
|
||||
@($"Between {FormatDate(Model.ExportContext.Request.After.Value.ToDate())} and {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}")
|
||||
@($"Between {FormatDate(ExportContext.Request.After.Value.ToDate())} and {FormatDate(ExportContext.Request.Before.Value.ToDate())}")
|
||||
}
|
||||
else if (Model.ExportContext.Request.After is not null)
|
||||
else if (ExportContext.Request.After is not null)
|
||||
{
|
||||
@($"After {FormatDate(Model.ExportContext.Request.After.Value.ToDate())}")
|
||||
@($"After {FormatDate(ExportContext.Request.After.Value.ToDate())}")
|
||||
}
|
||||
else if (Model.ExportContext.Request.Before is not null)
|
||||
else if (ExportContext.Request.Before is not null)
|
||||
{
|
||||
@($"Before {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}")
|
||||
@($"Before {FormatDate(ExportContext.Request.Before.Value.ToDate())}")
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
|
||||
|
||||
namespace DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||
|
||||
internal class PreambleTemplateContext
|
||||
{
|
||||
public ExportContext ExportContext { get; }
|
||||
|
||||
public string ThemeName { get; }
|
||||
|
||||
public PreambleTemplateContext(ExportContext exportContext, string themeName)
|
||||
{
|
||||
ExportContext = exportContext;
|
||||
ThemeName = themeName;
|
||||
}
|
||||
|
||||
public ValueTask<string> FormatMarkdownAsync(string? markdown, bool isJumboAllowed = true) =>
|
||||
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown ?? "", isJumboAllowed);
|
||||
}
|
||||
@@ -71,15 +71,16 @@ internal class HtmlMessageWriter : MessageWriter
|
||||
.Minify(html, false)
|
||||
.MinifiedContent;
|
||||
|
||||
public override async ValueTask WritePreambleAsync(CancellationToken cancellationToken = default)
|
||||
public override async ValueTask WritePreambleAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var templateContext = new PreambleTemplateContext(Context, _themeName);
|
||||
|
||||
// We are not writing directly to output because Razor
|
||||
// does not actually do asynchronous writes to stream.
|
||||
await _writer.WriteLineAsync(
|
||||
Minify(
|
||||
await PreambleTemplate.RenderAsync(templateContext, cancellationToken)
|
||||
await new PreambleTemplate
|
||||
{
|
||||
ExportContext = Context,
|
||||
ThemeName = _themeName
|
||||
}.RenderAsync(cancellationToken)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -88,13 +89,13 @@ internal class HtmlMessageWriter : MessageWriter
|
||||
IReadOnlyList<Message> messages,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var templateContext = new MessageGroupTemplateContext(Context, messages);
|
||||
|
||||
// We are not writing directly to output because Razor
|
||||
// does not actually do asynchronous writes to stream.
|
||||
await _writer.WriteLineAsync(
|
||||
Minify(
|
||||
await MessageGroupTemplate.RenderAsync(templateContext, cancellationToken)
|
||||
await new MessageGroupTemplate
|
||||
{
|
||||
ExportContext = Context,
|
||||
Messages = messages
|
||||
}.RenderAsync(cancellationToken)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -126,13 +127,13 @@ internal class HtmlMessageWriter : MessageWriter
|
||||
if (_messageGroup.Any())
|
||||
await WriteMessageGroupAsync(_messageGroup, cancellationToken);
|
||||
|
||||
var templateContext = new PostambleTemplateContext(Context, MessagesWritten);
|
||||
|
||||
// We are not writing directly to output because Razor
|
||||
// does not actually do asynchronous writes to stream.
|
||||
await _writer.WriteLineAsync(
|
||||
Minify(
|
||||
await PostambleTemplate.RenderAsync(templateContext, cancellationToken)
|
||||
await new PostambleTemplate
|
||||
{
|
||||
ExportContext = Context,
|
||||
MessagesWritten = MessagesWritten
|
||||
}.RenderAsync(cancellationToken)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,8 +29,10 @@ internal class JsonMessageWriter : MessageWriter
|
||||
});
|
||||
}
|
||||
|
||||
private ValueTask<string> FormatMarkdownAsync(string? markdown) =>
|
||||
PlainTextMarkdownVisitor.FormatAsync(Context, markdown ?? "");
|
||||
private ValueTask<string> FormatMarkdownAsync(
|
||||
string markdown,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken);
|
||||
|
||||
private async ValueTask WriteAttachmentAsync(
|
||||
Attachment attachment,
|
||||
@@ -115,8 +117,8 @@ internal class JsonMessageWriter : MessageWriter
|
||||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
_writer.WriteString("name", await FormatMarkdownAsync(embedField.Name));
|
||||
_writer.WriteString("value", await FormatMarkdownAsync(embedField.Value));
|
||||
_writer.WriteString("name", await FormatMarkdownAsync(embedField.Name, cancellationToken));
|
||||
_writer.WriteString("value", await FormatMarkdownAsync(embedField.Value, cancellationToken));
|
||||
_writer.WriteBoolean("isInline", embedField.IsInline);
|
||||
|
||||
_writer.WriteEndObject();
|
||||
@@ -129,10 +131,10 @@ internal class JsonMessageWriter : MessageWriter
|
||||
{
|
||||
_writer.WriteStartObject();
|
||||
|
||||
_writer.WriteString("title", await FormatMarkdownAsync(embed.Title));
|
||||
_writer.WriteString("title", await FormatMarkdownAsync(embed.Title ?? "", cancellationToken));
|
||||
_writer.WriteString("url", embed.Url);
|
||||
_writer.WriteString("timestamp", embed.Timestamp);
|
||||
_writer.WriteString("description", await FormatMarkdownAsync(embed.Description));
|
||||
_writer.WriteString("description", await FormatMarkdownAsync(embed.Description ?? "", cancellationToken));
|
||||
|
||||
if (embed.Color is not null)
|
||||
_writer.WriteString("color", embed.Color.Value.ToHex());
|
||||
@@ -283,7 +285,7 @@ internal class JsonMessageWriter : MessageWriter
|
||||
_writer.WriteBoolean("isPinned", message.IsPinned);
|
||||
|
||||
// Content
|
||||
_writer.WriteString("content", await FormatMarkdownAsync(message.Content));
|
||||
_writer.WriteString("content", await FormatMarkdownAsync(message.Content, cancellationToken));
|
||||
|
||||
// Author
|
||||
_writer.WriteStartObject("author");
|
||||
|
||||
+38
-19
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Core.Markdown;
|
||||
@@ -24,13 +25,17 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||
_isJumbo = isJumbo;
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitTextAsync(TextNode text)
|
||||
protected override async ValueTask<MarkdownNode> VisitTextAsync(
|
||||
TextNode text,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_buffer.Append(HtmlEncode(text.Text));
|
||||
return await base.VisitTextAsync(text);
|
||||
return await base.VisitTextAsync(text, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitFormattingAsync(FormattingNode formatting)
|
||||
protected override async ValueTask<MarkdownNode> VisitFormattingAsync(
|
||||
FormattingNode formatting,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var (openingTag, closingTag) = formatting.Kind switch
|
||||
{
|
||||
@@ -68,23 +73,27 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||
};
|
||||
|
||||
_buffer.Append(openingTag);
|
||||
var result = await base.VisitFormattingAsync(formatting);
|
||||
var result = await base.VisitFormattingAsync(formatting, cancellationToken);
|
||||
_buffer.Append(closingTag);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitInlineCodeBlockAsync(InlineCodeBlockNode inlineCodeBlock)
|
||||
protected override async ValueTask<MarkdownNode> VisitInlineCodeBlockAsync(
|
||||
InlineCodeBlockNode inlineCodeBlock,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_buffer
|
||||
.Append("<code class=\"chatlog__markdown-pre chatlog__markdown-pre--inline\">")
|
||||
.Append(HtmlEncode(inlineCodeBlock.Code))
|
||||
.Append("</code>");
|
||||
|
||||
return await base.VisitInlineCodeBlockAsync(inlineCodeBlock);
|
||||
return await base.VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitMultiLineCodeBlockAsync(MultiLineCodeBlockNode multiLineCodeBlock)
|
||||
protected override async ValueTask<MarkdownNode> VisitMultiLineCodeBlockAsync(
|
||||
MultiLineCodeBlockNode multiLineCodeBlock,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var highlightCssClass = !string.IsNullOrWhiteSpace(multiLineCodeBlock.Language)
|
||||
? $"language-{multiLineCodeBlock.Language}"
|
||||
@@ -95,10 +104,12 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||
.Append(HtmlEncode(multiLineCodeBlock.Code))
|
||||
.Append("</code>");
|
||||
|
||||
return await base.VisitMultiLineCodeBlockAsync(multiLineCodeBlock);
|
||||
return await base.VisitMultiLineCodeBlockAsync(multiLineCodeBlock, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitLinkAsync(LinkNode link)
|
||||
protected override async ValueTask<MarkdownNode> VisitLinkAsync(
|
||||
LinkNode link,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Try to extract message ID if the link refers to a Discord message
|
||||
var linkedMessageId = Regex.Match(
|
||||
@@ -112,13 +123,15 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||
: $"<a href=\"{HtmlEncode(link.Url)}\">"
|
||||
);
|
||||
|
||||
var result = await base.VisitLinkAsync(link);
|
||||
var result = await base.VisitLinkAsync(link, cancellationToken);
|
||||
_buffer.Append("</a>");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitEmojiAsync(EmojiNode emoji)
|
||||
protected override async ValueTask<MarkdownNode> VisitEmojiAsync(
|
||||
EmojiNode emoji,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated);
|
||||
var jumboClass = _isJumbo ? "chatlog__emoji--large" : "";
|
||||
@@ -129,14 +142,16 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||
$"class=\"chatlog__emoji {jumboClass}\" " +
|
||||
$"alt=\"{emoji.Name}\" " +
|
||||
$"title=\"{emoji.Code}\" " +
|
||||
$"src=\"{await _context.ResolveAssetUrlAsync(emojiImageUrl)}\"" +
|
||||
$"src=\"{await _context.ResolveAssetUrlAsync(emojiImageUrl, cancellationToken)}\"" +
|
||||
$">"
|
||||
);
|
||||
|
||||
return await base.VisitEmojiAsync(emoji);
|
||||
return await base.VisitEmojiAsync(emoji, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitMentionAsync(MentionNode mention)
|
||||
protected override async ValueTask<MarkdownNode> VisitMentionAsync(
|
||||
MentionNode mention,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (mention.Kind == MentionKind.Everyone)
|
||||
{
|
||||
@@ -191,10 +206,12 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||
.Append("</span>");
|
||||
}
|
||||
|
||||
return await base.VisitMentionAsync(mention);
|
||||
return await base.VisitMentionAsync(mention, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitUnixTimestampAsync(UnixTimestampNode timestamp)
|
||||
protected override async ValueTask<MarkdownNode> VisitUnixTimestampAsync(
|
||||
UnixTimestampNode timestamp,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dateString = timestamp.Date is not null
|
||||
? _context.FormatDate(timestamp.Date.Value)
|
||||
@@ -210,7 +227,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||
.Append(HtmlEncode(dateString))
|
||||
.Append("</span>");
|
||||
|
||||
return await base.VisitUnixTimestampAsync(timestamp);
|
||||
return await base.VisitUnixTimestampAsync(timestamp, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +238,8 @@ internal partial class HtmlMarkdownVisitor
|
||||
public static async ValueTask<string> FormatAsync(
|
||||
ExportContext context,
|
||||
string markdown,
|
||||
bool isJumboAllowed = true)
|
||||
bool isJumboAllowed = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var nodes = MarkdownParser.Parse(markdown);
|
||||
|
||||
@@ -231,7 +249,8 @@ internal partial class HtmlMarkdownVisitor
|
||||
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
await new HtmlMarkdownVisitor(context, buffer, isJumbo).VisitAsync(nodes);
|
||||
await new HtmlMarkdownVisitor(context, buffer, isJumbo)
|
||||
.VisitAsync(nodes, cancellationToken);
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
+23
-10
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Markdown;
|
||||
using DiscordChatExporter.Core.Markdown.Parsing;
|
||||
@@ -17,13 +18,17 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
||||
_buffer = buffer;
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitTextAsync(TextNode text)
|
||||
protected override async ValueTask<MarkdownNode> VisitTextAsync(
|
||||
TextNode text,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_buffer.Append(text.Text);
|
||||
return await base.VisitTextAsync(text);
|
||||
return await base.VisitTextAsync(text, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitEmojiAsync(EmojiNode emoji)
|
||||
protected override async ValueTask<MarkdownNode> VisitEmojiAsync(
|
||||
EmojiNode emoji,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_buffer.Append(
|
||||
emoji.IsCustomEmoji
|
||||
@@ -31,10 +36,12 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
||||
: emoji.Name
|
||||
);
|
||||
|
||||
return await base.VisitEmojiAsync(emoji);
|
||||
return await base.VisitEmojiAsync(emoji, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitMentionAsync(MentionNode mention)
|
||||
protected override async ValueTask<MarkdownNode> VisitMentionAsync(
|
||||
MentionNode mention,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (mention.Kind == MentionKind.Everyone)
|
||||
{
|
||||
@@ -70,10 +77,12 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
||||
_buffer.Append($"@{name}");
|
||||
}
|
||||
|
||||
return await base.VisitMentionAsync(mention);
|
||||
return await base.VisitMentionAsync(mention, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async ValueTask<MarkdownNode> VisitUnixTimestampAsync(UnixTimestampNode timestamp)
|
||||
protected override async ValueTask<MarkdownNode> VisitUnixTimestampAsync(
|
||||
UnixTimestampNode timestamp,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_buffer.Append(
|
||||
timestamp.Date is not null
|
||||
@@ -81,18 +90,22 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
||||
: "Invalid date"
|
||||
);
|
||||
|
||||
return await base.VisitUnixTimestampAsync(timestamp);
|
||||
return await base.VisitUnixTimestampAsync(timestamp, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class PlainTextMarkdownVisitor
|
||||
{
|
||||
public static async ValueTask<string> FormatAsync(ExportContext context, string markdown)
|
||||
public static async ValueTask<string> FormatAsync(
|
||||
ExportContext context,
|
||||
string markdown,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var nodes = MarkdownParser.ParseMinimal(markdown);
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
await new PlainTextMarkdownVisitor(context, buffer).VisitAsync(nodes);
|
||||
await new PlainTextMarkdownVisitor(context, buffer)
|
||||
.VisitAsync(nodes, cancellationToken);
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@ internal class PlainTextMessageWriter : MessageWriter
|
||||
_writer = new StreamWriter(stream);
|
||||
}
|
||||
|
||||
private ValueTask<string> FormatMarkdownAsync(string? markdown) =>
|
||||
PlainTextMarkdownVisitor.FormatAsync(Context, markdown ?? "");
|
||||
private ValueTask<string> FormatMarkdownAsync(
|
||||
string markdown,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
PlainTextMarkdownVisitor.FormatAsync(Context, markdown, cancellationToken);
|
||||
|
||||
private async ValueTask WriteMessageHeaderAsync(Message message)
|
||||
{
|
||||
@@ -48,7 +50,9 @@ internal class PlainTextMessageWriter : MessageWriter
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await _writer.WriteLineAsync(await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken));
|
||||
await _writer.WriteLineAsync(
|
||||
await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken)
|
||||
);
|
||||
}
|
||||
|
||||
await _writer.WriteLineAsync();
|
||||
@@ -65,24 +69,44 @@ internal class PlainTextMessageWriter : MessageWriter
|
||||
await _writer.WriteLineAsync("{Embed}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(embed.Author?.Name))
|
||||
{
|
||||
await _writer.WriteLineAsync(embed.Author.Name);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(embed.Url))
|
||||
{
|
||||
await _writer.WriteLineAsync(embed.Url);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(embed.Title))
|
||||
await _writer.WriteLineAsync(await FormatMarkdownAsync(embed.Title));
|
||||
{
|
||||
await _writer.WriteLineAsync(
|
||||
await FormatMarkdownAsync(embed.Title, cancellationToken)
|
||||
);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(embed.Description))
|
||||
await _writer.WriteLineAsync(await FormatMarkdownAsync(embed.Description));
|
||||
{
|
||||
await _writer.WriteLineAsync(
|
||||
await FormatMarkdownAsync(embed.Description, cancellationToken)
|
||||
);
|
||||
}
|
||||
|
||||
foreach (var field in embed.Fields)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(field.Name))
|
||||
await _writer.WriteLineAsync(await FormatMarkdownAsync(field.Name));
|
||||
{
|
||||
await _writer.WriteLineAsync(
|
||||
await FormatMarkdownAsync(field.Name, cancellationToken)
|
||||
);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(field.Value))
|
||||
await _writer.WriteLineAsync(await FormatMarkdownAsync(field.Value));
|
||||
{
|
||||
await _writer.WriteLineAsync(
|
||||
await FormatMarkdownAsync(field.Value, cancellationToken)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(embed.Thumbnail?.Url))
|
||||
@@ -109,7 +133,9 @@ internal class PlainTextMessageWriter : MessageWriter
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(embed.Footer?.Text))
|
||||
{
|
||||
await _writer.WriteLineAsync(embed.Footer.Text);
|
||||
}
|
||||
|
||||
await _writer.WriteLineAsync();
|
||||
}
|
||||
@@ -152,7 +178,9 @@ internal class PlainTextMessageWriter : MessageWriter
|
||||
await _writer.WriteAsync(reaction.Emoji.Name);
|
||||
|
||||
if (reaction.Count > 1)
|
||||
{
|
||||
await _writer.WriteAsync($" ({reaction.Count})");
|
||||
}
|
||||
|
||||
await _writer.WriteAsync(' ');
|
||||
}
|
||||
@@ -167,13 +195,19 @@ internal class PlainTextMessageWriter : MessageWriter
|
||||
await _writer.WriteLineAsync($"Channel: {Context.Request.Channel.Category.Name} / {Context.Request.Channel.Name}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Context.Request.Channel.Topic))
|
||||
{
|
||||
await _writer.WriteLineAsync($"Topic: {Context.Request.Channel.Topic}");
|
||||
}
|
||||
|
||||
if (Context.Request.After is not null)
|
||||
{
|
||||
await _writer.WriteLineAsync($"After: {Context.FormatDate(Context.Request.After.Value.ToDate())}");
|
||||
}
|
||||
|
||||
if (Context.Request.Before is not null)
|
||||
{
|
||||
await _writer.WriteLineAsync($"Before: {Context.FormatDate(Context.Request.Before.Value.ToDate())}");
|
||||
}
|
||||
|
||||
await _writer.WriteLineAsync(new string('=', 62));
|
||||
await _writer.WriteLineAsync();
|
||||
@@ -190,7 +224,11 @@ internal class PlainTextMessageWriter : MessageWriter
|
||||
|
||||
// Content
|
||||
if (!string.IsNullOrWhiteSpace(message.Content))
|
||||
await _writer.WriteLineAsync(await FormatMarkdownAsync(message.Content));
|
||||
{
|
||||
await _writer.WriteLineAsync(
|
||||
await FormatMarkdownAsync(message.Content, cancellationToken)
|
||||
);
|
||||
}
|
||||
|
||||
await _writer.WriteLineAsync();
|
||||
|
||||
|
||||
@@ -1,57 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DiscordChatExporter.Core.Markdown.Parsing;
|
||||
|
||||
internal abstract class MarkdownVisitor
|
||||
{
|
||||
protected virtual ValueTask<MarkdownNode> VisitTextAsync(TextNode text) =>
|
||||
protected virtual ValueTask<MarkdownNode> VisitTextAsync(
|
||||
TextNode text,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new(text);
|
||||
|
||||
protected virtual async ValueTask<MarkdownNode> VisitFormattingAsync(FormattingNode formatting)
|
||||
protected virtual async ValueTask<MarkdownNode> VisitFormattingAsync(
|
||||
FormattingNode formatting,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await VisitAsync(formatting.Children);
|
||||
await VisitAsync(formatting.Children, cancellationToken);
|
||||
return formatting;
|
||||
}
|
||||
|
||||
protected virtual ValueTask<MarkdownNode> VisitInlineCodeBlockAsync(InlineCodeBlockNode inlineCodeBlock) =>
|
||||
protected virtual ValueTask<MarkdownNode> VisitInlineCodeBlockAsync(
|
||||
InlineCodeBlockNode inlineCodeBlock,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new(inlineCodeBlock);
|
||||
|
||||
protected virtual ValueTask<MarkdownNode> VisitMultiLineCodeBlockAsync(MultiLineCodeBlockNode multiLineCodeBlock) =>
|
||||
protected virtual ValueTask<MarkdownNode> VisitMultiLineCodeBlockAsync(
|
||||
MultiLineCodeBlockNode multiLineCodeBlock,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new(multiLineCodeBlock);
|
||||
|
||||
protected virtual async ValueTask<MarkdownNode> VisitLinkAsync(LinkNode link)
|
||||
protected virtual async ValueTask<MarkdownNode> VisitLinkAsync(
|
||||
LinkNode link,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await VisitAsync(link.Children);
|
||||
await VisitAsync(link.Children, cancellationToken);
|
||||
return link;
|
||||
}
|
||||
|
||||
protected virtual ValueTask<MarkdownNode> VisitEmojiAsync(EmojiNode emoji) =>
|
||||
protected virtual ValueTask<MarkdownNode> VisitEmojiAsync(
|
||||
EmojiNode emoji,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new(emoji);
|
||||
|
||||
protected virtual ValueTask<MarkdownNode> VisitMentionAsync(MentionNode mention) =>
|
||||
protected virtual ValueTask<MarkdownNode> VisitMentionAsync(
|
||||
MentionNode mention,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new(mention);
|
||||
|
||||
protected virtual ValueTask<MarkdownNode> VisitUnixTimestampAsync(UnixTimestampNode timestamp) =>
|
||||
protected virtual ValueTask<MarkdownNode> VisitUnixTimestampAsync(
|
||||
UnixTimestampNode timestamp,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new(timestamp);
|
||||
|
||||
public async ValueTask<MarkdownNode> VisitAsync(MarkdownNode node) => node switch
|
||||
{
|
||||
TextNode text => await VisitTextAsync(text),
|
||||
FormattingNode formatting => await VisitFormattingAsync(formatting),
|
||||
InlineCodeBlockNode inlineCodeBlock => await VisitInlineCodeBlockAsync(inlineCodeBlock),
|
||||
MultiLineCodeBlockNode multiLineCodeBlock => await VisitMultiLineCodeBlockAsync(multiLineCodeBlock),
|
||||
LinkNode link => await VisitLinkAsync(link),
|
||||
EmojiNode emoji => await VisitEmojiAsync(emoji),
|
||||
MentionNode mention => await VisitMentionAsync(mention),
|
||||
UnixTimestampNode timestamp => await VisitUnixTimestampAsync(timestamp),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(node))
|
||||
};
|
||||
public async ValueTask<MarkdownNode> VisitAsync(
|
||||
MarkdownNode node,
|
||||
CancellationToken cancellationToken = default) => node switch
|
||||
{
|
||||
TextNode text =>
|
||||
await VisitTextAsync(text, cancellationToken),
|
||||
|
||||
public async ValueTask VisitAsync(IEnumerable<MarkdownNode> nodes)
|
||||
FormattingNode formatting =>
|
||||
await VisitFormattingAsync(formatting, cancellationToken),
|
||||
|
||||
InlineCodeBlockNode inlineCodeBlock =>
|
||||
await VisitInlineCodeBlockAsync(inlineCodeBlock, cancellationToken),
|
||||
|
||||
MultiLineCodeBlockNode multiLineCodeBlock =>
|
||||
await VisitMultiLineCodeBlockAsync(multiLineCodeBlock, cancellationToken),
|
||||
|
||||
LinkNode link =>
|
||||
await VisitLinkAsync(link, cancellationToken),
|
||||
|
||||
EmojiNode emoji =>
|
||||
await VisitEmojiAsync(emoji, cancellationToken),
|
||||
|
||||
MentionNode mention =>
|
||||
await VisitMentionAsync(mention, cancellationToken),
|
||||
|
||||
UnixTimestampNode timestamp =>
|
||||
await VisitUnixTimestampAsync(timestamp, cancellationToken),
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(node))
|
||||
};
|
||||
|
||||
public async ValueTask VisitAsync(
|
||||
IEnumerable<MarkdownNode> nodes,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var node in nodes)
|
||||
await VisitAsync(node);
|
||||
await VisitAsync(node, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Utils.Extensions;
|
||||
|
||||
@@ -32,13 +28,4 @@ public static class ExceptionExtensions
|
||||
PopulateChildren(exception, children);
|
||||
return children;
|
||||
}
|
||||
|
||||
public static HttpStatusCode? TryGetStatusCode(this HttpRequestException ex) =>
|
||||
// This is extremely frail, but there's no other way
|
||||
Regex
|
||||
.Match(ex.Message, @": (\d+) \(")
|
||||
.Groups[1]
|
||||
.Value
|
||||
.NullIfWhiteSpace()?
|
||||
.Pipe(s => (HttpStatusCode) int.Parse(s, CultureInfo.InvariantCulture));
|
||||
}
|
||||
@@ -1,11 +1,24 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Utils.Extensions;
|
||||
|
||||
public static class HttpExtensions
|
||||
{
|
||||
public static string? TryGetValue(this HttpContentHeaders headers, string name) =>
|
||||
public static string? TryGetValue(this HttpHeaders headers, string name) =>
|
||||
headers.TryGetValues(name, out var values)
|
||||
? string.Concat(values)
|
||||
: null;
|
||||
|
||||
public static HttpStatusCode? TryGetStatusCode(this HttpRequestException ex) =>
|
||||
// This is extremely frail, but there's no other way
|
||||
Regex
|
||||
.Match(ex.Message, @": (\d+) \(")
|
||||
.Groups[1]
|
||||
.Value
|
||||
.NullIfWhiteSpace()?
|
||||
.Pipe(s => (HttpStatusCode) int.Parse(s, CultureInfo.InvariantCulture));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace DiscordChatExporter.Core.Utils.Extensions;
|
||||
|
||||
public static class TimeSpanExtensions
|
||||
{
|
||||
public static TimeSpan Clamp(this TimeSpan value, TimeSpan min, TimeSpan max)
|
||||
{
|
||||
if (value < min)
|
||||
return min;
|
||||
|
||||
if (value > max)
|
||||
return max;
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -39,18 +39,12 @@ public static class Http
|
||||
8,
|
||||
(i, result, _) =>
|
||||
{
|
||||
// If rate-limited, use retry-after as a guide
|
||||
if (result.Result?.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
// 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)
|
||||
{
|
||||
// Only start respecting retry-after after a few attempts, because
|
||||
// Discord often sends unreasonable (20+ minutes) retry-after
|
||||
// on the very first request.
|
||||
if (i > 3)
|
||||
{
|
||||
var retryAfterDelay = result.Result.Headers.RetryAfter?.Delta;
|
||||
if (retryAfterDelay is not null)
|
||||
return retryAfterDelay.Value + TimeSpan.FromSeconds(1); // margin just in case
|
||||
}
|
||||
// Add some buffer just in case
|
||||
return retryAfter + TimeSpan.FromSeconds(1);
|
||||
}
|
||||
|
||||
return TimeSpan.FromSeconds(Math.Pow(2, i) + 1);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace DiscordChatExporter.Core.Utils;
|
||||
@@ -15,6 +16,14 @@ public static class PathEx
|
||||
foreach (var c in path)
|
||||
buffer.Append(!InvalidFileNameChars.Contains(c) ? c : '_');
|
||||
|
||||
// File names cannot end with a dot on Windows
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/977
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
while (buffer.Length > 0 && buffer[^1] == '.')
|
||||
buffer.Remove(buffer.Length - 1, 1);
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public partial class App
|
||||
|
||||
public static string GitHubProjectUrl { get; } = "https://github.com/Tyrrrz/DiscordChatExporter";
|
||||
|
||||
public static string GitHubProjectWikiUrl { get; } = GitHubProjectUrl + "/wiki";
|
||||
public static string GitHubProjectDocsUrl { get; } = GitHubProjectUrl + "/blob/master/.docs";
|
||||
}
|
||||
|
||||
public partial class App
|
||||
|
||||
@@ -9,37 +9,41 @@ using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
#endif
|
||||
|
||||
namespace DiscordChatExporter.Gui
|
||||
namespace DiscordChatExporter.Gui;
|
||||
|
||||
public class Bootstrapper : Bootstrapper<RootViewModel>
|
||||
{
|
||||
public class Bootstrapper : Bootstrapper<RootViewModel>
|
||||
protected override void OnStart()
|
||||
{
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
base.OnStart();
|
||||
|
||||
// Set default theme
|
||||
// (preferred theme will be chosen later, once the settings are loaded)
|
||||
App.SetLightTheme();
|
||||
}
|
||||
// Set default theme
|
||||
// (preferred theme will be set later, once the settings are loaded)
|
||||
App.SetLightTheme();
|
||||
}
|
||||
|
||||
protected override void ConfigureIoC(IStyletIoCBuilder builder)
|
||||
{
|
||||
base.ConfigureIoC(builder);
|
||||
protected override void ConfigureIoC(IStyletIoCBuilder builder)
|
||||
{
|
||||
base.ConfigureIoC(builder);
|
||||
|
||||
// Bind settings as singleton
|
||||
builder.Bind<SettingsService>().ToSelf().InSingletonScope();
|
||||
// Bind settings as singleton
|
||||
builder.Bind<SettingsService>().ToSelf().InSingletonScope();
|
||||
|
||||
// Bind view model factory
|
||||
builder.Bind<IViewModelFactory>().ToAbstractFactory();
|
||||
}
|
||||
// Bind view model factory
|
||||
builder.Bind<IViewModelFactory>().ToAbstractFactory();
|
||||
}
|
||||
|
||||
#if !DEBUG
|
||||
protected override void OnUnhandledException(DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
base.OnUnhandledException(e);
|
||||
protected override void OnUnhandledException(DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
base.OnUnhandledException(e);
|
||||
|
||||
MessageBox.Show(e.Exception.ToString(), "Error occured", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
#endif
|
||||
MessageBox.Show(
|
||||
e.Exception.ToString(),
|
||||
"Error occured",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error
|
||||
);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(TargetFramework)-windows</TargetFramework>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>$(TargetFramework)-windows</TargetFramework>
|
||||
<AssemblyName>DiscordChatExporter</AssemblyName>
|
||||
<UseWPF>true</UseWPF>
|
||||
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||
@@ -14,15 +14,15 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Gress" Version="2.0.1" />
|
||||
<PackageReference Include="MaterialDesignColors" Version="2.0.9" />
|
||||
<PackageReference Include="MaterialDesignThemes" Version="4.6.1" />
|
||||
<PackageReference Include="MaterialDesignColors" Version="2.1.1" />
|
||||
<PackageReference Include="MaterialDesignThemes" Version="4.7.1" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
|
||||
<PackageReference Include="Onova" Version="2.6.2" />
|
||||
<PackageReference Include="Onova" Version="2.6.4" />
|
||||
<PackageReference Include="Stylet" Version="1.3.6" />
|
||||
<PackageReference Include="Tyrrrz.Settings" Version="1.3.4" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="4.0.5" PrivateAssets="all" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.3.1" PrivateAssets="all" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" PrivateAssets="all" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.4.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace DiscordChatExporter.Gui;
|
||||
|
||||
public static class Sanctions
|
||||
{
|
||||
[ModuleInitializer]
|
||||
internal static void Verify()
|
||||
{
|
||||
var isSkipped = string.Equals(
|
||||
Environment.GetEnvironmentVariable("RUSNI"),
|
||||
"PYZDA",
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
if (isSkipped)
|
||||
return;
|
||||
|
||||
var locale = CultureInfo.CurrentCulture.Name;
|
||||
|
||||
var region = Registry.CurrentUser
|
||||
.OpenSubKey(@"Control Panel\International\Geo", false)?
|
||||
.GetValue("Name") as string;
|
||||
|
||||
var isSanctioned =
|
||||
locale.EndsWith("-ru", StringComparison.OrdinalIgnoreCase) ||
|
||||
locale.EndsWith("-by", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(region, "ru", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(region, "by", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!isSanctioned)
|
||||
return;
|
||||
|
||||
MessageBox.Show(
|
||||
"You cannot use this software on the territory of a terrorist state. " +
|
||||
"Set the environment variable `RUSNI=PYZDA` if you wish to override this check.",
|
||||
"Sanctioned region",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error
|
||||
);
|
||||
|
||||
Environment.Exit(0xFACC);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ namespace DiscordChatExporter.Gui.Services;
|
||||
|
||||
public partial class SettingsService : SettingsManager
|
||||
{
|
||||
public bool IsUkraineSupportMessageEnabled { get; set; } = true;
|
||||
|
||||
public bool IsAutoUpdateEnabled { get; set; } = true;
|
||||
|
||||
public bool IsDarkModeEnabled { get; set; } = IsDarkModeEnabledByDefault();
|
||||
|
||||
@@ -83,7 +83,7 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
await _dialogManager.ShowDialogAsync(dialog);
|
||||
}
|
||||
|
||||
public void ShowHelp() => ProcessEx.StartShellExecute(App.GitHubProjectWikiUrl);
|
||||
public void ShowHelp() => ProcessEx.StartShellExecute(App.GitHubProjectDocsUrl);
|
||||
|
||||
public bool CanPopulateGuildsAndChannels =>
|
||||
!IsBusy && !string.IsNullOrWhiteSpace(Token);
|
||||
|
||||
@@ -27,7 +27,15 @@ public class DialogManager : IDisposable
|
||||
{
|
||||
void OnScreenClosed(object? closeSender, EventArgs args)
|
||||
{
|
||||
openArgs.Session.Close();
|
||||
try
|
||||
{
|
||||
openArgs.Session.Close();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Race condition: dialog is already being closed
|
||||
}
|
||||
|
||||
dialogScreen.Closed -= OnScreenClosed;
|
||||
}
|
||||
dialogScreen.Closed += OnScreenClosed;
|
||||
|
||||
@@ -17,9 +17,9 @@ public class RootViewModel : Screen, IHandle<NotificationMessage>, IDisposable
|
||||
private readonly DialogManager _dialogManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
private readonly UpdateService _updateService;
|
||||
|
||||
|
||||
public SnackbarMessageQueue Notifications { get; } = new(TimeSpan.FromSeconds(5));
|
||||
|
||||
|
||||
public DashboardViewModel Dashboard { get; }
|
||||
|
||||
public RootViewModel(
|
||||
@@ -33,31 +33,40 @@ public class RootViewModel : Screen, IHandle<NotificationMessage>, IDisposable
|
||||
_dialogManager = dialogManager;
|
||||
_settingsService = settingsService;
|
||||
_updateService = updateService;
|
||||
|
||||
|
||||
eventAggregator.Subscribe(this);
|
||||
|
||||
|
||||
Dashboard = _viewModelFactory.CreateDashboardViewModel();
|
||||
|
||||
DisplayName = $"{App.Name} v{App.VersionString}";
|
||||
}
|
||||
|
||||
private async Task ShowWarInUkraineMessageAsync()
|
||||
private async Task ShowUkraineSupportMessageAsync()
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
"Ukraine is at war!", @"
|
||||
My country, Ukraine, has been invaded by Russian military forces in an act of aggression that can only be described as genocide.
|
||||
Be on the right side of history! Consider supporting Ukraine in its fight for freedom.
|
||||
if (!_settingsService.IsUkraineSupportMessageEnabled)
|
||||
return;
|
||||
|
||||
Press LEARN MORE to find ways that you can help.".Trim(),
|
||||
"LEARN MORE", "CLOSE"
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
"Thank you for supporting Ukraine!",
|
||||
"""
|
||||
As Russia wages a genocidal war against my country, I'm grateful to everyone who continues to stand with Ukraine in our fight for freedom.
|
||||
|
||||
Click LEARN MORE to find ways that you can help.
|
||||
""",
|
||||
"LEARN MORE",
|
||||
"CANCEL"
|
||||
);
|
||||
|
||||
// Disable this message in the future
|
||||
_settingsService.IsUkraineSupportMessageEnabled = false;
|
||||
_settingsService.Save();
|
||||
|
||||
if (await _dialogManager.ShowDialogAsync(dialog) == true)
|
||||
{
|
||||
ProcessEx.StartShellExecute("https://tyrrrz.me");
|
||||
ProcessEx.StartShellExecute("https://tyrrrz.me/ukraine?source=discordchatexporter");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async ValueTask CheckForUpdatesAsync()
|
||||
{
|
||||
try
|
||||
@@ -84,10 +93,10 @@ Press LEARN MORE to find ways that you can help.".Trim(),
|
||||
Notifications.Enqueue("Failed to perform application update");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async void OnViewFullyLoaded()
|
||||
{
|
||||
await ShowWarInUkraineMessageAsync();
|
||||
await ShowUkraineSupportMessageAsync();
|
||||
await CheckForUpdatesAsync();
|
||||
}
|
||||
|
||||
@@ -115,7 +124,7 @@ Press LEARN MORE to find ways that you can help.".Trim(),
|
||||
_updateService.FinalizeUpdate(false);
|
||||
}
|
||||
|
||||
public void Handle(NotificationMessage message) =>
|
||||
public void Handle(NotificationMessage message) =>
|
||||
Notifications.Enqueue(message.Text);
|
||||
|
||||
public void Dispose() => Notifications.Dispose();
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
<LineBreak />
|
||||
<Run Text="7. Select the" />
|
||||
<Run FontWeight="SemiBold" Text="Headers" />
|
||||
<Run Text=" tab on the right" />
|
||||
<Run Text="tab on the right" />
|
||||
<LineBreak />
|
||||
<Run Text="8. Scroll down to the" />
|
||||
<Run FontWeight="SemiBold" Text="Request Headers" />
|
||||
@@ -225,7 +225,7 @@
|
||||
<LineBreak />
|
||||
|
||||
<Run Text="If you have questions or issues, please refer to the" />
|
||||
<Hyperlink Command="{s:Action ShowHelp}">wiki</Hyperlink><Run Text="." />
|
||||
<Hyperlink Command="{s:Action ShowHelp}">documentation</Hyperlink><Run Text="." />
|
||||
</TextBlock>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
DiscordChatExporter
|
||||
Copyright (C) 2017-2022 Oleksii Holub
|
||||
Copyright (C) 2017-2023 Oleksii Holub
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# DiscordChatExporter
|
||||
|
||||
[](https://vshymanskyy.github.io/StandWithUkraine)
|
||||
[](https://github.com/Tyrrrz/DiscordChatExporter/actions)
|
||||
[](https://tyrrrz.me/ukraine)
|
||||
[](https://github.com/Tyrrrz/DiscordChatExporter/actions)
|
||||
[](https://codecov.io/gh/Tyrrrz/DiscordChatExporter)
|
||||
[](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
||||
[](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
||||
@@ -11,10 +11,10 @@
|
||||
|
||||
> 🟡 **Project status**: maintenance mode<sup>[[?]](https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md)</sup>
|
||||
|
||||
**DiscordChatExporter** can be used to export message history from a [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 all other rich media features.
|
||||
**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 [wiki](https://github.com/Tyrrrz/DiscordChatExporter/wiki)**.
|
||||
> ❔ If you have questions or issues, **please refer to the [docs](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs)**.
|
||||
|
||||
> 💬 If you want to chat, **join my [Discord server](https://discord.gg/2SUWKFnHSm)**.
|
||||
|
||||
@@ -84,12 +84,12 @@ The following table lists all available download options:
|
||||
> 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 Runtime v6** is installed.
|
||||
> To run **DiscordChatExporter** on macOS and Linux, you need to make sure that **.NET 7.0 Runtime** is installed.
|
||||
> You can download it here:
|
||||
>
|
||||
> - [.NET Runtime v6 for **macOS x64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-6.0.6-macos-x64-installer)
|
||||
> - [.NET Runtime v6 for **macOS Arm64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-6.0.6-macos-arm64-installer)
|
||||
> - [.NET Runtime v6 for **Linux**](https://docs.microsoft.com/en-us/dotnet/core/install/linux) (find the correct download for your distro)
|
||||
> - [.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)
|
||||
>
|
||||
> 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.
|
||||
@@ -112,4 +112,4 @@ The following table lists all available download options:
|
||||
## Related projects
|
||||
|
||||
- [**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**.
|
||||
- [**DiscordChatExporter-frontend**](https://github.com/slatinsky/DiscordChatExporter-frontend) — convenient viewer for exports produced by **DiscordChatExporter**.
|
||||
|
||||
Reference in New Issue
Block a user