mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-09 23:49:34 +02:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f47fb6a1c | |||
| 6150720b1e | |||
| dcdeea19ab | |||
| ca56d47110 | |||
| 17b548d679 | |||
| 20eeaa58e9 | |||
| 6621c10ae0 | |||
| 75214354ca | |||
| 84d99e8520 | |||
| 4b80eabb73 | |||
| e6830bbd0e | |||
| 97c751f171 | |||
| dc814e08e7 | |||
| 54bf7c204a | |||
| 36c9983c9e | |||
| 361e0489ed | |||
| 77436120a8 | |||
| 2d7ab5411f | |||
| 1ce523b252 | |||
| bb81cf06ae |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
@@ -1,30 +0,0 @@
|
|||||||
name: Docker CD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2.3.3
|
|
||||||
|
|
||||||
- name: Get release version
|
|
||||||
id: get_version
|
|
||||||
uses: dawidd6/action-get-tag@v1
|
|
||||||
|
|
||||||
- name: Login
|
|
||||||
run: docker login -u tyrrrz -p ${{ secrets.DOCKER_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: docker build -t tyrrrz/discordchatexporter:${{ steps.get_version.outputs.tag }} -t tyrrrz/discordchatexporter:stable .
|
|
||||||
|
|
||||||
- name: Deploy (version)
|
|
||||||
run: docker push tyrrrz/discordchatexporter:${{ steps.get_version.outputs.tag }}
|
|
||||||
|
|
||||||
- name: Deploy (stable)
|
|
||||||
run: docker push tyrrrz/discordchatexporter:stable
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
name: CD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: windows-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2.3.3
|
|
||||||
|
|
||||||
- name: Get release version
|
|
||||||
id: get_version
|
|
||||||
uses: dawidd6/action-get-tag@v1
|
|
||||||
|
|
||||||
- name: Install .NET
|
|
||||||
uses: actions/setup-dotnet@v1.7.2
|
|
||||||
with:
|
|
||||||
# Fixed version, workaround for https://github.com/dotnet/core/issues/7176
|
|
||||||
dotnet-version: 6.0.100
|
|
||||||
|
|
||||||
- name: Build & publish (CLI)
|
|
||||||
run: dotnet publish DiscordChatExporter.Cli/ -o DiscordChatExporter.Cli/bin/Publish/ --configuration Release
|
|
||||||
|
|
||||||
- name: Build & publish (GUI)
|
|
||||||
run: dotnet publish DiscordChatExporter.Gui/ -o DiscordChatExporter.Gui/bin/Publish/ --configuration Release
|
|
||||||
|
|
||||||
- name: Pack (CLI)
|
|
||||||
run: Compress-Archive -Path DiscordChatExporter.Cli/bin/Publish/* -DestinationPath DiscordChatExporter.Cli/bin/Publish/Archive.zip -Force
|
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Pack (GUI)
|
|
||||||
run: Compress-Archive -Path DiscordChatExporter.Gui/bin/Publish/* -DestinationPath DiscordChatExporter.Gui/bin/Publish/Archive.zip -Force
|
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Create release
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1.0.1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
release_name: ${{ github.ref }}
|
|
||||||
body: |
|
|
||||||
[Changelog](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/Changelog.md)
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
|
|
||||||
- name: Upload release asset (CLI)
|
|
||||||
uses: actions/upload-release-asset@v1.0.2
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: DiscordChatExporter.Cli/bin/Publish/Archive.zip
|
|
||||||
asset_name: DiscordChatExporter.CLI.zip
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload release asset (GUI)
|
|
||||||
uses: actions/upload-release-asset@v1.0.2
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: DiscordChatExporter.Gui/bin/Publish/Archive.zip
|
|
||||||
asset_name: DiscordChatExporter.zip
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Notify Discord
|
|
||||||
uses: satak/webrequest-action@v1.2.4
|
|
||||||
with:
|
|
||||||
url: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
method: POST
|
|
||||||
headers: |
|
|
||||||
{
|
|
||||||
"ContentType": "application/json; charset=UTF-8"
|
|
||||||
}
|
|
||||||
payload: |
|
|
||||||
{
|
|
||||||
"content": "**DiscordChatExporter** new version released!\nVersion: `${{ steps.get_version.outputs.tag }}`\nChangelog: <https://github.com/Tyrrrz/DiscordChatExporter/blob/${{ steps.get_version.outputs.tag }}/Changelog.md>"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
name: Docker CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2.3.3
|
|
||||||
|
|
||||||
- name: Login
|
|
||||||
run: docker login -u tyrrrz -p ${{ secrets.DOCKER_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: docker build -t tyrrrz/discordchatexporter:latest .
|
|
||||||
|
|
||||||
- name: Deploy (latest)
|
|
||||||
run: docker push tyrrrz/discordchatexporter:latest
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: windows-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2.3.3
|
|
||||||
|
|
||||||
- name: Install .NET
|
|
||||||
uses: actions/setup-dotnet@v1.7.2
|
|
||||||
with:
|
|
||||||
# Fixed version, workaround for https://github.com/dotnet/core/issues/7176
|
|
||||||
dotnet-version: 6.0.100
|
|
||||||
|
|
||||||
- name: Build & test
|
|
||||||
# Tests need access to secrets, so we can't run them against PRs because of limited trust
|
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
|
||||||
run: dotnet test --configuration Release --logger GitHubActions --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
|
|
||||||
env:
|
|
||||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
|
||||||
DISCORD_TOKEN_BOT: true
|
|
||||||
|
|
||||||
- name: Upload coverage
|
|
||||||
uses: codecov/codecov-action@v1.0.5
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build & publish (CLI)
|
|
||||||
run: dotnet publish DiscordChatExporter.Cli/ -o DiscordChatExporter.Cli/bin/Publish/ --configuration Release
|
|
||||||
|
|
||||||
- name: Build & publish (GUI)
|
|
||||||
run: dotnet publish DiscordChatExporter.Gui/ -o DiscordChatExporter.Gui/bin/Publish/ --configuration Release
|
|
||||||
|
|
||||||
- name: Upload build artifacts (CLI)
|
|
||||||
uses: actions/upload-artifact@v1
|
|
||||||
with:
|
|
||||||
name: DiscordChatExporter.CLI
|
|
||||||
path: DiscordChatExporter.Cli/bin/Publish/
|
|
||||||
|
|
||||||
- name: Upload build artifact (GUI)
|
|
||||||
uses: actions/upload-artifact@v1
|
|
||||||
with:
|
|
||||||
name: DiscordChatExporter
|
|
||||||
path: DiscordChatExporter.Gui/bin/Publish/
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: docker
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build container
|
||||||
|
run: docker build .
|
||||||
|
|
||||||
|
deploy-latest:
|
||||||
|
if: ${{ github.event_name == 'push' && github.ref_type == 'branch' && github.ref_name == 'master' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
run: docker login -u tyrrrz -p ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build container
|
||||||
|
run: docker build -t tyrrrz/discordchatexporter:latest .
|
||||||
|
|
||||||
|
- name: Push container
|
||||||
|
run: docker push tyrrrz/discordchatexporter:latest
|
||||||
|
|
||||||
|
deploy-stable:
|
||||||
|
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Get release version
|
||||||
|
id: get_version
|
||||||
|
uses: dawidd6/action-get-tag@v1
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
run: docker login -u tyrrrz -p ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build container
|
||||||
|
run: docker build -t tyrrrz/discordchatexporter:stable .
|
||||||
|
|
||||||
|
- name: Push container (stable)
|
||||||
|
run: docker push tyrrrz/discordchatexporter:stable
|
||||||
|
|
||||||
|
- name: Push container (${{ steps.get_version.outputs.tag }})
|
||||||
|
run: docker push tyrrrz/discordchatexporter:${{ steps.get_version.outputs.tag }}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
name: main
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||||
|
DOTNET_NOLOGO: true
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install .NET
|
||||||
|
uses: actions/setup-dotnet@v2
|
||||||
|
with:
|
||||||
|
# Fixed version, workaround for https://github.com/dotnet/core/issues/7176
|
||||||
|
dotnet-version: 6.0.100
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
# Tests need access to secrets, so we can't run them against PRs because of limited trust
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
run: dotnet test --configuration Release --logger GitHubActions --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
|
||||||
|
env:
|
||||||
|
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||||
|
DISCORD_TOKEN_BOT: true
|
||||||
|
|
||||||
|
- name: Upload coverage
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: test
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||||
|
DOTNET_NOLOGO: true
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install .NET
|
||||||
|
uses: actions/setup-dotnet@v2
|
||||||
|
with:
|
||||||
|
# Fixed version, workaround for https://github.com/dotnet/core/issues/7176
|
||||||
|
dotnet-version: 6.0.100
|
||||||
|
|
||||||
|
- name: Publish (CLI)
|
||||||
|
run: dotnet publish DiscordChatExporter.Cli/ -o DiscordChatExporter.Cli/bin/Publish/ --configuration Release
|
||||||
|
|
||||||
|
- name: Publish (GUI)
|
||||||
|
run: dotnet publish DiscordChatExporter.Gui/ -o DiscordChatExporter.Gui/bin/Publish/ --configuration Release
|
||||||
|
|
||||||
|
- name: Upload artifacts (CLI)
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: DiscordChatExporter.Cli
|
||||||
|
path: DiscordChatExporter.Cli/bin/Publish/
|
||||||
|
|
||||||
|
- name: Upload artifacts (GUI)
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: DiscordChatExporter
|
||||||
|
path: DiscordChatExporter.Gui/bin/Publish/
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
|
||||||
|
needs: publish
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download artifacts (CLI)
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: DiscordChatExporter.Cli
|
||||||
|
path: DiscordChatExporter.Cli
|
||||||
|
|
||||||
|
- name: Download artifacts (GUI)
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: DiscordChatExporter
|
||||||
|
path: DiscordChatExporter.Gui
|
||||||
|
|
||||||
|
- name: Create package (CLI)
|
||||||
|
run: Compress-Archive -Path DiscordChatExporter.Cli/* -DestinationPath DiscordChatExporter.Cli.zip -Force
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Create package (GUI)
|
||||||
|
run: Compress-Archive -Path DiscordChatExporter.Gui/* -DestinationPath DiscordChatExporter.Gui.zip -Force
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: ${{ github.ref }}
|
||||||
|
body: |
|
||||||
|
[Changelog](https://github.com/Tyrrrz/DiscordChatExporter/blob/master/Changelog.md)
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload release asset (CLI)
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_name: DiscordChatExporter.Cli.zip
|
||||||
|
asset_path: DiscordChatExporter.Cli.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Upload release asset (GUI)
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_name: DiscordChatExporter.zip
|
||||||
|
asset_path: DiscordChatExporter.Gui.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
notify:
|
||||||
|
needs: deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Get release version
|
||||||
|
id: get_version
|
||||||
|
uses: dawidd6/action-get-tag@v1
|
||||||
|
|
||||||
|
- name: Notify Discord
|
||||||
|
uses: satak/webrequest-action@v1.2.4
|
||||||
|
with:
|
||||||
|
url: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
method: POST
|
||||||
|
headers: |
|
||||||
|
{
|
||||||
|
"ContentType": "application/json; charset=UTF-8"
|
||||||
|
}
|
||||||
|
payload: |
|
||||||
|
{
|
||||||
|
"content": "**DiscordChatExporter** new version released!\nVersion: `${{ steps.get_version.outputs.tag }}`\nChangelog: <https://github.com/Tyrrrz/DiscordChatExporter/blob/${{ steps.get_version.outputs.tag }}/Changelog.md>"
|
||||||
|
}
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
### v2.34.1 (01-Jun-2022)
|
||||||
|
|
||||||
|
- Improved retry policy to handle more transient errors. This should result in fewer errors exporting a channel due to timeout.
|
||||||
|
- Added `has:pin` message filter. You can use it to limit exported messages to those that have been pinned. (Thanks [@Andrew Kolos](https://github.com/andrewkolos))
|
||||||
|
- Fixed an issue which caused the export to fail when processing stickers that have no name.
|
||||||
|
|
||||||
### v2.34 (10-Apr-2022)
|
### v2.34 (10-Apr-2022)
|
||||||
|
|
||||||
- [HTML] Reworked layout to be more consistent with Discord.
|
- [HTML] Reworked layout to be more consistent with Discord.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Version>2.34</Version>
|
<Version>2.34.1</Version>
|
||||||
<Company>Tyrrrz</Company>
|
<Company>Tyrrrz</Company>
|
||||||
<Copyright>Copyright (c) Oleksii Holub</Copyright>
|
<Copyright>Copyright (c) Oleksii Holub</Copyright>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="0.16.1" />
|
<PackageReference Include="AngleSharp" Version="0.16.1" />
|
||||||
<PackageReference Include="FluentAssertions" Version="6.6.0" />
|
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.3.0" PrivateAssets="all" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
|
||||||
<PackageReference Include="coverlet.collector" Version="3.1.2" PrivateAssets="all" />
|
<PackageReference Include="coverlet.collector" Version="3.1.2" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,11 @@
|
|||||||
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. Join the server: https://discord.gg/eRV8Vap5bm
|
1. [Join the test server](https://discord.gg/eRV8Vap5bm)
|
||||||
2. Locate your Discord authentication token
|
2. Locate your Discord authentication token
|
||||||
3. Specify your token using a file or an environment variable:
|
3. Specify your token using a file or an environment variable:
|
||||||
- **Using a file**: put your token in a new `DiscordToken.secret` file created in this directory
|
- **Using a file**: put your token in a new `DiscordToken.secret` file created in this directory
|
||||||
- **Using an environment variable**: set `DISCORD_TOKEN` variable to your token
|
- **Using an environment variable**: set `DISCORD_TOKEN` variable to your token
|
||||||
4. Run the tests: `dotnet test`
|
4. Run the tests: `dotnet test`
|
||||||
|
|
||||||
> If you want to have a new test case or a scenario added, please let me know in your pull request.
|
> If you want to add a new test case, please let me know and I will give you the required permissions
|
||||||
Currently, it's not possible to add them by yourself.
|
|
||||||
@@ -100,6 +100,34 @@ public record FilterSpecs(TempOutputFixture TempOutput) : IClassFixture<TempOutp
|
|||||||
.ContainSingle("This has image");
|
.ContainSingle("This has image");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Messages_filtered_by_pin_only_include_messages_that_have_been_pinned()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filePath = TempOutput.GetTempFilePath();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await new ExportChannelsCommand
|
||||||
|
{
|
||||||
|
Token = Secrets.DiscordToken,
|
||||||
|
ChannelIds = new[] { ChannelIds.FilterTestCases },
|
||||||
|
ExportFormat = ExportFormat.Json,
|
||||||
|
OutputPath = filePath,
|
||||||
|
MessageFilter = MessageFilter.Parse("has:pin")
|
||||||
|
}.ExecuteAsync(new FakeConsole());
|
||||||
|
|
||||||
|
var data = await File.ReadAllTextAsync(filePath);
|
||||||
|
var document = Json.Parse(data);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
document
|
||||||
|
.GetProperty("messages")
|
||||||
|
.EnumerateArray()
|
||||||
|
.Select(j => j.GetProperty("content").GetString())
|
||||||
|
.Should()
|
||||||
|
.ContainSingle("This is pinned");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Messages_filtered_by_mention_only_include_messages_that_have_that_mention()
|
public async Task Messages_filtered_by_mention_only_include_messages_that_have_that_mention()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CliFx" Version="2.2.2" />
|
<PackageReference Include="CliFx" Version="2.2.5" />
|
||||||
<PackageReference Include="Spectre.Console" Version="0.44.0" />
|
<PackageReference Include="Spectre.Console" Version="0.44.0" />
|
||||||
<PackageReference Include="Gress" Version="2.0.1" />
|
<PackageReference Include="Gress" Version="2.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using JsonExtensions.Reading;
|
|||||||
|
|
||||||
namespace DiscordChatExporter.Core.Discord.Data;
|
namespace DiscordChatExporter.Core.Discord.Data;
|
||||||
|
|
||||||
|
// https://discord.com/developers/docs/resources/sticker#sticker-resource
|
||||||
public record Sticker(Snowflake Id, string Name, StickerFormat Format, string SourceUrl)
|
public record Sticker(Snowflake Id, string Name, StickerFormat Format, string SourceUrl)
|
||||||
{
|
{
|
||||||
private static string GetSourceUrl(Snowflake id, StickerFormat format)
|
private static string GetSourceUrl(Snowflake id, StickerFormat format)
|
||||||
@@ -15,7 +16,7 @@ public record Sticker(Snowflake Id, string Name, StickerFormat Format, string So
|
|||||||
public static Sticker Parse(JsonElement json)
|
public static Sticker Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||||
var name = json.GetProperty("name").GetNonWhiteSpaceString();
|
var name = json.GetProperty("name").GetNonNullString();
|
||||||
var format = (StickerFormat)json.GetProperty("format_type").GetInt32();
|
var format = (StickerFormat)json.GetProperty("format_type").GetInt32();
|
||||||
|
|
||||||
var sourceUrl = GetSourceUrl(id, format);
|
var sourceUrl = GetSourceUrl(id, format);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -52,7 +51,7 @@ public class DiscordClient
|
|||||||
string url,
|
string url,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await Http.ResponsePolicy.ExecuteAsync(async innerCancellationToken =>
|
return await Http.ResponseResiliencePolicy.ExecuteAsync(async innerCancellationToken =>
|
||||||
{
|
{
|
||||||
if (_tokenKind == TokenKind.User)
|
if (_tokenKind == TokenKind.User)
|
||||||
return await GetResponseAsync(url, false, innerCancellationToken);
|
return await GetResponseAsync(url, false, innerCancellationToken);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ internal class HasMessageFilter : MessageFilter
|
|||||||
MessageContentMatchKind.Video => message.Attachments.Any(file => file.IsVideo),
|
MessageContentMatchKind.Video => message.Attachments.Any(file => file.IsVideo),
|
||||||
MessageContentMatchKind.Image => message.Attachments.Any(file => file.IsImage),
|
MessageContentMatchKind.Image => message.Attachments.Any(file => file.IsImage),
|
||||||
MessageContentMatchKind.Sound => message.Attachments.Any(file => file.IsAudio),
|
MessageContentMatchKind.Sound => message.Attachments.Any(file => file.IsAudio),
|
||||||
|
MessageContentMatchKind.Pin => message.IsPinned,
|
||||||
_ => throw new InvalidOperationException($"Unknown message content match kind '{_kind}'.")
|
_ => throw new InvalidOperationException($"Unknown message content match kind '{_kind}'.")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -7,5 +7,6 @@ internal enum MessageContentMatchKind
|
|||||||
File,
|
File,
|
||||||
Video,
|
Video,
|
||||||
Image,
|
Image,
|
||||||
Sound
|
Sound,
|
||||||
|
Pin
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,8 @@ internal static class FilterGrammar
|
|||||||
Span.EqualToIgnoreCase("file").IgnoreThen(Parse.Return(MessageContentMatchKind.File)),
|
Span.EqualToIgnoreCase("file").IgnoreThen(Parse.Return(MessageContentMatchKind.File)),
|
||||||
Span.EqualToIgnoreCase("video").IgnoreThen(Parse.Return(MessageContentMatchKind.Video)),
|
Span.EqualToIgnoreCase("video").IgnoreThen(Parse.Return(MessageContentMatchKind.Video)),
|
||||||
Span.EqualToIgnoreCase("image").IgnoreThen(Parse.Return(MessageContentMatchKind.Image)),
|
Span.EqualToIgnoreCase("image").IgnoreThen(Parse.Return(MessageContentMatchKind.Image)),
|
||||||
Span.EqualToIgnoreCase("sound").IgnoreThen(Parse.Return(MessageContentMatchKind.Sound))
|
Span.EqualToIgnoreCase("sound").IgnoreThen(Parse.Return(MessageContentMatchKind.Sound)),
|
||||||
|
Span.EqualToIgnoreCase("pin").IgnoreThen(Parse.Return(MessageContentMatchKind.Pin))
|
||||||
))
|
))
|
||||||
.Select(k => (MessageFilter) new HasMessageFilter(k))
|
.Select(k => (MessageFilter) new HasMessageFilter(k))
|
||||||
.Named("has:<value>");
|
.Named("has:<value>");
|
||||||
|
|||||||
@@ -35,45 +35,42 @@ internal partial class MediaDownloader
|
|||||||
var filePath = Path.Combine(_workingDirPath, fileName);
|
var filePath = Path.Combine(_workingDirPath, fileName);
|
||||||
|
|
||||||
// Reuse existing files if we're allowed to
|
// Reuse existing files if we're allowed to
|
||||||
if (_reuseMedia && File.Exists(filePath))
|
if (!_reuseMedia || !File.Exists(filePath))
|
||||||
return _pathCache[url] = filePath;
|
|
||||||
|
|
||||||
Directory.CreateDirectory(_workingDirPath);
|
|
||||||
|
|
||||||
// This retries on IOExceptions which is dangerous as we're also working with files
|
|
||||||
await Http.ExceptionPolicy.ExecuteAsync(async () =>
|
|
||||||
{
|
{
|
||||||
// Download the file
|
Directory.CreateDirectory(_workingDirPath);
|
||||||
using var response = await Http.Client.GetAsync(url, cancellationToken);
|
|
||||||
await using (var output = File.Create(filePath))
|
|
||||||
{
|
|
||||||
await response.Content.CopyToAsync(output, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to set the file date according to the last-modified header
|
await Http.ResiliencePolicy.ExecuteAsync(async () =>
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var lastModified = response.Content.Headers.TryGetValue("Last-Modified")?.Pipe(s =>
|
// Download the file
|
||||||
DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)
|
using var response = await Http.Client.GetAsync(url, cancellationToken);
|
||||||
? date
|
await using (var output = File.Create(filePath))
|
||||||
: (DateTimeOffset?) null
|
await response.Content.CopyToAsync(output, cancellationToken);
|
||||||
);
|
|
||||||
|
|
||||||
if (lastModified is not null)
|
// Try to set the file date according to the last-modified header
|
||||||
|
try
|
||||||
{
|
{
|
||||||
File.SetCreationTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
var lastModified = response.Content.Headers.TryGetValue("Last-Modified")?.Pipe(s =>
|
||||||
File.SetLastWriteTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)
|
||||||
File.SetLastAccessTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
? date
|
||||||
|
: (DateTimeOffset?) null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastModified is not null)
|
||||||
|
{
|
||||||
|
File.SetCreationTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
||||||
|
File.SetLastWriteTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
||||||
|
File.SetLastAccessTimeUtc(filePath, lastModified.Value.UtcDateTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
catch
|
||||||
catch
|
{
|
||||||
{
|
// This can apparently fail for some reason.
|
||||||
// This can apparently fail for some reason.
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/585
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/585
|
// Updating file dates is not a critical task, so we'll just
|
||||||
// Updating file dates is not a critical task, so we'll just
|
// ignore exceptions thrown here.
|
||||||
// ignore exceptions thrown here.
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return _pathCache[url] = filePath;
|
return _pathCache[url] = filePath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
|
public static class ExceptionExtensions
|
||||||
|
{
|
||||||
|
private static void PopulateChildren(this Exception exception, ICollection<Exception> children)
|
||||||
|
{
|
||||||
|
if (exception is AggregateException aggregateException)
|
||||||
|
{
|
||||||
|
foreach (var innerException in aggregateException.InnerExceptions)
|
||||||
|
{
|
||||||
|
children.Add(innerException);
|
||||||
|
PopulateChildren(innerException, children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (exception.InnerException is not null)
|
||||||
|
{
|
||||||
|
children.Add(exception.InnerException);
|
||||||
|
PopulateChildren(exception.InnerException, children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<Exception> GetSelfAndChildren(this Exception exception)
|
||||||
|
{
|
||||||
|
var children = new List<Exception> {exception};
|
||||||
|
PopulateChildren(exception, children);
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpStatusCode? TryGetStatusCode(this HttpRequestException ex) =>
|
||||||
|
// This is extremely frail, but there's no other way
|
||||||
|
Regex
|
||||||
|
.Match(ex.Message, @": (\d+) \(")
|
||||||
|
.Groups[1]
|
||||||
|
.Value
|
||||||
|
.NullIfWhiteSpace()?
|
||||||
|
.Pipe(s => (HttpStatusCode) int.Parse(s, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Linq;
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.RegularExpressions;
|
using System.Net.Sockets;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using Polly;
|
using Polly;
|
||||||
@@ -14,13 +13,26 @@ public static class Http
|
|||||||
{
|
{
|
||||||
public static HttpClient Client { get; } = new();
|
public static HttpClient Client { get; } = new();
|
||||||
|
|
||||||
public static IAsyncPolicy<HttpResponseMessage> ResponsePolicy { get; } =
|
private static bool IsRetryableStatusCode(HttpStatusCode statusCode) => statusCode is
|
||||||
|
HttpStatusCode.TooManyRequests or
|
||||||
|
HttpStatusCode.RequestTimeout or
|
||||||
|
HttpStatusCode.InternalServerError;
|
||||||
|
|
||||||
|
private static bool IsRetryableException(Exception exception) =>
|
||||||
|
exception.GetSelfAndChildren().Any(ex =>
|
||||||
|
ex is TimeoutException or SocketException ||
|
||||||
|
ex is HttpRequestException hrex && IsRetryableStatusCode(hrex.TryGetStatusCode() ?? HttpStatusCode.OK)
|
||||||
|
);
|
||||||
|
|
||||||
|
public static IAsyncPolicy ResiliencePolicy { get; } =
|
||||||
Policy
|
Policy
|
||||||
.Handle<IOException>()
|
.Handle<Exception>(IsRetryableException)
|
||||||
.Or<HttpRequestException>()
|
.WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1));
|
||||||
.OrResult<HttpResponseMessage>(m => m.StatusCode == HttpStatusCode.TooManyRequests)
|
|
||||||
.OrResult(m => m.StatusCode == HttpStatusCode.RequestTimeout)
|
public static IAsyncPolicy<HttpResponseMessage> ResponseResiliencePolicy { get; } =
|
||||||
.OrResult(m => m.StatusCode >= HttpStatusCode.InternalServerError)
|
Policy
|
||||||
|
.Handle<Exception>(IsRetryableException)
|
||||||
|
.OrResult<HttpResponseMessage>(m => IsRetryableStatusCode(m.StatusCode))
|
||||||
.WaitAndRetryAsync(
|
.WaitAndRetryAsync(
|
||||||
8,
|
8,
|
||||||
(i, result, _) =>
|
(i, result, _) =>
|
||||||
@@ -43,24 +55,4 @@ public static class Http
|
|||||||
},
|
},
|
||||||
(_, _, _, _) => Task.CompletedTask
|
(_, _, _, _) => Task.CompletedTask
|
||||||
);
|
);
|
||||||
|
|
||||||
private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex) =>
|
|
||||||
// This is extremely frail, but there's no other way
|
|
||||||
Regex
|
|
||||||
.Match(ex.Message, @": (\d+) \(")
|
|
||||||
.Groups[1]
|
|
||||||
.Value
|
|
||||||
.NullIfWhiteSpace()?
|
|
||||||
.Pipe(s => (HttpStatusCode) int.Parse(s, CultureInfo.InvariantCulture));
|
|
||||||
|
|
||||||
public static IAsyncPolicy ExceptionPolicy { get; } =
|
|
||||||
Policy
|
|
||||||
.Handle<IOException>() // dangerous
|
|
||||||
.Or<HttpRequestException>(ex =>
|
|
||||||
TryGetStatusCodeFromException(ex) is
|
|
||||||
HttpStatusCode.TooManyRequests or
|
|
||||||
HttpStatusCode.RequestTimeout or
|
|
||||||
HttpStatusCode.InternalServerError
|
|
||||||
)
|
|
||||||
.WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1));
|
|
||||||
}
|
}
|
||||||
@@ -14,14 +14,14 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Gress" Version="2.0.1" />
|
<PackageReference Include="Gress" Version="2.0.1" />
|
||||||
<PackageReference Include="MaterialDesignColors" Version="2.0.5" />
|
<PackageReference Include="MaterialDesignColors" Version="2.0.6" />
|
||||||
<PackageReference Include="MaterialDesignThemes" Version="4.4.0" />
|
<PackageReference Include="MaterialDesignThemes" Version="4.5.0" />
|
||||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||||
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
|
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
|
||||||
<PackageReference Include="Onova" Version="2.6.2" />
|
<PackageReference Include="Onova" Version="2.6.2" />
|
||||||
<PackageReference Include="Stylet" Version="1.3.6" />
|
<PackageReference Include="Stylet" Version="1.3.6" />
|
||||||
<PackageReference Include="Tyrrrz.Settings" Version="1.3.4" />
|
<PackageReference Include="Tyrrrz.Settings" Version="1.3.4" />
|
||||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" PrivateAssets="all" />
|
<PackageReference Include="PropertyChanged.Fody" Version="3.4.1" PrivateAssets="all" />
|
||||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.2.0" PrivateAssets="all" />
|
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.2.0" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -2,6 +2,9 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<clear />
|
<clear />
|
||||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
</configuration>
|
<config>
|
||||||
|
<add key="defaultPushSource" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
</config>
|
||||||
|
</configuration>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# DiscordChatExporter
|
# DiscordChatExporter
|
||||||
|
|
||||||
[](https://vshymanskyy.github.io/StandWithUkraine)
|
[](https://vshymanskyy.github.io/StandWithUkraine)
|
||||||
[](https://github.com/Tyrrrz/DiscordChatExporter/actions)
|
[](https://github.com/Tyrrrz/DiscordChatExporter/actions)
|
||||||
[](https://codecov.io/gh/Tyrrrz/DiscordChatExporter)
|
[](https://codecov.io/gh/Tyrrrz/DiscordChatExporter)
|
||||||
[](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
[](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
||||||
[](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
[](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
||||||
@@ -16,7 +16,9 @@ It works with direct messages, group messages, and server channels, and supports
|
|||||||
|
|
||||||
> ❔ If you have questions or issues, **please refer to the [wiki](https://github.com/Tyrrrz/DiscordChatExporter/wiki)**.
|
> ❔ If you have questions or issues, **please refer to the [wiki](https://github.com/Tyrrrz/DiscordChatExporter/wiki)**.
|
||||||
|
|
||||||
## Terms of use
|
> 💬 If you want to chat, **join my [Discord server](https://discord.gg/2SUWKFnHSm)**.
|
||||||
|
|
||||||
|
## Terms of use<sup>[[?]](https://github.com/Tyrrrz/.github/blob/master/docs/why-so-political.md)</sup>
|
||||||
|
|
||||||
By using this project or its source code, for any purpose and in any shape or form, you grant your **implicit agreement** to all the following statements:
|
By using this project or its source code, for any purpose and in any shape or form, you grant your **implicit agreement** to all the following statements:
|
||||||
|
|
||||||
@@ -46,7 +48,7 @@ The following table lists all available download options:
|
|||||||
<td>
|
<td>
|
||||||
<ul>
|
<ul>
|
||||||
<li>🟢 <b><a href="https://github.com/Tyrrrz/DiscordChatExporter/releases/latest">Stable release</a></b> (<code>DiscordChatExporter.zip</code>)</li>
|
<li>🟢 <b><a href="https://github.com/Tyrrrz/DiscordChatExporter/releases/latest">Stable release</a></b> (<code>DiscordChatExporter.zip</code>)</li>
|
||||||
<li>🟠 <a href="https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/CI.yml">CI build</a> (<code>DiscordChatExporter.zip</code>)</li>
|
<li>🟠 <a href="https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/main.yml">CI build</a> (<code>DiscordChatExporter.zip</code>)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -60,7 +62,7 @@ The following table lists all available download options:
|
|||||||
<td>
|
<td>
|
||||||
<ul>
|
<ul>
|
||||||
<li>🟢 <b><a href="https://github.com/Tyrrrz/DiscordChatExporter/releases/latest">Stable release</a></b> (<code>DiscordChatExporter.CLI.zip</code>) ⚙️</li>
|
<li>🟢 <b><a href="https://github.com/Tyrrrz/DiscordChatExporter/releases/latest">Stable release</a></b> (<code>DiscordChatExporter.CLI.zip</code>) ⚙️</li>
|
||||||
<li>🟠 <a href="https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/CI.yml">CI build</a> (<code>DiscordChatExporter.CLI.zip</code>) ⚙️</li>
|
<li>🟠 <a href="https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/main.yml">CI build</a> (<code>DiscordChatExporter.CLI.zip</code>) ⚙️</li>
|
||||||
<li>🐋 <a href="https://hub.docker.com/r/tyrrrz/discordchatexporter">Docker</a> (<code>tyrrrz/discordchatexporter</code>)</li>
|
<li>🐋 <a href="https://hub.docker.com/r/tyrrrz/discordchatexporter">Docker</a> (<code>tyrrrz/discordchatexporter</code>)</li>
|
||||||
<li>📦 <a href="https://aur.archlinux.org/packages/discord-chat-exporter-cli">AUR</a> (<code>discord-chat-exporter-cli</code>) 🦄</li>
|
<li>📦 <a href="https://aur.archlinux.org/packages/discord-chat-exporter-cli">AUR</a> (<code>discord-chat-exporter-cli</code>) 🦄</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -95,5 +97,5 @@ The following table lists all available download options:
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
Reference in New Issue
Block a user