mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-09 07:30:28 +02:00
Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31de8a5b06 | |||
| efd430ed29 | |||
| 623c688e1b | |||
| 05f8df51e9 | |||
| e2c633b004 | |||
| b11a57a825 | |||
| f502e577c2 | |||
| 8bc9fe7c72 | |||
| 97485c280b | |||
| 5a57c4202d | |||
| acac8f7bbb | |||
| 5632d6c386 | |||
| f4d1e630f7 | |||
| 799dd1ad81 | |||
| 64a4ba296e | |||
| f0e99765f7 | |||
| 45629c9d7f | |||
| 2aec84884b | |||
| 6258394fc0 | |||
| e6472543e0 | |||
| ee163d6de7 | |||
| 5838c0546e | |||
| 07a02773be | |||
| 0a623b24d1 | |||
| da4f5ea74a | |||
| 34b5ccc75b | |||
| 7456f0fe3a | |||
| 757daa7a60 | |||
| 9590d4461a | |||
| a27fc3e72e | |||
| f10bba63e6 | |||
| b76b42549b | |||
| ae70fe9741 | |||
| af3ec8c299 | |||
| f1673cde77 | |||
| ed33d19bc0 | |||
| 80e9ad75ba | |||
| 295f4cf9a2 | |||
| 1f189e5206 | |||
| df683b3b41 | |||
| d467948d0b | |||
| f6166764e9 | |||
| 7ee2763d4b | |||
| eb99ab29ed | |||
| 5c61306231 | |||
| 2e4601d378 | |||
| cc703cc860 | |||
| 661f82c478 | |||
| b9f19f7003 | |||
| e794d7c833 | |||
| 5a26ba0b5a | |||
| 1181a35f08 | |||
| 4a62f9c34f | |||
| b6e1f92989 | |||
| 0d3d2ddcf0 | |||
| 9afecd47bd | |||
| 5cec162b4e | |||
| 6647f90ced | |||
| b9a3001c11 | |||
| dadfe5b8ef | |||
| 4f29fa63d0 | |||
| 54b0174bde | |||
| 3be5bc6a21 | |||
| c9f152773a | |||
| 5e39611395 | |||
| 40491bdc0f | |||
| 03dd272eaf | |||
| 98e294f7e4 | |||
| b22e786449 | |||
| 951c1e4613 | |||
| 675d910ea3 | |||
| 28f26e45fb | |||
| 4674c517e3 | |||
| b660edfe78 | |||
| 532470e3db | |||
| b533e08d74 | |||
| d490fd0a38 | |||
| 21c2398e2a | |||
| 7ff2347df1 | |||
| 3391e755fc | |||
| f0e9d3be1b | |||
| eef0fc742d | |||
| 2e47c73388 | |||
| 2d49146c74 | |||
| c59374598a | |||
| 01d0203445 | |||
| c4bfb3424e | |||
| 522caba420 | |||
| ad1170b42e | |||
| d2c0e2169b | |||
| e6562de1f5 | |||
| 5f8054e2f0 | |||
| 12d98e9ab0 | |||
| 18086aa209 | |||
| dd7196b6a5 |
+2
-2
@@ -17,7 +17,7 @@ Note the `:stable` tag. DiscordChatExporter images are tagged according to the f
|
|||||||
|
|
||||||
- `stable` — latest stable version release. This tag is updated with each release of a new project version. Recommended for personal use.
|
- `stable` — latest stable version release. This tag is updated with each release of a new project version. Recommended for personal use.
|
||||||
- `x.y.z` (e.g. `2.30.1`) — specific stable version release. This tag is pushed when the corresponding version is released and never updated thereafter. Recommended for use in automation scenarios.
|
- `x.y.z` (e.g. `2.30.1`) — specific stable version release. This tag is pushed when the corresponding version is released and never updated thereafter. Recommended for use in automation scenarios.
|
||||||
- `latest` — latest (potentially unstable) build. This tag is updated with each new commit to the `master` branch. Not recommended, unless you want to test a new feature that has not been released in a stable version yet.
|
- `latest` — latest (potentially unstable) build. This tag is updated with each new commit to the `prime` branch. Not recommended, unless you want to test a new feature that has not been released in a stable version yet.
|
||||||
|
|
||||||
You can see all available tags [here](https://hub.docker.com/r/tyrrrz/discordchatexporter/tags?ordering=name).
|
You can see all available tags [here](https://hub.docker.com/r/tyrrrz/discordchatexporter/tags?ordering=name).
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ You can also use the current working directory as the output directory by specif
|
|||||||
- `-v $PWD:/out` in Bash
|
- `-v $PWD:/out` in Bash
|
||||||
- `-v $pwd.Path:/out` in PowerShell
|
- `-v $pwd.Path:/out` in PowerShell
|
||||||
|
|
||||||
For more information, please refer to the [Dockerfile](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/DiscordChatExporter.Cli.dockerfile) and [Docker documentation](https://docs.docker.com/engine/reference/run).
|
For more information, please refer to the [Dockerfile](https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/DiscordChatExporter.Cli.dockerfile) and [Docker documentation](https://docs.docker.com/engine/reference/run).
|
||||||
|
|
||||||
To get your Token and Channel IDs, please refer to [this page](Token-and-IDs.md).
|
To get your Token and Channel IDs, please refer to [this page](Token-and-IDs.md).
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs
|
# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs
|
||||||
|
|
||||||
TOKEN=tokenhere
|
TOKEN=tokenhere
|
||||||
CHANNELID=channelhere
|
CHANNELID=channelhere
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs
|
# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs
|
||||||
|
|
||||||
TOKEN=tokenhere
|
TOKEN=tokenhere
|
||||||
CHANNELID=channelhere
|
CHANNELID=channelhere
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
1. Open a text editor such as Notepad and paste:
|
1. Open a text editor such as Notepad and paste:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs
|
# Info: https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs
|
||||||
|
|
||||||
$TOKEN = "tokenhere"
|
$TOKEN = "tokenhere"
|
||||||
$CHANNEL = "channelhere"
|
$CHANNEL = "channelhere"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Prerequisite step: Navigate to [discord.com](https://discord.com) and login.
|
|||||||
3. Type
|
3. Type
|
||||||
|
|
||||||
```js
|
```js
|
||||||
let m;webpackChunkdiscord_app.push([[Math.random()],{},e=>{for(let i in e.c){let x=e.c[i];if(x?.exports?.$8&&x.exports.LP&&x.exports.gK){m=x;break}}}]);m&&console.log("Token:",m.exports.LP());
|
let m;webpackChunkdiscord_app.push([[Math.random()],{},e=>{for(let i in e.c){let x=e.c[i];if(x?.exports?.getToken){m=x;break}}}]);m&&console.log("Token:",m.exports.getToken());
|
||||||
```
|
```
|
||||||
|
|
||||||
into the console and press <kbd>Enter</kbd>. The console will display your user token.
|
into the console and press <kbd>Enter</kbd>. The console will display your user token.
|
||||||
@@ -120,7 +120,7 @@ Prerequisite step: Navigate to [discord.com](https://discord.com) and login.
|
|||||||
1. Type
|
1. Type
|
||||||
|
|
||||||
```js
|
```js
|
||||||
let m;webpackChunkdiscord_app.push([[Math.random()],{},e=>{for(let i in e.c){let x=e.c[i];if(x?.exports?.$8&&x.exports.LP&&x.exports.gK){m=x;break}}}]);m&&console.log("Token:",m.exports.LP());
|
let m;webpackChunkdiscord_app.push([[Math.random()],{},e=>{for(let i in e.c){let x=e.c[i];if(x?.exports?.getToken){m=x;break}}}]);m&&console.log("Token:",m.exports.getToken());
|
||||||
```
|
```
|
||||||
|
|
||||||
into the console and press <kbd>Enter</kbd>. The console will display your user token.
|
into the console and press <kbd>Enter</kbd>. The console will display your user token.
|
||||||
|
|||||||
+11
-12
@@ -29,21 +29,20 @@ Type the following command in your terminal of choice, then press ENTER to run i
|
|||||||
> **Note**:
|
> **Note**:
|
||||||
> On Windows, if you're using the default Command Prompt (`cmd`), omit the leading `./` at the start of the command.
|
> On Windows, if you're using the default Command Prompt (`cmd`), omit the leading `./` at the start of the command.
|
||||||
|
|
||||||
|
|
||||||
> **Docker** users, please refer to the [Docker usage instructions](Docker.md).
|
> **Docker** users, please refer to the [Docker usage instructions](Docker.md).
|
||||||
|
|
||||||
## CLI commands
|
## CLI commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|-------------------------|------------------------------------------------------|
|
| ----------- | ---------------------------------------------------- |
|
||||||
| export | Exports a channel |
|
| export | Exports a channel |
|
||||||
| exportdm | Exports all direct message channels |
|
| exportdm | Exports all direct message channels |
|
||||||
| exportguild | Exports all channels within the specified server |
|
| exportguild | Exports all channels within the specified server |
|
||||||
| exportall | Exports all accessible channels |
|
| exportall | Exports all accessible channels |
|
||||||
| channels | Outputs the list of channels in the given server |
|
| channels | Outputs the list of channels in the given server |
|
||||||
| dm | Outputs the list of direct message channels |
|
| dm | Outputs the list of direct message channels |
|
||||||
| guilds | Outputs the list of accessible servers |
|
| guilds | Outputs the list of accessible servers |
|
||||||
| guide | Explains how to obtain token, server, and channel ID |
|
| guide | Explains how to obtain token, server, and channel ID |
|
||||||
|
|
||||||
To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./DiscordChatExporter.Cli guide`.
|
To use the commands, you'll need a token. For the instructions on how to get a token, please refer to [this page](Token-and-IDs.md), or run `./DiscordChatExporter.Cli guide`.
|
||||||
|
|
||||||
@@ -222,7 +221,7 @@ Use `--filter` to filter what messages are included in the export.
|
|||||||
./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 --filter "from:Tyrrrz has:image"
|
./DiscordChatExporter.Cli export -t "mfa.Ifrn" -c 53555 --filter "from:Tyrrrz has:image"
|
||||||
```
|
```
|
||||||
|
|
||||||
Documentation on message filter syntax can be found [here](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Message-filters.md).
|
Documentation on message filter syntax can be found [here](https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs/Message-filters.md).
|
||||||
|
|
||||||
### Export channels from a specific server
|
### Export channels from a specific server
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: ⚠ Feature request
|
- name: ⚠ Feature request
|
||||||
url: https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md
|
url: https://github.com/Tyrrrz/.github/blob/prime/docs/project-status.md
|
||||||
about: Sorry, but this project is in maintenance mode and no longer accepts new feature requests.
|
about: Sorry, but this project is in maintenance mode and currently doesn't accept new feature requests.
|
||||||
- name: 📖 Documentation
|
- name: 📖 Documentation
|
||||||
url: https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs
|
url: https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs
|
||||||
about: Find usage guides and frequently asked questions.
|
about: Find usage guides and frequently asked questions.
|
||||||
- name: 🗨 Discussions
|
- name: 🗨 Discussions
|
||||||
url: https://github.com/Tyrrrz/DiscordChatExporter/discussions/new
|
url: https://github.com/Tyrrrz/DiscordChatExporter/discussions/new
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
changelog:
|
||||||
|
exclude:
|
||||||
|
authors:
|
||||||
|
- dependabot
|
||||||
|
- dependabot[bot]
|
||||||
|
|
||||||
|
categories:
|
||||||
|
- title: Enhancements
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
|
||||||
|
- title: Bugs
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
|
||||||
|
- title: Other
|
||||||
|
labels:
|
||||||
|
- "*"
|
||||||
@@ -4,12 +4,12 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- prime
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- prime
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Outputs from this job aren't really used, but it's here to verify that the Dockerfile builds correctly
|
# Outputs from this job aren't really used, but it's here to verify that the Dockerfile builds correctly
|
||||||
@@ -23,10 +23,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Install Docker Buildx
|
- name: Install Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
|
|
||||||
- name: Build image
|
- name: Build image
|
||||||
run: >
|
run: >
|
||||||
@@ -37,15 +37,15 @@ jobs:
|
|||||||
--output type=tar,dest=DiscordChatExporter.Cli.Docker.tar
|
--output type=tar,dest=DiscordChatExporter.Cli.Docker.tar
|
||||||
|
|
||||||
- name: Upload image
|
- name: Upload image
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: DiscordChatExporter.Cli.Docker
|
name: DiscordChatExporter.Cli.Docker
|
||||||
path: DiscordChatExporter.Cli.Docker.tar
|
path: DiscordChatExporter.Cli.Docker.tar
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
# Deploy to DockerHub only on tag push or master branch push
|
# Deploy to DockerHub only on tag push or prime branch push
|
||||||
if: ${{ github.ref_type == 'tag' || github.ref_type == 'branch' && github.ref_name == 'master' }}
|
if: ${{ github.ref_type == 'tag' || github.ref_type == 'branch' && github.ref_name == 'prime' }}
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
@@ -55,10 +55,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Install Docker Buildx
|
- name: Install Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
run: >
|
run: >
|
||||||
|
|||||||
+13
-12
@@ -4,12 +4,12 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- prime
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- prime
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||||
@@ -26,10 +26,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Install .NET
|
- name: Install .NET
|
||||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||||
|
|
||||||
# Build the project separately to discern between build and format errors
|
# Build the project separately to discern between build and format errors
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -62,10 +62,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Install .NET
|
- name: Install .NET
|
||||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
env:
|
env:
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
|
DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
@@ -117,16 +117,17 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Install .NET
|
- name: Install .NET
|
||||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||||
|
|
||||||
- name: Publish app
|
- name: Publish app
|
||||||
run: >
|
run: >
|
||||||
dotnet publish ${{ matrix.app }}
|
dotnet publish ${{ matrix.app }}
|
||||||
-p:Version=${{ github.ref_type == 'tag' && github.ref_name || format('999.9.9-ci-{0}', github.sha) }}
|
-p:Version=${{ github.ref_type == 'tag' && github.ref_name || format('999.9.9-ci-{0}', github.sha) }}
|
||||||
-p:CSharpier_Bypass=true
|
-p:CSharpier_Bypass=true
|
||||||
|
-p:EncryptionSalt=${{ secrets.ENCRYPTION_SALT || 'HimalayanPinkSalt' }}
|
||||||
-p:PublishMacOSBundle=${{ startsWith(matrix.rid, 'osx-') }}
|
-p:PublishMacOSBundle=${{ startsWith(matrix.rid, 'osx-') }}
|
||||||
--output ${{ matrix.app }}/bin/publish/
|
--output ${{ matrix.app }}/bin/publish/
|
||||||
--configuration Release
|
--configuration Release
|
||||||
@@ -134,7 +135,7 @@ jobs:
|
|||||||
--self-contained
|
--self-contained
|
||||||
|
|
||||||
- name: Upload app binaries
|
- name: Upload app binaries
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.asset }}.${{ matrix.rid }}
|
name: ${{ matrix.asset }}.${{ matrix.rid }}
|
||||||
path: ${{ matrix.app }}/bin/publish/
|
path: ${{ matrix.app }}/bin/publish/
|
||||||
@@ -199,7 +200,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download app binaries
|
- name: Download app binaries
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.asset }}.${{ matrix.rid }}
|
name: ${{ matrix.asset }}.${{ matrix.rid }}
|
||||||
path: ${{ matrix.app }}/
|
path: ${{ matrix.app }}/
|
||||||
@@ -235,7 +236,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Notify Discord
|
- name: Notify Discord
|
||||||
uses: tyrrrz/action-http-request@1dd7ad841a34b9299f3741f7c7399f9feefdfb08 # 1.1.3
|
uses: tyrrrz/action-http-request@110f1a0f0f7e91c3b2de349539249f4573c243fa # 1.1.5
|
||||||
with:
|
with:
|
||||||
url: ${{ secrets.DISCORD_WEBHOOK }}
|
url: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
method: POST
|
method: POST
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Version>999.9.9-dev</Version>
|
<Version>999.9.9-dev</Version>
|
||||||
<Company>Tyrrrz</Company>
|
<Company>Tyrrrz</Company>
|
||||||
<Copyright>Copyright (c) Oleksii Holub</Copyright>
|
<Copyright>Copyright (c) Oleksii Holub</Copyright>
|
||||||
<LangVersion>preview</LangVersion>
|
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<ILLinkTreatWarningsAsErrors>false</ILLinkTreatWarningsAsErrors>
|
<ILLinkTreatWarningsAsErrors>false</ILLinkTreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
</Project>
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="AngleSharp" Version="1.5.1" />
|
||||||
|
<PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.8.0" />
|
||||||
|
<PackageVersion Include="AsyncKeyedLock" Version="8.0.2" />
|
||||||
|
<PackageVersion Include="Avalonia" Version="12.0.5" />
|
||||||
|
<PackageVersion Include="Avalonia.Desktop" Version="12.0.5" />
|
||||||
|
<PackageVersion Include="CliFx" Version="3.0.0" />
|
||||||
|
<PackageVersion Include="Cogwheel" Version="2.1.1" />
|
||||||
|
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||||
|
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
|
||||||
|
<PackageVersion Include="CSharpier.MsBuild" Version="1.3.0" />
|
||||||
|
<PackageVersion Include="Deorcify" Version="2.0.1" />
|
||||||
|
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.2" />
|
||||||
|
<PackageVersion Include="FluentAssertions" Version="8.10.0" />
|
||||||
|
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.4" />
|
||||||
|
<PackageVersion Include="Gress" Version="2.2.0" />
|
||||||
|
<PackageVersion Include="JsonExtensions" Version="1.2.3" />
|
||||||
|
<PackageVersion Include="Markdig" Version="1.3.2" />
|
||||||
|
<PackageVersion Include="Material.Avalonia" Version="3.16.1" />
|
||||||
|
<PackageVersion Include="Material.Icons.Avalonia" Version="3.0.2" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.9" />
|
||||||
|
<PackageVersion
|
||||||
|
Include="Microsoft.Extensions.Configuration.EnvironmentVariables"
|
||||||
|
Version="10.0.9"
|
||||||
|
/>
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.9" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
|
||||||
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
|
||||||
|
<PackageVersion Include="Onova" Version="2.6.13" />
|
||||||
|
<PackageVersion Include="Polly" Version="8.7.0" />
|
||||||
|
<PackageVersion Include="PowerKit" Version="2.0.2" />
|
||||||
|
<PackageVersion Include="RazorBlade" Version="1.0.0" />
|
||||||
|
<PackageVersion Include="Spectre.Console" Version="0.57.1" />
|
||||||
|
<PackageVersion Include="Superpower" Version="3.1.0" />
|
||||||
|
<PackageVersion Include="ThisAssembly.Project" Version="2.1.2" />
|
||||||
|
<PackageVersion Include="WebMarkupMin.Core" Version="2.21.0" />
|
||||||
|
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||||
|
<PackageVersion Include="YoutubeExplode" Version="6.6.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -10,24 +10,22 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="1.4.0" />
|
<PackageReference Include="AngleSharp" />
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" PrivateAssets="all" />
|
<PackageReference Include="coverlet.collector" PrivateAssets="all" />
|
||||||
<PackageReference Include="CSharpier.MsBuild" Version="1.2.5" PrivateAssets="all" />
|
<PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" />
|
||||||
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
<PackageReference Include="FluentAssertions" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="3.0.1" PrivateAssets="all" />
|
<PackageReference Include="GitHubActionsTestLogger" />
|
||||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
<PackageReference Include="JsonExtensions" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||||
<PackageReference
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
||||||
Include="Microsoft.Extensions.Configuration.EnvironmentVariables"
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||||
Version="10.0.2"
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
/>
|
<PackageReference Include="PowerKit" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.2" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" PrivateAssets="all" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DiscordChatExporter.Cli\DiscordChatExporter.Cli.csproj" />
|
<ProjectReference Include="../DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Infra;
|
namespace DiscordChatExporter.Cli.Tests.Infra;
|
||||||
|
|
||||||
@@ -16,6 +16,8 @@ public static class ChannelIds
|
|||||||
|
|
||||||
public static Snowflake FilterTestCases { get; } = Snowflake.Parse("866744075033641020");
|
public static Snowflake FilterTestCases { get; } = Snowflake.Parse("866744075033641020");
|
||||||
|
|
||||||
|
public static Snowflake ForwardTestCases { get; } = Snowflake.Parse("1455202357204877477");
|
||||||
|
|
||||||
public static Snowflake MarkdownTestCases { get; } = Snowflake.Parse("866459526819348521");
|
public static Snowflake MarkdownTestCases { get; } = Snowflake.Parse("866459526819348521");
|
||||||
|
|
||||||
public static Snowflake MentionTestCases { get; } = Snowflake.Parse("866458801389174794");
|
public static Snowflake MentionTestCases { get; } = Snowflake.Parse("866458801389174794");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# DiscordChatExporter Tests
|
# DiscordChatExporter Tests
|
||||||
|
|
||||||
This test suite runs against a real Discord server, specifically created to exercise different behaviors required by the test scenarios.
|
This test suite runs against a real Discord server, specifically created to exercise different behaviors required by the test scenarios.
|
||||||
In order to run these tests locally, you need to join the test server and configure your authentication token.
|
In order to run these tests locally, you need to join the test server and configure your authentication token.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands;
|
using DiscordChatExporter.Cli.Commands;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using JsonExtensions;
|
using JsonExtensions;
|
||||||
|
using PowerKit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Specs;
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands;
|
using DiscordChatExporter.Cli.Commands;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using DiscordChatExporter.Core.Exporting.Filtering;
|
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using JsonExtensions;
|
using JsonExtensions;
|
||||||
|
using PowerKit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Specs;
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
using System.Linq;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
using DiscordChatExporter.Cli.Commands;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
|
using DiscordChatExporter.Cli.Tests.Utils;
|
||||||
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using PowerKit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Specs;
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
@@ -44,4 +50,41 @@ public class HtmlContentSpecs
|
|||||||
"Yeet"
|
"Yeet"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_export_a_channel_in_the_HTML_format_in_the_reverse_order()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using var file = TempFile.Create();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await new ExportChannelsCommand
|
||||||
|
{
|
||||||
|
Token = Secrets.DiscordToken,
|
||||||
|
ChannelIds = [ChannelIds.DateRangeTestCases],
|
||||||
|
ExportFormat = ExportFormat.HtmlDark,
|
||||||
|
OutputPath = file.Path,
|
||||||
|
Locale = "en-US",
|
||||||
|
IsUtcNormalizationEnabled = true,
|
||||||
|
IsReverseMessageOrder = true,
|
||||||
|
}.ExecuteAsync(new FakeConsole());
|
||||||
|
|
||||||
|
var document = Html.Parse(await File.ReadAllTextAsync(file.Path));
|
||||||
|
var messages = document.QuerySelectorAll("[data-message-id]").ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
messages
|
||||||
|
.Select(e => e.GetAttribute("data-message-id"))
|
||||||
|
.Should()
|
||||||
|
.Equal(
|
||||||
|
"885169254029213696",
|
||||||
|
"868505973294268457",
|
||||||
|
"868505969821364245",
|
||||||
|
"868505966528835604",
|
||||||
|
"868490009366396958",
|
||||||
|
"866732113319428096",
|
||||||
|
"866710679758045195",
|
||||||
|
"866674314627121232"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using PowerKit.Extensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Specs;
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using AngleSharp.Dom;
|
||||||
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
using FluentAssertions;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
|
|
||||||
|
public class HtmlForwardSpecs
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_export_a_channel_that_contains_a_forwarded_message()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.ForwardTestCases,
|
||||||
|
Snowflake.Parse("1455202427115536514")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
message
|
||||||
|
.Text()
|
||||||
|
.ReplaceWhiteSpace(' ')
|
||||||
|
.Should()
|
||||||
|
.ContainAll("Forwarded", @"¯\_(ツ)_/¯", "12/29/2025 2:14 PM");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
@@ -8,6 +8,7 @@ using DiscordChatExporter.Cli.Tests.Infra;
|
|||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
using DiscordChatExporter.Cli.Tests.Utils;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using PowerKit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Specs;
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.Utils.Extensions;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using PowerKit.Extensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Specs;
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
@@ -22,11 +22,14 @@ public class HtmlMarkdownSpecs
|
|||||||
// Assert
|
// Assert
|
||||||
message
|
message
|
||||||
.Text()
|
.Text()
|
||||||
.ReplaceWhiteSpace()
|
.ReplaceWhiteSpace(' ')
|
||||||
.Should()
|
.Should()
|
||||||
.Contain("Default timestamp: 2/12/2023 1:36 PM");
|
.Contain("Default timestamp: 2/12/2023 1:36 PM");
|
||||||
|
|
||||||
message.InnerHtml.ReplaceWhiteSpace().Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
message
|
||||||
|
.InnerHtml.ReplaceWhiteSpace(' ')
|
||||||
|
.Should()
|
||||||
|
.Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -39,8 +42,11 @@ public class HtmlMarkdownSpecs
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
message.Text().ReplaceWhiteSpace().Should().Contain("Short time timestamp: 1:36 PM");
|
message.Text().ReplaceWhiteSpace(' ').Should().Contain("Short time timestamp: 1:36 PM");
|
||||||
message.InnerHtml.ReplaceWhiteSpace().Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
message
|
||||||
|
.InnerHtml.ReplaceWhiteSpace(' ')
|
||||||
|
.Should()
|
||||||
|
.Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -53,8 +59,11 @@ public class HtmlMarkdownSpecs
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
message.Text().ReplaceWhiteSpace().Should().Contain("Long time timestamp: 1:36:12 PM");
|
message.Text().ReplaceWhiteSpace(' ').Should().Contain("Long time timestamp: 1:36:12 PM");
|
||||||
message.InnerHtml.ReplaceWhiteSpace().Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
message
|
||||||
|
.InnerHtml.ReplaceWhiteSpace(' ')
|
||||||
|
.Should()
|
||||||
|
.Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -67,8 +76,11 @@ public class HtmlMarkdownSpecs
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
message.Text().ReplaceWhiteSpace().Should().Contain("Short date timestamp: 2/12/2023");
|
message.Text().ReplaceWhiteSpace(' ').Should().Contain("Short date timestamp: 2/12/2023");
|
||||||
message.InnerHtml.ReplaceWhiteSpace().Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
message
|
||||||
|
.InnerHtml.ReplaceWhiteSpace(' ')
|
||||||
|
.Should()
|
||||||
|
.Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -83,11 +95,14 @@ public class HtmlMarkdownSpecs
|
|||||||
// Assert
|
// Assert
|
||||||
message
|
message
|
||||||
.Text()
|
.Text()
|
||||||
.ReplaceWhiteSpace()
|
.ReplaceWhiteSpace(' ')
|
||||||
.Should()
|
.Should()
|
||||||
.Contain("Long date timestamp: Sunday, February 12, 2023");
|
.Contain("Long date timestamp: Sunday, February 12, 2023");
|
||||||
|
|
||||||
message.InnerHtml.ReplaceWhiteSpace().Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
message
|
||||||
|
.InnerHtml.ReplaceWhiteSpace(' ')
|
||||||
|
.Should()
|
||||||
|
.Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -102,11 +117,14 @@ public class HtmlMarkdownSpecs
|
|||||||
// Assert
|
// Assert
|
||||||
message
|
message
|
||||||
.Text()
|
.Text()
|
||||||
.ReplaceWhiteSpace()
|
.ReplaceWhiteSpace(' ')
|
||||||
.Should()
|
.Should()
|
||||||
.Contain("Full timestamp: Sunday, February 12, 2023 1:36 PM");
|
.Contain("Full timestamp: Sunday, February 12, 2023 1:36 PM");
|
||||||
|
|
||||||
message.InnerHtml.ReplaceWhiteSpace().Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
message
|
||||||
|
.InnerHtml.ReplaceWhiteSpace(' ')
|
||||||
|
.Should()
|
||||||
|
.Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -121,11 +139,14 @@ public class HtmlMarkdownSpecs
|
|||||||
// Assert
|
// Assert
|
||||||
message
|
message
|
||||||
.Text()
|
.Text()
|
||||||
.ReplaceWhiteSpace()
|
.ReplaceWhiteSpace(' ')
|
||||||
.Should()
|
.Should()
|
||||||
.Contain("Full long timestamp: Sunday, February 12, 2023 1:36:12 PM");
|
.Contain("Full long timestamp: Sunday, February 12, 2023 1:36:12 PM");
|
||||||
|
|
||||||
message.InnerHtml.ReplaceWhiteSpace().Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
message
|
||||||
|
.InnerHtml.ReplaceWhiteSpace(' ')
|
||||||
|
.Should()
|
||||||
|
.Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -140,11 +161,14 @@ public class HtmlMarkdownSpecs
|
|||||||
// Assert
|
// Assert
|
||||||
message
|
message
|
||||||
.Text()
|
.Text()
|
||||||
.ReplaceWhiteSpace()
|
.ReplaceWhiteSpace(' ')
|
||||||
.Should()
|
.Should()
|
||||||
.Contain("Relative timestamp: 2/12/2023 1:36 PM");
|
.Contain("Relative timestamp: 2/12/2023 1:36 PM");
|
||||||
|
|
||||||
message.InnerHtml.ReplaceWhiteSpace().Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
message
|
||||||
|
.InnerHtml.ReplaceWhiteSpace(' ')
|
||||||
|
.Should()
|
||||||
|
.Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
@@ -61,4 +61,17 @@ public class HtmlMentionSpecs
|
|||||||
// Assert
|
// Assert
|
||||||
message.Text().Should().Contain("Role mention: @Role 1");
|
message.Text().Should().Contain("Role mention: @Role 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_export_a_channel_that_contains_a_message_with_a_thread_mention()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.MentionTestCases,
|
||||||
|
Snowflake.Parse("1474874276828938290")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
message.Text().Should().Contain("Thread mention: #Thread starting message");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
using System.Linq;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
|
using DiscordChatExporter.Cli.Commands;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using JsonExtensions;
|
||||||
|
using PowerKit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Specs;
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
@@ -43,4 +49,43 @@ public class JsonContentSpecs
|
|||||||
"Yeet"
|
"Yeet"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_export_a_channel_in_the_JSON_format_in_the_reverse_order()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using var file = TempFile.Create();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await new ExportChannelsCommand
|
||||||
|
{
|
||||||
|
Token = Secrets.DiscordToken,
|
||||||
|
ChannelIds = [ChannelIds.DateRangeTestCases],
|
||||||
|
ExportFormat = ExportFormat.Json,
|
||||||
|
OutputPath = file.Path,
|
||||||
|
Locale = "en-US",
|
||||||
|
IsUtcNormalizationEnabled = true,
|
||||||
|
IsReverseMessageOrder = true,
|
||||||
|
}.ExecuteAsync(new FakeConsole());
|
||||||
|
|
||||||
|
var messages = Json.Parse(await File.ReadAllTextAsync(file.Path))
|
||||||
|
.GetProperty("messages")
|
||||||
|
.EnumerateArray()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
messages
|
||||||
|
.Select(j => j.GetProperty("id").GetString())
|
||||||
|
.Should()
|
||||||
|
.Equal(
|
||||||
|
"885169254029213696",
|
||||||
|
"868505973294268457",
|
||||||
|
"868505969821364245",
|
||||||
|
"868505966528835604",
|
||||||
|
"868490009366396958",
|
||||||
|
"866732113319428096",
|
||||||
|
"866710679758045195",
|
||||||
|
"866674314627121232"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
|
|
||||||
|
public class JsonForwardSpecs
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_export_a_channel_that_contains_a_forwarded_message()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||||
|
ChannelIds.ForwardTestCases,
|
||||||
|
Snowflake.Parse("1455202427115536514")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var reference = message.GetProperty("reference");
|
||||||
|
reference.GetProperty("type").GetString().Should().Be("Forward");
|
||||||
|
reference.GetProperty("guildId").GetString().Should().Be("869237470565392384");
|
||||||
|
|
||||||
|
var forwardedMessage = message.GetProperty("forwardedMessage");
|
||||||
|
forwardedMessage.GetProperty("content").GetString().Should().Contain(@"¯\_(ツ)_/¯");
|
||||||
|
forwardedMessage
|
||||||
|
.GetProperty("timestamp")
|
||||||
|
.GetString()
|
||||||
|
.Should()
|
||||||
|
.StartWith("2025-12-28T22:52:42.175+00:00");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
@@ -75,4 +75,21 @@ public class JsonMentionSpecs
|
|||||||
// Assert
|
// Assert
|
||||||
message.GetProperty("content").GetString().Should().Be("Role mention: @Role 1");
|
message.GetProperty("content").GetString().Should().Be("Role mention: @Role 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_export_a_channel_that_contains_a_message_with_a_thread_mention()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var message = await ExportWrapper.GetMessageAsJsonAsync(
|
||||||
|
ChannelIds.MentionTestCases,
|
||||||
|
Snowflake.Parse("1474874276828938290")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
message
|
||||||
|
.GetProperty("content")
|
||||||
|
.GetString()
|
||||||
|
.Should()
|
||||||
|
.Be("Thread mention: #Thread starting message");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands;
|
using DiscordChatExporter.Cli.Commands;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using PowerKit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Specs;
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
@@ -17,7 +17,7 @@ public class PartitioningSpecs
|
|||||||
public async Task I_can_export_a_channel_with_partitioning_based_on_message_count()
|
public async Task I_can_export_a_channel_with_partitioning_based_on_message_count()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
using var dir = TempDir.Create();
|
using var dir = TempDirectory.Create();
|
||||||
var filePath = Path.Combine(dir.Path, "output.html");
|
var filePath = Path.Combine(dir.Path, "output.html");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -38,7 +38,7 @@ public class PartitioningSpecs
|
|||||||
public async Task I_can_export_a_channel_with_partitioning_based_on_file_size()
|
public async Task I_can_export_a_channel_with_partitioning_based_on_file_size()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
using var dir = TempDir.Create();
|
using var dir = TempDirectory.Create();
|
||||||
var filePath = Path.Combine(dir.Path, "output.html");
|
var filePath = Path.Combine(dir.Path, "output.html");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
|
using FluentAssertions;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
|
|
||||||
|
public class PlainTextForwardSpecs
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_export_a_channel_that_contains_a_forwarded_message()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var document = await ExportWrapper.ExportAsPlainTextAsync(ChannelIds.ForwardTestCases);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
document
|
||||||
|
.ReplaceWhiteSpace(' ')
|
||||||
|
.Should()
|
||||||
|
.ContainAll("{Forwarded Message}", @"¯\_(ツ)_/¯", "12/28/2025 10:52 PM");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
@@ -7,6 +7,7 @@ using DiscordChatExporter.Cli.Tests.Infra;
|
|||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
using DiscordChatExporter.Cli.Tests.Utils;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using PowerKit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Specs;
|
namespace DiscordChatExporter.Cli.Tests.Specs;
|
||||||
@@ -17,7 +18,7 @@ public class SelfContainedSpecs
|
|||||||
public async Task I_can_export_a_channel_and_download_all_referenced_assets()
|
public async Task I_can_export_a_channel_and_download_all_referenced_assets()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
using var dir = TempDir.Create();
|
using var dir = TempDirectory.Create();
|
||||||
var filePath = Path.Combine(dir.Path, "output.html");
|
var filePath = Path.Combine(dir.Path, "output.html");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Utils.Extensions;
|
|
||||||
|
|
||||||
internal static class StringExtensions
|
|
||||||
{
|
|
||||||
extension(string str)
|
|
||||||
{
|
|
||||||
public string ReplaceWhiteSpace(string replacement = " ")
|
|
||||||
{
|
|
||||||
var buffer = new StringBuilder(str.Length);
|
|
||||||
|
|
||||||
foreach (var ch in str)
|
|
||||||
buffer.Append(char.IsWhiteSpace(ch) ? replacement : ch);
|
|
||||||
|
|
||||||
return buffer.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using AngleSharp.Html.Dom;
|
using AngleSharp.Html.Dom;
|
||||||
using AngleSharp.Html.Parser;
|
using AngleSharp.Html.Parser;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Utils;
|
namespace DiscordChatExporter.Cli.Tests.Utils;
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Utils;
|
|
||||||
|
|
||||||
internal partial class TempDir(string path) : IDisposable
|
|
||||||
{
|
|
||||||
public string Path { get; } = path;
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(Path, true);
|
|
||||||
}
|
|
||||||
catch (DirectoryNotFoundException) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class TempDir
|
|
||||||
{
|
|
||||||
public static TempDir Create()
|
|
||||||
{
|
|
||||||
var dirPath = System.IO.Path.Combine(
|
|
||||||
System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
|
|
||||||
?? Directory.GetCurrentDirectory(),
|
|
||||||
"Temp",
|
|
||||||
Guid.NewGuid().ToString()
|
|
||||||
);
|
|
||||||
|
|
||||||
Directory.CreateDirectory(dirPath);
|
|
||||||
|
|
||||||
return new TempDir(dirPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Utils;
|
|
||||||
|
|
||||||
internal partial class TempFile(string path) : IDisposable
|
|
||||||
{
|
|
||||||
public string Path { get; } = path;
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete(Path);
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class TempFile
|
|
||||||
{
|
|
||||||
public static TempFile Create()
|
|
||||||
{
|
|
||||||
var dirPath = System.IO.Path.Combine(
|
|
||||||
System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
|
|
||||||
?? Directory.GetCurrentDirectory(),
|
|
||||||
"Temp"
|
|
||||||
);
|
|
||||||
|
|
||||||
Directory.CreateDirectory(dirPath);
|
|
||||||
|
|
||||||
var filePath = System.IO.Path.Combine(dirPath, Guid.NewGuid() + ".tmp");
|
|
||||||
|
|
||||||
return new TempFile(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||||
"methodDisplayOptions": "all",
|
"methodDisplayOptions": "all",
|
||||||
"methodDisplay": "method"
|
"methodDisplay": "method"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ WORKDIR /tmp/app
|
|||||||
COPY favicon.ico .
|
COPY favicon.ico .
|
||||||
COPY NuGet.config .
|
COPY NuGet.config .
|
||||||
COPY Directory.Build.props .
|
COPY Directory.Build.props .
|
||||||
|
COPY Directory.Packages.props .
|
||||||
COPY DiscordChatExporter.Core DiscordChatExporter.Core
|
COPY DiscordChatExporter.Core DiscordChatExporter.Core
|
||||||
COPY DiscordChatExporter.Cli DiscordChatExporter.Cli
|
COPY DiscordChatExporter.Cli DiscordChatExporter.Cli
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Binding;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Utils;
|
using DiscordChatExporter.Core.Utils;
|
||||||
@@ -17,23 +17,22 @@ public abstract class DiscordCommandBase : ICommand
|
|||||||
EnvironmentVariable = "DISCORD_TOKEN",
|
EnvironmentVariable = "DISCORD_TOKEN",
|
||||||
Description = "Authentication token."
|
Description = "Authentication token."
|
||||||
)]
|
)]
|
||||||
public required string Token { get; init; }
|
public required string Token { get; set; }
|
||||||
|
|
||||||
[Obsolete("This option doesn't do anything. Kept for backwards compatibility.")]
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"bot",
|
"bot",
|
||||||
'b',
|
'b',
|
||||||
EnvironmentVariable = "DISCORD_TOKEN_BOT",
|
EnvironmentVariable = "DISCORD_TOKEN_BOT",
|
||||||
Description = "This option doesn't do anything. Kept for backwards compatibility."
|
Description = "This option doesn't do anything. Kept for backwards compatibility."
|
||||||
)]
|
)]
|
||||||
public bool IsBotToken { get; init; } = false;
|
public bool IsBotToken { get; set; } = false;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"respect-rate-limits",
|
"respect-rate-limits",
|
||||||
Description = "Whether to respect advisory rate limits. "
|
Description = "Whether to respect advisory rate limits. "
|
||||||
+ "If disabled, only hard rate limits (i.e. 429 responses) will be respected."
|
+ "If disabled, only hard rate limits (i.e. 429 responses) will be respected."
|
||||||
)]
|
)]
|
||||||
public bool ShouldRespectRateLimits { get; init; } = true;
|
public bool ShouldRespectRateLimits { get; set; } = true;
|
||||||
|
|
||||||
[field: AllowNull, MaybeNull]
|
[field: AllowNull, MaybeNull]
|
||||||
protected DiscordClient Discord =>
|
protected DiscordClient Discord =>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Binding;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Converters;
|
using DiscordChatExporter.Cli.Commands.Converters;
|
||||||
using DiscordChatExporter.Cli.Commands.Shared;
|
using DiscordChatExporter.Cli.Commands.Shared;
|
||||||
@@ -37,23 +37,23 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||||||
get;
|
get;
|
||||||
// Handle ~/ in paths on Unix systems
|
// Handle ~/ in paths on Unix systems
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/pull/903
|
// https://github.com/Tyrrrz/DiscordChatExporter/pull/903
|
||||||
init => field = Path.GetFullPath(value);
|
set => field = Path.GetFullPath(value);
|
||||||
} = Directory.GetCurrentDirectory();
|
} = Directory.GetCurrentDirectory();
|
||||||
|
|
||||||
[CommandOption("format", 'f', Description = "Export format.")]
|
[CommandOption("format", 'f', Description = "Export format.")]
|
||||||
public ExportFormat ExportFormat { get; init; } = ExportFormat.HtmlDark;
|
public ExportFormat ExportFormat { get; set; } = ExportFormat.HtmlDark;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"after",
|
"after",
|
||||||
Description = "Only include messages sent after this date or message ID."
|
Description = "Only include messages sent after this date or message ID."
|
||||||
)]
|
)]
|
||||||
public Snowflake? After { get; init; }
|
public Snowflake? After { get; set; }
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"before",
|
"before",
|
||||||
Description = "Only include messages sent before this date or message ID."
|
Description = "Only include messages sent before this date or message ID."
|
||||||
)]
|
)]
|
||||||
public Snowflake? Before { get; init; }
|
public Snowflake? Before { get; set; }
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"partition",
|
"partition",
|
||||||
@@ -61,45 +61,51 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||||||
Description = "Split the output into partitions, each limited to the specified "
|
Description = "Split the output into partitions, each limited to the specified "
|
||||||
+ "number of messages (e.g. '100') or file size (e.g. '10mb')."
|
+ "number of messages (e.g. '100') or file size (e.g. '10mb')."
|
||||||
)]
|
)]
|
||||||
public PartitionLimit PartitionLimit { get; init; } = PartitionLimit.Null;
|
public PartitionLimit PartitionLimit { get; set; } = PartitionLimit.Null;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"include-threads",
|
"include-threads",
|
||||||
Description = "Which types of threads should be included.",
|
Description = "Which types of threads should be included.",
|
||||||
Converter = typeof(ThreadInclusionModeBindingConverter)
|
Converter = typeof(ThreadInclusionModeInputConverter)
|
||||||
)]
|
)]
|
||||||
public ThreadInclusionMode ThreadInclusionMode { get; init; } = ThreadInclusionMode.None;
|
public ThreadInclusionMode ThreadInclusionMode { get; set; } = ThreadInclusionMode.None;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"filter",
|
"filter",
|
||||||
Description = "Only include messages that satisfy this filter. "
|
Description = "Only include messages that satisfy this filter. "
|
||||||
+ "See the documentation for more info."
|
+ "See the documentation for more info."
|
||||||
)]
|
)]
|
||||||
public MessageFilter MessageFilter { get; init; } = MessageFilter.Null;
|
public MessageFilter MessageFilter { get; set; } = MessageFilter.Null;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"parallel",
|
"parallel",
|
||||||
Description = "Limits how many channels can be exported in parallel."
|
Description = "Limits how many channels can be exported in parallel."
|
||||||
)]
|
)]
|
||||||
public int ParallelLimit { get; init; } = 1;
|
public int ParallelLimit { get; set; } = 1;
|
||||||
|
|
||||||
|
[CommandOption(
|
||||||
|
"reverse",
|
||||||
|
Description = "Export messages in reverse chronological order (newest first)."
|
||||||
|
)]
|
||||||
|
public bool IsReverseMessageOrder { get; set; }
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"markdown",
|
"markdown",
|
||||||
Description = "Process markdown, mentions, and other special tokens."
|
Description = "Process markdown, mentions, and other special tokens."
|
||||||
)]
|
)]
|
||||||
public bool ShouldFormatMarkdown { get; init; } = true;
|
public bool ShouldFormatMarkdown { get; set; } = true;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"media",
|
"media",
|
||||||
Description = "Download assets referenced by the export (user avatars, attached files, embedded images, etc.)."
|
Description = "Download assets referenced by the export (user avatars, attached files, embedded images, etc.)."
|
||||||
)]
|
)]
|
||||||
public bool ShouldDownloadAssets { get; init; }
|
public bool ShouldDownloadAssets { get; set; }
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"reuse-media",
|
"reuse-media",
|
||||||
Description = "Reuse previously downloaded assets to avoid redundant requests."
|
Description = "Reuse previously downloaded assets to avoid redundant requests."
|
||||||
)]
|
)]
|
||||||
public bool ShouldReuseAssets { get; init; } = false;
|
public bool ShouldReuseAssets { get; set; } = false;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"media-dir",
|
"media-dir",
|
||||||
@@ -111,34 +117,33 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||||||
get;
|
get;
|
||||||
// Handle ~/ in paths on Unix systems
|
// Handle ~/ in paths on Unix systems
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/pull/903
|
// https://github.com/Tyrrrz/DiscordChatExporter/pull/903
|
||||||
init => field = value is not null ? Path.GetFullPath(value) : null;
|
set => field = value is not null ? Path.GetFullPath(value) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("This option doesn't do anything. Kept for backwards compatibility.")]
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"dateformat",
|
"dateformat",
|
||||||
Description = "This option doesn't do anything. Kept for backwards compatibility."
|
Description = "This option doesn't do anything. Kept for backwards compatibility."
|
||||||
)]
|
)]
|
||||||
public string DateFormat { get; init; } = "MM/dd/yyyy h:mm tt";
|
public string DateFormat { get; set; } = "MM/dd/yyyy h:mm tt";
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"locale",
|
"locale",
|
||||||
Description = "Locale to use when formatting dates and numbers. "
|
Description = "Locale to use when formatting dates and numbers. "
|
||||||
+ "If not specified, the default system locale will be used."
|
+ "If not specified, the default system locale will be used."
|
||||||
)]
|
)]
|
||||||
public string? Locale { get; init; }
|
public string? Locale { get; set; }
|
||||||
|
|
||||||
[CommandOption("utc", Description = "Normalize all timestamps to UTC+0.")]
|
[CommandOption("utc", Description = "Normalize all timestamps to UTC+0.")]
|
||||||
public bool IsUtcNormalizationEnabled { get; init; } = false;
|
public bool IsUtcNormalizationEnabled { get; set; } = false;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"fuck-russia",
|
"fuck-russia",
|
||||||
EnvironmentVariable = "FUCK_RUSSIA",
|
EnvironmentVariable = "FUCK_RUSSIA",
|
||||||
Description = "Don't print the Support Ukraine message to the console.",
|
Description = "Don't print the Support Ukraine message to the console.",
|
||||||
// Use a converter to accept '1' as 'true' to reuse the existing environment variable
|
// Use a converter to accept '1' as 'true' to reuse the existing environment variable
|
||||||
Converter = typeof(TruthyBooleanBindingConverter)
|
Converter = typeof(TruthyBooleanInputConverter)
|
||||||
)]
|
)]
|
||||||
public bool IsUkraineSupportMessageDisabled { get; init; } = false;
|
public bool IsUkraineSupportMessageDisabled { get; set; } = false;
|
||||||
|
|
||||||
[field: AllowNull, MaybeNull]
|
[field: AllowNull, MaybeNull]
|
||||||
protected ChannelExporter Exporter => field ??= new ChannelExporter(Discord);
|
protected ChannelExporter Exporter => field ??= new ChannelExporter(Discord);
|
||||||
@@ -160,6 +165,37 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||||||
throw new CommandException("Option --media-dir cannot be used without --media.");
|
throw new CommandException("Option --media-dir cannot be used without --media.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure the user does not try to export multiple channels into one file.
|
||||||
|
// Output path must either be a directory or contain template tokens for this to work.
|
||||||
|
// Validate this up-front, before fetching threads, because thread fetching can take a
|
||||||
|
// long time and it's frustrating to fail only after it completes.
|
||||||
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/799
|
||||||
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/917
|
||||||
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1549
|
||||||
|
var mayExportMultipleChannels =
|
||||||
|
// Multiple channels were provided explicitly
|
||||||
|
channels.Count > 1
|
||||||
|
// Thread inclusion can add more channels to the export
|
||||||
|
|| ThreadInclusionMode != ThreadInclusionMode.None;
|
||||||
|
|
||||||
|
var isValidOutputPath =
|
||||||
|
// Anything is valid when exporting a single channel
|
||||||
|
!mayExportMultipleChannels
|
||||||
|
// When using template tokens, assume the user knows what they're doing
|
||||||
|
|| OutputPath.Contains('%')
|
||||||
|
// Otherwise, require an existing directory or an unambiguous directory path
|
||||||
|
|| Directory.Exists(OutputPath)
|
||||||
|
|| Path.EndsInDirectorySeparator(OutputPath);
|
||||||
|
|
||||||
|
if (!isValidOutputPath)
|
||||||
|
{
|
||||||
|
throw new CommandException(
|
||||||
|
"Attempted to export multiple channels, but the output path is neither a directory nor a template. "
|
||||||
|
+ "If the provided output path is meant to be treated as a directory, make sure it ends with a slash. "
|
||||||
|
+ $"Provided output path: '{OutputPath}'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
var unwrappedChannels = new List<Channel>(channels);
|
var unwrappedChannels = new List<Channel>(channels);
|
||||||
|
|
||||||
// Unwrap threads
|
// Unwrap threads
|
||||||
@@ -200,28 +236,6 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||||||
await console.Output.WriteLineAsync($"Fetched {fetchedThreadsCount} thread(s).");
|
await console.Output.WriteLineAsync($"Fetched {fetchedThreadsCount} thread(s).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the user does not try to export multiple channels into one file.
|
|
||||||
// Output path must either be a directory or contain template tokens for this to work.
|
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/799
|
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/917
|
|
||||||
var isValidOutputPath =
|
|
||||||
// Anything is valid when exporting a single channel
|
|
||||||
unwrappedChannels.Count <= 1
|
|
||||||
// When using template tokens, assume the user knows what they're doing
|
|
||||||
|| OutputPath.Contains('%')
|
|
||||||
// Otherwise, require an existing directory or an unambiguous directory path
|
|
||||||
|| Directory.Exists(OutputPath)
|
|
||||||
|| Path.EndsInDirectorySeparator(OutputPath);
|
|
||||||
|
|
||||||
if (!isValidOutputPath)
|
|
||||||
{
|
|
||||||
throw new CommandException(
|
|
||||||
"Attempted to export multiple channels, but the output path is neither a directory nor a template. "
|
|
||||||
+ "If the provided output path is meant to be treated as a directory, make sure it ends with a slash. "
|
|
||||||
+ $"Provided output path: '{OutputPath}'."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export
|
// Export
|
||||||
var errorsByChannel = new ConcurrentDictionary<Channel, string>();
|
var errorsByChannel = new ConcurrentDictionary<Channel, string>();
|
||||||
var warningsByChannel = new ConcurrentDictionary<Channel, string>();
|
var warningsByChannel = new ConcurrentDictionary<Channel, string>();
|
||||||
@@ -267,6 +281,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
|||||||
Before,
|
Before,
|
||||||
PartitionLimit,
|
PartitionLimit,
|
||||||
MessageFilter,
|
MessageFilter,
|
||||||
|
IsReverseMessageOrder,
|
||||||
ShouldFormatMarkdown,
|
ShouldFormatMarkdown,
|
||||||
ShouldDownloadAssets,
|
ShouldDownloadAssets,
|
||||||
ShouldReuseAssets,
|
ShouldReuseAssets,
|
||||||
|
|||||||
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using CliFx.Extensibility;
|
using CliFx.Activation;
|
||||||
using DiscordChatExporter.Cli.Commands.Shared;
|
using DiscordChatExporter.Cli.Commands.Shared;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands.Converters;
|
namespace DiscordChatExporter.Cli.Commands.Converters;
|
||||||
|
|
||||||
internal class ThreadInclusionModeBindingConverter : BindingConverter<ThreadInclusionMode>
|
internal class ThreadInclusionModeInputConverter : ScalarInputConverter<ThreadInclusionMode>
|
||||||
{
|
{
|
||||||
public override ThreadInclusionMode Convert(string? rawValue)
|
public override ThreadInclusionMode Convert(string? rawValue)
|
||||||
{
|
{
|
||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using CliFx.Extensibility;
|
using CliFx.Activation;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands.Converters;
|
namespace DiscordChatExporter.Cli.Commands.Converters;
|
||||||
|
|
||||||
internal class TruthyBooleanBindingConverter : BindingConverter<bool>
|
internal class TruthyBooleanInputConverter : ScalarInputConverter<bool>
|
||||||
{
|
{
|
||||||
public override bool Convert(string? rawValue)
|
public override bool Convert(string? rawValue)
|
||||||
{
|
{
|
||||||
@@ -2,7 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Binding;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Cli.Utils.Extensions;
|
using DiscordChatExporter.Cli.Utils.Extensions;
|
||||||
@@ -14,23 +14,23 @@ using Spectre.Console;
|
|||||||
namespace DiscordChatExporter.Cli.Commands;
|
namespace DiscordChatExporter.Cli.Commands;
|
||||||
|
|
||||||
[Command("exportall", Description = "Exports all accessible channels.")]
|
[Command("exportall", Description = "Exports all accessible channels.")]
|
||||||
public class ExportAllCommand : ExportCommandBase
|
public partial class ExportAllCommand : ExportCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("include-dm", Description = "Include direct message channels.")]
|
[CommandOption("include-dm", Description = "Include direct message channels.")]
|
||||||
public bool IncludeDirectChannels { get; init; } = true;
|
public bool IncludeDirectChannels { get; set; } = true;
|
||||||
|
|
||||||
[CommandOption("include-guilds", Description = "Include server channels.")]
|
[CommandOption("include-guilds", Description = "Include server channels.")]
|
||||||
public bool IncludeGuildChannels { get; init; } = true;
|
public bool IncludeGuildChannels { get; set; } = true;
|
||||||
|
|
||||||
[CommandOption("include-vc", Description = "Include voice channels.")]
|
[CommandOption("include-vc", Description = "Include voice channels.")]
|
||||||
public bool IncludeVoiceChannels { get; init; } = true;
|
public bool IncludeVoiceChannels { get; set; } = true;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"data-package",
|
"data-package",
|
||||||
Description = "Path to the personal data package (ZIP file) requested from Discord. "
|
Description = "Path to the personal data package (ZIP file) requested from Discord. "
|
||||||
+ "If provided, only channels referenced in the dump will be exported."
|
+ "If provided, only channels referenced in the dump will be exported."
|
||||||
)]
|
)]
|
||||||
public string? DataPackageFilePath { get; init; }
|
public string? DataPackageFilePath { get; set; }
|
||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Binding;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands;
|
namespace DiscordChatExporter.Cli.Commands;
|
||||||
|
|
||||||
[Command("export", Description = "Exports one or multiple channels.")]
|
[Command("export", Description = "Exports one or multiple channels.")]
|
||||||
public class ExportChannelsCommand : ExportCommandBase
|
public partial class ExportChannelsCommand : ExportCommandBase
|
||||||
{
|
{
|
||||||
// TODO: change this to plural (breaking change)
|
// TODO: change this to plural (breaking change)
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
@@ -19,7 +19,7 @@ public class ExportChannelsCommand : ExportCommandBase
|
|||||||
Description = "Channel ID(s). "
|
Description = "Channel ID(s). "
|
||||||
+ "If provided with category ID(s), all channels inside those categories will be exported."
|
+ "If provided with category ID(s), all channels inside those categories will be exported."
|
||||||
)]
|
)]
|
||||||
public required IReadOnlyList<Snowflake> ChannelIds { get; init; }
|
public required IReadOnlyList<Snowflake> ChannelIds { get; set; }
|
||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Binding;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands;
|
namespace DiscordChatExporter.Cli.Commands;
|
||||||
|
|
||||||
[Command("exportdm", Description = "Exports all direct message channels.")]
|
[Command("exportdm", Description = "Exports all direct message channels.")]
|
||||||
public class ExportDirectMessagesCommand : ExportCommandBase
|
public partial class ExportDirectMessagesCommand : ExportCommandBase
|
||||||
{
|
{
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Binding;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Cli.Utils.Extensions;
|
using DiscordChatExporter.Cli.Utils.Extensions;
|
||||||
@@ -11,13 +11,13 @@ using Spectre.Console;
|
|||||||
namespace DiscordChatExporter.Cli.Commands;
|
namespace DiscordChatExporter.Cli.Commands;
|
||||||
|
|
||||||
[Command("exportguild", Description = "Exports all channels within the specified server.")]
|
[Command("exportguild", Description = "Exports all channels within the specified server.")]
|
||||||
public class ExportGuildCommand : ExportCommandBase
|
public partial class ExportGuildCommand : ExportCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("guild", 'g', Description = "Server ID.")]
|
[CommandOption("guild", 'g', Description = "Server ID.")]
|
||||||
public required Snowflake GuildId { get; init; }
|
public required Snowflake GuildId { get; set; }
|
||||||
|
|
||||||
[CommandOption("include-vc", Description = "Include voice channels.")]
|
[CommandOption("include-vc", Description = "Include voice channels.")]
|
||||||
public bool IncludeVoiceChannels { get; init; } = true;
|
public bool IncludeVoiceChannels { get; set; } = true;
|
||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Binding;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Cli.Commands.Converters;
|
using DiscordChatExporter.Cli.Commands.Converters;
|
||||||
using DiscordChatExporter.Cli.Commands.Shared;
|
using DiscordChatExporter.Cli.Commands.Shared;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands;
|
namespace DiscordChatExporter.Cli.Commands;
|
||||||
|
|
||||||
[Command("channels", Description = "Get the list of channels in a server.")]
|
[Command("channels", Description = "Get the list of channels in a server.")]
|
||||||
public class GetChannelsCommand : DiscordCommandBase
|
public partial class GetChannelsCommand : DiscordCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("guild", 'g', Description = "Server ID.")]
|
[CommandOption("guild", 'g', Description = "Server ID.")]
|
||||||
public required Snowflake GuildId { get; init; }
|
public required Snowflake GuildId { get; set; }
|
||||||
|
|
||||||
[CommandOption("include-vc", Description = "Include voice channels.")]
|
[CommandOption("include-vc", Description = "Include voice channels.")]
|
||||||
public bool IncludeVoiceChannels { get; init; } = true;
|
public bool IncludeVoiceChannels { get; set; } = true;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"include-threads",
|
"include-threads",
|
||||||
Description = "Which types of threads should be included.",
|
Description = "Which types of threads should be included.",
|
||||||
Converter = typeof(ThreadInclusionModeBindingConverter)
|
Converter = typeof(ThreadInclusionModeInputConverter)
|
||||||
)]
|
)]
|
||||||
public ThreadInclusionMode ThreadInclusionMode { get; init; } = ThreadInclusionMode.None;
|
public ThreadInclusionMode ThreadInclusionMode { get; set; } = ThreadInclusionMode.None;
|
||||||
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Binding;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands;
|
namespace DiscordChatExporter.Cli.Commands;
|
||||||
|
|
||||||
[Command("dm", Description = "Gets the list of all direct message channels.")]
|
[Command("dm", Description = "Gets the list of all direct message channels.")]
|
||||||
public class GetDirectChannelsCommand : DiscordCommandBase
|
public partial class GetDirectChannelsCommand : DiscordCommandBase
|
||||||
{
|
{
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Binding;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands;
|
namespace DiscordChatExporter.Cli.Commands;
|
||||||
|
|
||||||
[Command("guilds", Description = "Gets the list of accessible servers.")]
|
[Command("guilds", Description = "Gets the list of accessible servers.")]
|
||||||
public class GetGuildsCommand : DiscordCommandBase
|
public partial class GetGuildsCommand : DiscordCommandBase
|
||||||
{
|
{
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using CliFx.Attributes;
|
using CliFx.Binding;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands;
|
namespace DiscordChatExporter.Cli.Commands;
|
||||||
|
|
||||||
[Command("guide", Description = "Explains how to obtain the token, server or channel ID.")]
|
[Command("guide", Description = "Explains how to obtain the token, server or channel ID.")]
|
||||||
public class GuideCommand : ICommand
|
public partial class GuideCommand : ICommand
|
||||||
{
|
{
|
||||||
public ValueTask ExecuteAsync(IConsole console)
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
@@ -74,7 +74,7 @@ public class GuideCommand : ICommand
|
|||||||
using (console.WithForegroundColor(ConsoleColor.DarkCyan))
|
using (console.WithForegroundColor(ConsoleColor.DarkCyan))
|
||||||
{
|
{
|
||||||
console.Output.WriteLine(
|
console.Output.WriteLine(
|
||||||
"https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs"
|
"https://github.com/Tyrrrz/DiscordChatExporter/blob/prime/.docs"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Cli.Commands.Shared;
|
namespace DiscordChatExporter.Cli.Commands.Shared;
|
||||||
|
|
||||||
public enum ThreadInclusionMode
|
public enum ThreadInclusionMode
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<ApplicationIcon>..\favicon.ico</ApplicationIcon>
|
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||||
<PublishTrimmed>true</PublishTrimmed>
|
<PublishTrimmed>true</PublishTrimmed>
|
||||||
<CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>
|
<CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- HACK: Disable trim warnings because they seem to break when the code contains C# 14 extension blocks -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
|
|
||||||
<EnableAotAnalyzer>false</EnableAotAnalyzer>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CliFx" Version="2.3.6" />
|
<PackageReference Include="CliFx" />
|
||||||
<PackageReference Include="CSharpier.MsBuild" Version="1.2.5" PrivateAssets="all" />
|
<PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" />
|
||||||
<PackageReference Include="Deorcify" Version="1.1.0" PrivateAssets="all" />
|
<PackageReference Include="Deorcify" />
|
||||||
<PackageReference Include="Gress" Version="2.1.1" />
|
<PackageReference Include="Gress" />
|
||||||
<PackageReference Include="Spectre.Console" Version="0.54.0" />
|
<PackageReference Include="PowerKit" />
|
||||||
|
<PackageReference Include="Spectre.Console" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DiscordChatExporter.Core\DiscordChatExporter.Core.csproj" />
|
<ProjectReference Include="../DiscordChatExporter.Core/DiscordChatExporter.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,41 +1,13 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx;
|
using CliFx;
|
||||||
using DiscordChatExporter.Cli.Commands;
|
|
||||||
using DiscordChatExporter.Cli.Commands.Converters;
|
|
||||||
using DiscordChatExporter.Core.Exporting.Filtering;
|
|
||||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli;
|
namespace DiscordChatExporter.Cli;
|
||||||
|
|
||||||
public static class Program
|
public static class Program
|
||||||
{
|
{
|
||||||
// Explicit references because CliFx relies on reflection and we're publishing with trimming enabled
|
|
||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportAllCommand))]
|
|
||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportChannelsCommand))]
|
|
||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportDirectMessagesCommand))]
|
|
||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ExportGuildCommand))]
|
|
||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GetChannelsCommand))]
|
|
||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GetDirectChannelsCommand))]
|
|
||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GetGuildsCommand))]
|
|
||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(GuideCommand))]
|
|
||||||
[DynamicDependency(
|
|
||||||
DynamicallyAccessedMemberTypes.All,
|
|
||||||
typeof(ThreadInclusionModeBindingConverter)
|
|
||||||
)]
|
|
||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(TruthyBooleanBindingConverter))]
|
|
||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(PartitionLimit))]
|
|
||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MessageFilter))]
|
|
||||||
public static async Task<int> Main(string[] args) =>
|
public static async Task<int> Main(string[] args) =>
|
||||||
await new CliApplicationBuilder()
|
await new CommandLineApplicationBuilder()
|
||||||
.AddCommand<ExportAllCommand>()
|
.AddCommandsFromThisAssembly()
|
||||||
.AddCommand<ExportChannelsCommand>()
|
|
||||||
.AddCommand<ExportDirectMessagesCommand>()
|
|
||||||
.AddCommand<ExportGuildCommand>()
|
|
||||||
.AddCommand<GetChannelsCommand>()
|
|
||||||
.AddCommand<GetDirectChannelsCommand>()
|
|
||||||
.AddCommand<GetGuildsCommand>()
|
|
||||||
.AddCommand<GuideCommand>()
|
|
||||||
.Build()
|
.Build()
|
||||||
.RunAsync(args);
|
.RunAsync(args);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||||
public enum ChannelKind
|
public enum ChannelKind
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Discord.Data.Common;
|
namespace DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
|
|
||||||
public interface IHasId
|
public interface IHasId
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ using System.Collections.Generic;
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
|
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
|
||||||
|
|
||||||
@@ -47,9 +47,7 @@ public partial record Embed
|
|||||||
var kind =
|
var kind =
|
||||||
json.GetPropertyOrNull("type")
|
json.GetPropertyOrNull("type")
|
||||||
?.GetStringOrNull()
|
?.GetStringOrNull()
|
||||||
?.Pipe(s =>
|
.Pipe(s => Enum.ParseOrNull<EmbedKind>(s, true))
|
||||||
Enum.TryParse<EmbedKind>(s, true, out var result) ? result : (EmbedKind?)null
|
|
||||||
)
|
|
||||||
?? EmbedKind.Rich;
|
?? EmbedKind.Rich;
|
||||||
|
|
||||||
var url = json.GetPropertyOrNull("url")?.GetNonWhiteSpaceStringOrNull();
|
var url = json.GetPropertyOrNull("url")?.GetNonWhiteSpaceStringOrNull();
|
||||||
@@ -58,7 +56,7 @@ public partial record Embed
|
|||||||
var color = json.GetPropertyOrNull("color")
|
var color = json.GetPropertyOrNull("color")
|
||||||
?.GetInt32OrNull()
|
?.GetInt32OrNull()
|
||||||
?.Pipe(System.Drawing.Color.FromArgb)
|
?.Pipe(System.Drawing.Color.FromArgb)
|
||||||
.ResetAlpha();
|
.WithFullAlpha();
|
||||||
|
|
||||||
var author = json.GetPropertyOrNull("author")?.Pipe(EmbedAuthor.Parse);
|
var author = json.GetPropertyOrNull("author")?.Pipe(EmbedAuthor.Parse);
|
||||||
var description = json.GetPropertyOrNull("description")?.GetStringOrNull();
|
var description = json.GetPropertyOrNull("description")?.GetStringOrNull();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
|
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
|
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
|
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
|
||||||
|
|
||||||
public partial record YouTubeVideoEmbedProjection(string VideoId)
|
public partial record YouTubeVideoEmbedProjection(string VideoId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Utils;
|
using DiscordChatExporter.Core.Utils;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
@@ -8855,6 +8855,8 @@ internal static class EmojiIndex
|
|||||||
["united_nations"] = "🇺🇳",
|
["united_nations"] = "🇺🇳",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static IReadOnlyCollection<string> GetAllNames() => _toCodes.Keys;
|
||||||
|
|
||||||
public static string? TryGetCode(string name) => _toCodes.GetValueOrDefault(name);
|
public static string? TryGetCode(string name) => _toCodes.GetValueOrDefault(name);
|
||||||
|
|
||||||
public static string? TryGetName(string code) => _fromCodes.GetValueOrDefault(code);
|
public static string? TryGetName(string code) => _fromCodes.GetValueOrDefault(code);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Embeds;
|
using DiscordChatExporter.Core.Discord.Data.Embeds;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
@@ -27,9 +27,16 @@ public partial record Message(
|
|||||||
IReadOnlyList<User> MentionedUsers,
|
IReadOnlyList<User> MentionedUsers,
|
||||||
MessageReference? Reference,
|
MessageReference? Reference,
|
||||||
Message? ReferencedMessage,
|
Message? ReferencedMessage,
|
||||||
|
MessageSnapshot? ForwardedMessage,
|
||||||
Interaction? Interaction
|
Interaction? Interaction
|
||||||
) : IHasId
|
) : IHasId
|
||||||
{
|
{
|
||||||
|
public bool IsEmpty { get; } =
|
||||||
|
string.IsNullOrWhiteSpace(Content)
|
||||||
|
&& !Attachments.Any()
|
||||||
|
&& !Embeds.Any()
|
||||||
|
&& !Stickers.Any();
|
||||||
|
|
||||||
public bool IsSystemNotification { get; } =
|
public bool IsSystemNotification { get; } =
|
||||||
Kind is >= MessageKind.RecipientAdd and <= MessageKind.ThreadCreated;
|
Kind is >= MessageKind.RecipientAdd and <= MessageKind.ThreadCreated;
|
||||||
|
|
||||||
@@ -38,11 +45,7 @@ public partial record Message(
|
|||||||
// App interactions are rendered as replies in the Discord client, but they are not actually replies
|
// App interactions are rendered as replies in the Discord client, but they are not actually replies
|
||||||
public bool IsReplyLike => IsReply || Interaction is not null;
|
public bool IsReplyLike => IsReply || Interaction is not null;
|
||||||
|
|
||||||
public bool IsEmpty { get; } =
|
public bool IsForwarded { get; } = Reference?.Kind == MessageReferenceKind.Forward;
|
||||||
string.IsNullOrWhiteSpace(Content)
|
|
||||||
&& !Attachments.Any()
|
|
||||||
&& !Embeds.Any()
|
|
||||||
&& !Stickers.Any();
|
|
||||||
|
|
||||||
public IEnumerable<User> GetReferencedUsers()
|
public IEnumerable<User> GetReferencedUsers()
|
||||||
{
|
{
|
||||||
@@ -171,7 +174,17 @@ public partial record Message
|
|||||||
|
|
||||||
var messageReference = json.GetPropertyOrNull("message_reference")
|
var messageReference = json.GetPropertyOrNull("message_reference")
|
||||||
?.Pipe(MessageReference.Parse);
|
?.Pipe(MessageReference.Parse);
|
||||||
|
|
||||||
var referencedMessage = json.GetPropertyOrNull("referenced_message")?.Pipe(Parse);
|
var referencedMessage = json.GetPropertyOrNull("referenced_message")?.Pipe(Parse);
|
||||||
|
|
||||||
|
// Currently Discord only supports 1 snapshot per forward
|
||||||
|
var forwardedMessage = json.GetPropertyOrNull("message_snapshots")
|
||||||
|
?.EnumerateArrayOrNull()
|
||||||
|
?.Select(j => j.GetPropertyOrNull("message"))
|
||||||
|
.WhereNotNull()
|
||||||
|
.Select(MessageSnapshot.Parse)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
var interaction = json.GetPropertyOrNull("interaction")?.Pipe(Interaction.Parse);
|
var interaction = json.GetPropertyOrNull("interaction")?.Pipe(Interaction.Parse);
|
||||||
|
|
||||||
return new Message(
|
return new Message(
|
||||||
@@ -191,6 +204,7 @@ public partial record Message
|
|||||||
mentionedUsers,
|
mentionedUsers,
|
||||||
messageReference,
|
messageReference,
|
||||||
referencedMessage,
|
referencedMessage,
|
||||||
|
forwardedMessage,
|
||||||
interaction
|
interaction
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
// https://discord.com/developers/docs/resources/channel#message-object-message-types
|
// https://discord.com/developers/docs/resources/channel#message-object-message-types
|
||||||
public enum MessageKind
|
public enum MessageKind
|
||||||
@@ -13,4 +13,5 @@ public enum MessageKind
|
|||||||
GuildMemberJoin = 7,
|
GuildMemberJoin = 7,
|
||||||
ThreadCreated = 18,
|
ThreadCreated = 18,
|
||||||
Reply = 19,
|
Reply = 19,
|
||||||
|
ThreadStarterMessage = 21,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure
|
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure
|
||||||
public record MessageReference(Snowflake? MessageId, Snowflake? ChannelId, Snowflake? GuildId)
|
public record MessageReference(
|
||||||
|
MessageReferenceKind Kind,
|
||||||
|
Snowflake? MessageId,
|
||||||
|
Snowflake? ChannelId,
|
||||||
|
Snowflake? GuildId
|
||||||
|
)
|
||||||
{
|
{
|
||||||
public static MessageReference Parse(JsonElement json)
|
public static MessageReference Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
|
var kind =
|
||||||
|
json.GetPropertyOrNull("type")?.GetInt32OrNull()?.Pipe(t => (MessageReferenceKind)t)
|
||||||
|
?? MessageReferenceKind.Default;
|
||||||
|
|
||||||
var messageId = json.GetPropertyOrNull("message_id")
|
var messageId = json.GetPropertyOrNull("message_id")
|
||||||
?.GetNonWhiteSpaceStringOrNull()
|
?.GetNonWhiteSpaceStringOrNull()
|
||||||
?.Pipe(Snowflake.Parse);
|
?.Pipe(Snowflake.Parse);
|
||||||
@@ -21,6 +30,6 @@ public record MessageReference(Snowflake? MessageId, Snowflake? ChannelId, Snowf
|
|||||||
?.GetNonWhiteSpaceStringOrNull()
|
?.GetNonWhiteSpaceStringOrNull()
|
||||||
?.Pipe(Snowflake.Parse);
|
?.Pipe(Snowflake.Parse);
|
||||||
|
|
||||||
return new MessageReference(messageId, channelId, guildId);
|
return new MessageReference(kind, messageId, channelId, guildId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
// https://discord.com/developers/docs/resources/channel#message-reference-types
|
||||||
|
public enum MessageReferenceKind
|
||||||
|
{
|
||||||
|
Default = 0,
|
||||||
|
Forward = 1,
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using DiscordChatExporter.Core.Discord.Data.Embeds;
|
||||||
|
using JsonExtensions.Reading;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
// https://docs.discord.com/developers/resources/message#message-snapshot-object
|
||||||
|
public record MessageSnapshot(
|
||||||
|
DateTimeOffset Timestamp,
|
||||||
|
DateTimeOffset? EditedTimestamp,
|
||||||
|
string Content,
|
||||||
|
IReadOnlyList<Attachment> Attachments,
|
||||||
|
IReadOnlyList<Embed> Embeds,
|
||||||
|
IReadOnlyList<Sticker> Stickers
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public static MessageSnapshot Parse(JsonElement json)
|
||||||
|
{
|
||||||
|
var timestamp =
|
||||||
|
json.GetPropertyOrNull("timestamp")?.GetDateTimeOffsetOrNull()
|
||||||
|
?? DateTimeOffset.MinValue;
|
||||||
|
|
||||||
|
var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffsetOrNull();
|
||||||
|
|
||||||
|
var content = json.GetPropertyOrNull("content")?.GetStringOrNull() ?? "";
|
||||||
|
|
||||||
|
var attachments =
|
||||||
|
json.GetPropertyOrNull("attachments")
|
||||||
|
?.EnumerateArrayOrNull()
|
||||||
|
?.Select(Attachment.Parse)
|
||||||
|
.ToArray()
|
||||||
|
?? [];
|
||||||
|
|
||||||
|
var embeds =
|
||||||
|
json.GetPropertyOrNull("embeds")?.EnumerateArrayOrNull()?.Select(Embed.Parse).ToArray()
|
||||||
|
?? [];
|
||||||
|
|
||||||
|
var stickers =
|
||||||
|
json.GetPropertyOrNull("sticker_items")
|
||||||
|
?.EnumerateArrayOrNull()
|
||||||
|
?.Select(Sticker.Parse)
|
||||||
|
.ToArray()
|
||||||
|
?? [];
|
||||||
|
|
||||||
|
return new MessageSnapshot(
|
||||||
|
timestamp,
|
||||||
|
editedTimestamp,
|
||||||
|
content,
|
||||||
|
attachments,
|
||||||
|
embeds,
|
||||||
|
stickers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ public record Role(Snowflake Id, string Name, int Position, Color? Color) : IHas
|
|||||||
var color = json.GetPropertyOrNull("color")
|
var color = json.GetPropertyOrNull("color")
|
||||||
?.GetInt32OrNull()
|
?.GetInt32OrNull()
|
||||||
?.Pipe(System.Drawing.Color.FromArgb)
|
?.Pipe(System.Drawing.Color.FromArgb)
|
||||||
.ResetAlpha()
|
.WithFullAlpha()
|
||||||
.NullIf(c => c.ToRgb() <= 0);
|
.NullIf(c => c.ToRgb() <= 0);
|
||||||
|
|
||||||
return new Role(id, name, position, color);
|
return new Role(id, name, position, color);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
public enum StickerFormat
|
public enum StickerFormat
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -11,10 +11,10 @@ using System.Threading.Tasks;
|
|||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Exceptions;
|
using DiscordChatExporter.Core.Exceptions;
|
||||||
using DiscordChatExporter.Core.Utils;
|
using DiscordChatExporter.Core.Utils;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
using Gress;
|
using Gress;
|
||||||
using JsonExtensions.Http;
|
using JsonExtensions.Http;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
|
using PowerKit.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord;
|
namespace DiscordChatExporter.Core.Discord;
|
||||||
|
|
||||||
@@ -30,9 +30,8 @@ public class DiscordClient(
|
|||||||
string url,
|
string url,
|
||||||
TokenKind tokenKind,
|
TokenKind tokenKind,
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
)
|
) =>
|
||||||
{
|
await Http.ResponseResiliencePipeline.ExecuteAsync(
|
||||||
return await Http.ResponseResiliencePipeline.ExecuteAsync(
|
|
||||||
async innerCancellationToken =>
|
async innerCancellationToken =>
|
||||||
{
|
{
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
|
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
|
||||||
@@ -59,12 +58,12 @@ public class DiscordClient(
|
|||||||
{
|
{
|
||||||
var remainingRequestCount = response
|
var remainingRequestCount = response
|
||||||
.Headers.TryGetValue("X-RateLimit-Remaining")
|
.Headers.TryGetValue("X-RateLimit-Remaining")
|
||||||
?.Pipe(s => int.Parse(s, CultureInfo.InvariantCulture));
|
?.Pipe(s => int.ParseOrNull(s, CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
var resetAfterDelay = response
|
var resetAfterDelay = response
|
||||||
.Headers.TryGetValue("X-RateLimit-Reset-After")
|
.Headers.TryGetValue("X-RateLimit-Reset-After")
|
||||||
?.Pipe(s => double.Parse(s, CultureInfo.InvariantCulture))
|
?.Pipe(s => double.ParseOrNull(s, CultureInfo.InvariantCulture))
|
||||||
.Pipe(TimeSpan.FromSeconds);
|
?.Pipe(TimeSpan.FromSeconds);
|
||||||
|
|
||||||
// If this was the last request available before hitting the rate limit,
|
// If this was the last request available before hitting the rate limit,
|
||||||
// wait out the reset time so that future requests can succeed.
|
// wait out the reset time so that future requests can succeed.
|
||||||
@@ -91,7 +90,6 @@ public class DiscordClient(
|
|||||||
},
|
},
|
||||||
cancellationToken
|
cancellationToken
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask<TokenKind> ResolveTokenKindAsync(
|
private async ValueTask<TokenKind> ResolveTokenKindAsync(
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
@@ -161,7 +159,7 @@ public class DiscordClient(
|
|||||||
$"""
|
$"""
|
||||||
Request to '{url}' failed: {response
|
Request to '{url}' failed: {response
|
||||||
.StatusCode.ToString()
|
.StatusCode.ToString()
|
||||||
.ToSpaceSeparatedWords()
|
.SeparateWords(' ')
|
||||||
.ToLowerInvariant()}.
|
.ToLowerInvariant()}.
|
||||||
Response content: {await response.Content.ReadAsStringAsync(
|
Response content: {await response.Content.ReadAsStringAsync(
|
||||||
cancellationToken
|
cancellationToken
|
||||||
@@ -194,6 +192,23 @@ public class DiscordClient(
|
|||||||
return Application.Parse(response);
|
return Application.Parse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ValueTask EnsureMessageContentIntentAsync(
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (await ResolveTokenKindAsync(cancellationToken) != TokenKind.Bot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var application = await GetApplicationAsync(cancellationToken);
|
||||||
|
if (application.IsMessageContentIntentEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
throw new DiscordChatExporterException(
|
||||||
|
"Provided bot account is missing the MESSAGE_CONTENT privileged intent.",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask<User?> TryGetUserAsync(
|
public async ValueTask<User?> TryGetUserAsync(
|
||||||
Snowflake userId,
|
Snowflake userId,
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
@@ -347,6 +362,7 @@ public class DiscordClient(
|
|||||||
$"guilds/{guildId}/members/{memberId}",
|
$"guilds/{guildId}/members/{memberId}",
|
||||||
cancellationToken
|
cancellationToken
|
||||||
);
|
);
|
||||||
|
|
||||||
return response?.Pipe(j => Member.Parse(j, guildId));
|
return response?.Pipe(j => Member.Parse(j, guildId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,21 +387,38 @@ public class DiscordClient(
|
|||||||
?.GetNonWhiteSpaceStringOrNull()
|
?.GetNonWhiteSpaceStringOrNull()
|
||||||
?.Pipe(Snowflake.Parse);
|
?.Pipe(Snowflake.Parse);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var parent = parentId is not null
|
|
||||||
? await GetChannelAsync(parentId.Value, cancellationToken)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return Channel.Parse(response, parent);
|
|
||||||
}
|
|
||||||
// It's possible for the parent channel to be inaccessible, despite the
|
// It's possible for the parent channel to be inaccessible, despite the
|
||||||
// child channel being accessible.
|
// child channel being accessible.
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1108
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1108
|
||||||
catch (DiscordChatExporterException)
|
var parent = parentId is not null
|
||||||
{
|
? await TryGetChannelAsync(parentId.Value, cancellationToken)
|
||||||
return Channel.Parse(response);
|
: null;
|
||||||
}
|
|
||||||
|
return Channel.Parse(response, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Channel?> TryGetChannelAsync(
|
||||||
|
Snowflake channelId,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var response = await TryGetJsonResponseAsync($"channels/{channelId}", cancellationToken);
|
||||||
|
if (response is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var parentId = response
|
||||||
|
.Value.GetPropertyOrNull("parent_id")
|
||||||
|
?.GetNonWhiteSpaceStringOrNull()
|
||||||
|
?.Pipe(Snowflake.Parse);
|
||||||
|
|
||||||
|
// It's possible for the parent channel to be inaccessible, despite the
|
||||||
|
// child channel being accessible.
|
||||||
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1108
|
||||||
|
var parent = parentId is not null
|
||||||
|
? await TryGetChannelAsync(parentId.Value, cancellationToken)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return Channel.Parse(response.Value, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<Channel> GetChannelThreadsAsync(
|
public async IAsyncEnumerable<Channel> GetChannelThreadsAsync(
|
||||||
@@ -410,6 +443,11 @@ public class DiscordClient(
|
|||||||
.Where(c => before is null || c.MayHaveMessagesBefore(before.Value))
|
.Where(c => before is null || c.MayHaveMessagesBefore(before.Value))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
|
// Track yielded thread IDs to avoid duplicates that can occur when a thread transitions
|
||||||
|
// from active to archived between the two separate API calls used to fetch threads.
|
||||||
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1433
|
||||||
|
var seenThreadIds = new HashSet<Snowflake>();
|
||||||
|
|
||||||
// User accounts can only fetch threads using the search endpoint
|
// User accounts can only fetch threads using the search endpoint
|
||||||
if (await ResolveTokenKindAsync(cancellationToken) == TokenKind.User)
|
if (await ResolveTokenKindAsync(cancellationToken) == TokenKind.User)
|
||||||
{
|
{
|
||||||
@@ -453,7 +491,9 @@ public class DiscordClient(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield return thread;
|
if (seenThreadIds.Add(thread.Id))
|
||||||
|
yield return thread;
|
||||||
|
|
||||||
currentOffset++;
|
currentOffset++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +532,12 @@ public class DiscordClient(
|
|||||||
.Pipe(parentsById.GetValueOrDefault);
|
.Pipe(parentsById.GetValueOrDefault);
|
||||||
|
|
||||||
if (filteredChannels.Contains(parent))
|
if (filteredChannels.Contains(parent))
|
||||||
yield return Channel.Parse(threadJson, parent);
|
{
|
||||||
|
var thread = Channel.Parse(threadJson, parent);
|
||||||
|
|
||||||
|
if (seenThreadIds.Add(thread.Id))
|
||||||
|
yield return thread;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,12 +573,14 @@ public class DiscordClient(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
var thread = Channel.Parse(threadJson, channel);
|
var thread = Channel.Parse(threadJson, channel);
|
||||||
yield return thread;
|
|
||||||
|
|
||||||
currentBefore = threadJson
|
currentBefore = threadJson
|
||||||
.GetProperty("thread_metadata")
|
.GetProperty("thread_metadata")
|
||||||
.GetProperty("archive_timestamp")
|
.GetProperty("archive_timestamp")
|
||||||
.GetString();
|
.GetString();
|
||||||
|
|
||||||
|
if (seenThreadIds.Add(thread.Id))
|
||||||
|
yield return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.Value.GetProperty("has_more").GetBoolean())
|
if (!response.Value.GetProperty("has_more").GetBoolean())
|
||||||
@@ -545,6 +592,28 @@ public class DiscordClient(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ValueTask<Message?> TryGetFirstMessageAsync(
|
||||||
|
Snowflake channelId,
|
||||||
|
Snowflake? after = null,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var url = new UrlBuilder()
|
||||||
|
.SetPath($"channels/{channelId}/messages")
|
||||||
|
.SetQueryParameter("limit", "1")
|
||||||
|
.SetQueryParameter("after", (after ?? Snowflake.Zero).ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Can be null on channels that the user cannot access
|
||||||
|
var response = await TryGetJsonResponseAsync(url, cancellationToken);
|
||||||
|
if (response is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var message = response.Value.EnumerateArray().Select(Message.Parse).FirstOrDefault();
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
private async ValueTask<Message?> TryGetLastMessageAsync(
|
private async ValueTask<Message?> TryGetLastMessageAsync(
|
||||||
Snowflake channelId,
|
Snowflake channelId,
|
||||||
Snowflake? before = null,
|
Snowflake? before = null,
|
||||||
@@ -557,8 +626,39 @@ public class DiscordClient(
|
|||||||
.SetQueryParameter("before", before?.ToString())
|
.SetQueryParameter("before", before?.ToString())
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var response = await GetJsonResponseAsync(url, cancellationToken);
|
// Can be null on channels that the user cannot access
|
||||||
return response.EnumerateArray().Select(Message.Parse).LastOrDefault();
|
var response = await TryGetJsonResponseAsync(url, cancellationToken);
|
||||||
|
if (response is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return response.Value.EnumerateArray().Select(Message.Parse).LastOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Message?> TryGetMessageAsync(
|
||||||
|
Snowflake channelId,
|
||||||
|
Snowflake messageId,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Use the regular message listing endpoint with the 'around' parameter instead of the
|
||||||
|
// dedicated single-message endpoint, because the latter is not accessible to user tokens.
|
||||||
|
var url = new UrlBuilder()
|
||||||
|
.SetPath($"channels/{channelId}/messages")
|
||||||
|
.SetQueryParameter("around", messageId.ToString())
|
||||||
|
.SetQueryParameter("limit", "1")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Can be null on channels that the user cannot access
|
||||||
|
var response = await TryGetJsonResponseAsync(url, cancellationToken);
|
||||||
|
if (response is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// The endpoint returns messages around the requested ID, so make sure to only return
|
||||||
|
// the message that exactly matches it (it may be absent if it has been deleted).
|
||||||
|
return response
|
||||||
|
.Value.EnumerateArray()
|
||||||
|
.Select(Message.Parse)
|
||||||
|
.FirstOrDefault(m => m.Id == messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<Message> GetMessagesAsync(
|
public async IAsyncEnumerable<Message> GetMessagesAsync(
|
||||||
@@ -603,22 +703,10 @@ public class DiscordClient(
|
|||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
// If all messages are empty, make sure that it's not because the bot account doesn't
|
// If all messages are empty, make sure that it's not because the bot account doesn't
|
||||||
// have the Message Content Intent enabled.
|
// have the MESSAGE_CONTENT intent enabled.
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1106#issuecomment-1741548959
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1106#issuecomment-1741548959
|
||||||
if (
|
if (messages.All(m => m.IsEmpty))
|
||||||
messages.All(m => m.IsEmpty)
|
await EnsureMessageContentIntentAsync(cancellationToken);
|
||||||
&& await ResolveTokenKindAsync(cancellationToken) == TokenKind.Bot
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var application = await GetApplicationAsync(cancellationToken);
|
|
||||||
if (!application.IsMessageContentIntentEnabled)
|
|
||||||
{
|
|
||||||
throw new DiscordChatExporterException(
|
|
||||||
"Provided bot account does not have the Message Content Intent enabled.",
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var message in messages)
|
foreach (var message in messages)
|
||||||
{
|
{
|
||||||
@@ -645,12 +733,108 @@ public class DiscordClient(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield return message;
|
// Some messages, for example thread starter messages, are returned by the API as content-less references.
|
||||||
|
// Try to resolve them to the actual message so that they appear as they do in the Discord client.
|
||||||
|
var actualMessage =
|
||||||
|
message.Kind == MessageKind.ThreadStarterMessage
|
||||||
|
&& message.Reference?.ChannelId is { } referencedChannelId
|
||||||
|
&& message.Reference?.MessageId is { } referencedMessageId
|
||||||
|
? await TryGetMessageAsync(
|
||||||
|
referencedChannelId,
|
||||||
|
referencedMessageId,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
yield return actualMessage ?? message;
|
||||||
|
|
||||||
currentAfter = message.Id;
|
currentAfter = message.Id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<Message> GetMessagesInReverseAsync(
|
||||||
|
Snowflake channelId,
|
||||||
|
Snowflake? after = null,
|
||||||
|
Snowflake? before = null,
|
||||||
|
IProgress<Percentage>? progress = null,
|
||||||
|
[EnumeratorCancellation] CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Get the first message in the specified range, so we can later calculate the
|
||||||
|
// progress based on the difference between message timestamps.
|
||||||
|
// Snapshotting is not necessary here because new messages can't appear in the past.
|
||||||
|
var firstMessage = await TryGetFirstMessageAsync(channelId, after, cancellationToken);
|
||||||
|
if (firstMessage is null || firstMessage.Timestamp > before?.ToDate())
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
// Keep track of the last message in range in order to calculate the progress
|
||||||
|
var lastMessage = default(Message);
|
||||||
|
|
||||||
|
var currentBefore = before;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var url = new UrlBuilder()
|
||||||
|
.SetPath($"channels/{channelId}/messages")
|
||||||
|
.SetQueryParameter("limit", "100")
|
||||||
|
.SetQueryParameter("before", currentBefore?.ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var response = await GetJsonResponseAsync(url, cancellationToken);
|
||||||
|
|
||||||
|
var messages = response.EnumerateArray().Select(Message.Parse).ToArray();
|
||||||
|
|
||||||
|
// Break if there are no messages (can happen if messages are deleted during execution)
|
||||||
|
if (!messages.Any())
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
// If all messages are empty, make sure that it's not because the bot account doesn't
|
||||||
|
// have the MESSAGE_CONTENT intent enabled.
|
||||||
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1106#issuecomment-1741548959
|
||||||
|
if (messages.All(m => m.IsEmpty))
|
||||||
|
await EnsureMessageContentIntentAsync(cancellationToken);
|
||||||
|
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
lastMessage ??= message;
|
||||||
|
|
||||||
|
// Report progress based on timestamps
|
||||||
|
if (progress is not null)
|
||||||
|
{
|
||||||
|
var exportedDuration = (lastMessage.Timestamp - message.Timestamp).Duration();
|
||||||
|
var totalDuration = (lastMessage.Timestamp - firstMessage.Timestamp).Duration();
|
||||||
|
|
||||||
|
progress.Report(
|
||||||
|
Percentage.FromFraction(
|
||||||
|
// Avoid division by zero if all messages have the exact same timestamp
|
||||||
|
// (which happens when there's only one message in the channel)
|
||||||
|
totalDuration > TimeSpan.Zero
|
||||||
|
? exportedDuration / totalDuration
|
||||||
|
: 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some messages, for example thread starter messages, are returned by the API as content-less references.
|
||||||
|
// Try to resolve them to the actual message so that they appear as they do in the Discord client.
|
||||||
|
var actualMessage =
|
||||||
|
message.Kind == MessageKind.ThreadStarterMessage
|
||||||
|
&& message.Reference?.ChannelId is { } referencedChannelId
|
||||||
|
&& message.Reference?.MessageId is { } referencedMessageId
|
||||||
|
? await TryGetMessageAsync(
|
||||||
|
referencedChannelId,
|
||||||
|
referencedMessageId,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
yield return actualMessage ?? message;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBefore = messages.Last().Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<User> GetMessageReactionsAsync(
|
public async IAsyncEnumerable<User> GetMessageReactionsAsync(
|
||||||
Snowflake channelId,
|
Snowflake channelId,
|
||||||
Snowflake messageId,
|
Snowflake messageId,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
namespace DiscordChatExporter.Core.Discord.Dump;
|
namespace DiscordChatExporter.Core.Discord.Dump;
|
||||||
|
|
||||||
public record DataDumpChannel(Snowflake Id, string Name);
|
public record DataDumpChannel(Snowflake Id, string Name);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord;
|
namespace DiscordChatExporter.Core.Discord;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace DiscordChatExporter.Core.Discord;
|
namespace DiscordChatExporter.Core.Discord;
|
||||||
|
|
||||||
public enum TokenKind
|
public enum TokenKind
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="1.4.0" />
|
<PackageReference Include="AngleSharp" />
|
||||||
<PackageReference Include="AsyncKeyedLock" Version="8.0.1" />
|
<PackageReference Include="AsyncKeyedLock" />
|
||||||
<PackageReference Include="CSharpier.MsBuild" Version="1.2.5" PrivateAssets="all" />
|
<PackageReference Include="CSharpier.MsBuild" PrivateAssets="all" />
|
||||||
<PackageReference Include="Gress" Version="2.1.1" />
|
<PackageReference Include="Gress" />
|
||||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
<PackageReference Include="JsonExtensions" />
|
||||||
<PackageReference Include="Polly" Version="8.6.5" />
|
<PackageReference Include="Polly" />
|
||||||
<PackageReference Include="RazorBlade" Version="0.11.0" />
|
<PackageReference Include="PowerKit" />
|
||||||
<PackageReference Include="Superpower" Version="3.1.0" />
|
<PackageReference Include="RazorBlade" />
|
||||||
<PackageReference Include="WebMarkupMin.Core" Version="2.20.1" />
|
<PackageReference Include="Superpower" />
|
||||||
<PackageReference Include="YoutubeExplode" Version="6.5.6" />
|
<PackageReference Include="WebMarkupMin.Core" />
|
||||||
|
<PackageReference Include="YoutubeExplode" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Exceptions;
|
namespace DiscordChatExporter.Core.Exceptions;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user