mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-12 00:41:55 +02:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a405ab184e | |||
| b861a69799 | |||
| d53b4c825a | |||
| 30678e96e5 | |||
| 2024d3658b | |||
| 9453cac389 | |||
| 504c3c53cd | |||
| 4e358e8e6f | |||
| 5e852235ce | |||
| 612c6d37a4 | |||
| 92cf886eab | |||
| f48af53d72 | |||
| 99d721c63b | |||
| 11414b5937 | |||
| 6c039ebfa6 | |||
| 9cffbbc5ce | |||
| 6f87960513 | |||
| e8f8c47dbd | |||
| 9aba36fce5 | |||
| f29e1d2d9f | |||
| b000a72f81 |
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ⚠ Feature request
|
||||
url: https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md
|
||||
about: Sorry, but this project is in maintenance mode and no longer accepts new feature requests.
|
||||
- name: 💬 Discord server
|
||||
url: https://discord.gg/2SUWKFnHSm
|
||||
about: Chat with the project community.
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
name: ✨ Feature request
|
||||
description: Request a new feature.
|
||||
labels: [enhancement]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
🧐 **Guidelines:**
|
||||
|
||||
- Search through [existing issues](https://github.com/Tyrrrz/DiscordChatExporter/issues?q=is%3Aissue) first to ensure that this feature has not been requested before.
|
||||
- Write a descriptive title for your issue. Avoid generic or vague titles such as "Some suggestions" or "Ideas for improvement".
|
||||
- Keep your issue focused on one single problem. If you have multiple feature requests, please create separate issues for each of them.
|
||||
- Provide as much context as possible in the details section. Include screenshots, screen recordings, links, references, or anything else you may consider relevant.
|
||||
- If you want to ask a question instead of requesting a feature, please use [discussions](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new) instead.
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Flavor
|
||||
description: Is this feature request related to specific flavor(s) of DiscordChatExporter? If not applicable, leave this field blank.
|
||||
multiple: true
|
||||
options:
|
||||
- GUI (Graphical User Interface)
|
||||
- CLI (Command Line Interface)
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Export format
|
||||
description: Is this feature request related to specific export format(s)? If not applicable, leave this field blank.
|
||||
multiple: true
|
||||
options:
|
||||
- HTML
|
||||
- TXT
|
||||
- JSON
|
||||
- CSV
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Details
|
||||
description: Clear and thorough explanation of the feature you have in mind.
|
||||
validations:
|
||||
required: true
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
publish:
|
||||
package:
|
||||
needs: test
|
||||
runs-on: windows-latest
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
|
||||
deploy:
|
||||
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
|
||||
needs: publish
|
||||
needs: package
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -151,4 +151,4 @@ jobs:
|
||||
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>"
|
||||
}
|
||||
}
|
||||
|
||||
+20
-4
@@ -1,3 +1,18 @@
|
||||
### v2.35.2 (08-Sep-2022)
|
||||
|
||||
- Updated usage guide to mention the need to enable "Message Content Intent" to use a bot as a medium for exporting messages.
|
||||
- [CLI] Fixed an issue where passing an existing directory as the output path failed when exporting multiple channels (for example, when running `exportdm`). An ambiguous output path will now always be treated as a directory path if that directory already exists. If it doesn't exist, it will be treated as a file path, matching the previous behavior.
|
||||
|
||||
### v2.35.1 (26-Aug-2022)
|
||||
|
||||
- Added `~` character as an alias for `-` when negating a message filter. This should make it easier to use negated filters from CLI where the dash character already has other meanings.
|
||||
- Extended retry logic to encompass all server-side errors from Discord API, instead of only `500 Internal Server Error`.
|
||||
- [CLI] Attempt to export multiple channels will now fail if provided with an output path that is neither a directory, nor a file name template. Previously, this resulted in all channels being exported to the same file, overwriting each other.
|
||||
- Fixed an issue where a text message filter didn't work if it started with a character that's also the first character in one of the named filters (e.g. `from` or `mentions`).
|
||||
- Fixed an issue where a text message filter didn't work correctly if it was enclosed in parentheses.
|
||||
- Fixed an issue where categories were displayed alongside regular channels in the channel list, both in GUI and in CLI (when using the `guild` command).
|
||||
- [CLI] Fixed an issue where an output path starting with a tilde on Linux was not expanded properly into a full path. (Thanks [@TSRBerry](https://github.com/TSRBerry))
|
||||
|
||||
### v2.35 (02-Jul-2022)
|
||||
|
||||
- Added support for exporting text chat in voice channels. These channels are now also displayed when browsing in the GUI and in the CLI when executing the `channels` command.
|
||||
@@ -17,6 +32,7 @@
|
||||
- Added file name template token that resolves to the current date (`%d`). (Thanks [@Lucas LaBuff](https://github.com/96-LB))
|
||||
- Updated the usage guide to replace the instructions for retrieving the user token with those that appear to work for more people.
|
||||
- Updated the usage guide to feature the TOS warning more prominently.
|
||||
- [Docker] Changed internal working directory from `/app/out` to `/out`. This was an unintended breaking change. Please update your volume binding configuration from `-v /path/on/machine:/app/out` to `-v /path/on/machine:/out`.
|
||||
|
||||
### v2.34.1 (01-Jun-2022)
|
||||
|
||||
@@ -86,7 +102,7 @@
|
||||
|
||||
- [HTML] Added special casing for Spotify track embeds to render them directly using an embedded player.
|
||||
- [HTML] Added special casing for plain image embeds to render them like image attachments.
|
||||
- [HTML] Added tooltip text for unix timestamp markers which shows the full date represented by the timestamp, regardless of configured date format.
|
||||
- [HTML] Added tooltip text for unix timestamp markers which shows the full date represented by the timestamp, regardless of configured date format.
|
||||
- [HTML] Updated the fallback text shown when the referenced message could not be loaded to make it more explicit.
|
||||
- [HTML] Updated styling of quoted text in markdown to more closely resemble how it looks in Discord.
|
||||
- [HTML] Fixed an issue where some file formats were not correctly recognized as image or video attachments, which affected how they were rendered.
|
||||
@@ -113,7 +129,7 @@
|
||||
- [HTML] Added special casing for YouTube video embeds to render them directly using an embedded player. (Thanks [@quentinmay](https://github.com/quentinmay))
|
||||
- [HTML] Added support for rendering standard emoji by code. Such emoji may sometimes be found in messages sent by bots or through webhooks. (Thanks [@CanePlayz](https://github.com/CanePlayz) and [@Lucas LaBuff](https://github.com/96-LB))
|
||||
- [HTML] Changed tooltips on standard emoji to show emoji code instead of their raw string representations. (Thanks [@CanePlayz](https://github.com/CanePlayz) and [@Lucas LaBuff](https://github.com/96-LB))
|
||||
- [HTML] Added tooltips on individual messages to show when those messages were sent.
|
||||
- [HTML] Added tooltips on individual messages to show when those messages were sent.
|
||||
- [HTML] Updated colors and minor styling elements to match Discord's new style direction after rebranding.
|
||||
- [HTML] Updated Twemoji URLs to use SVG image variants for better rendering quality.
|
||||
- [HTML] Changed user avatar URLs to include predetermined size query parameter for better rendering quality.
|
||||
@@ -209,7 +225,7 @@
|
||||
- Added a new option that enables self-contained exports for all output formats. You can turn it on in the export setup dialog in GUI or using the `--media` option in CLI. When using this, the application will additionally download any media content directly referenced from the exported file instead of linking back to Discord CDN. The files which are downloaded include: guild icons, user avatars, attachments, embedded images, reaction emoji. Note that only files which are actually referenced by the export are downloaded, which means that, for example, user avatars will not be downloaded when using plain text export format. This option is not meant to enable complete offline viewing for HTML exports, but rather to make it easier to archive media content that may eventually get deleted from Discord servers. Also keep in mind that this option may make the export drastically slower and the total file size larger.
|
||||
- Changed "discordapp.com" to "discord.com" where applicable as Discord is migrating to a new domain. CDN will remain on "cdn.discordapp.com" for the foreseeable future.
|
||||
|
||||
Note that all existing and current HTML exports will likely not render accurately because Discord enabled CORS for their font resources, which prevents them from loading locally. Please refer to [issue #322](https://github.com/Tyrrrz/DiscordChatExporter/issues/322) for discussion on this topic.
|
||||
Note that all existing and current HTML exports will likely not render accurately because Discord enabled CORS for their font resources, which prevents them from loading locally. Please refer to [issue #322](https://github.com/Tyrrrz/DiscordChatExporter/issues/322) for discussion on this topic.
|
||||
|
||||
### v2.20 (27-Apr-2020)
|
||||
|
||||
@@ -418,4 +434,4 @@ Please note that CSV export is most likely not going to receive future updates a
|
||||
- Added support for user and role mentions.
|
||||
- Added support for channel mentions.
|
||||
- Fixed text in pre blocks not being wrapped correctly.
|
||||
- Added workaround for non-default message types.
|
||||
- Added workaround for non-default message types.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Version>2.35</Version>
|
||||
<Version>2.35.2</Version>
|
||||
<Company>Tyrrrz</Company>
|
||||
<Copyright>Copyright (c) Oleksii Holub</Copyright>
|
||||
<LangVersion>preview</LangVersion>
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.1" PrivateAssets="all" />
|
||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
|
||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -14,18 +14,27 @@ using DiscordChatExporter.Core.Exceptions;
|
||||
using DiscordChatExporter.Core.Exporting;
|
||||
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||
using DiscordChatExporter.Core.Utils;
|
||||
using Gress;
|
||||
|
||||
namespace DiscordChatExporter.Cli.Commands.Base;
|
||||
|
||||
public abstract class ExportCommandBase : TokenCommandBase
|
||||
{
|
||||
private readonly string _outputPath = Directory.GetCurrentDirectory();
|
||||
|
||||
[CommandOption(
|
||||
"output",
|
||||
'o',
|
||||
Description = "Output file or directory path."
|
||||
Description = "Output file or directory path. Directory path should end in a slash."
|
||||
)]
|
||||
public string OutputPath { get; init; } = Directory.GetCurrentDirectory();
|
||||
public string OutputPath
|
||||
{
|
||||
get => _outputPath;
|
||||
// Handle ~/ in paths on *nix systems
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/pull/903
|
||||
init => _outputPath = Path.GetFullPath(value);
|
||||
}
|
||||
|
||||
[CommandOption(
|
||||
"format",
|
||||
@@ -88,13 +97,35 @@ public abstract class ExportCommandBase : TokenCommandBase
|
||||
|
||||
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Channel> channels)
|
||||
{
|
||||
var cancellationToken = console.RegisterCancellationHandler();
|
||||
|
||||
// Reuse media option should only be used when the media option is set.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/425
|
||||
if (ShouldReuseMedia && !ShouldDownloadMedia)
|
||||
{
|
||||
throw new CommandException("Option --reuse-media cannot be used without --media.");
|
||||
throw new CommandException(
|
||||
"Option --reuse-media cannot be used without --media."
|
||||
);
|
||||
}
|
||||
|
||||
// Make sure the user does not try to export all channels into a single file.
|
||||
// Output path must either be a directory, or contain template tokens.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/799
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/917
|
||||
var isValidOutputPath =
|
||||
// Anything is valid when exporting a single channel
|
||||
channels.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) || PathEx.IsDirectoryPath(OutputPath);
|
||||
|
||||
if (!isValidOutputPath)
|
||||
{
|
||||
throw new CommandException(
|
||||
"Attempted to export multiple channels, but the output path is neither a directory nor a template."
|
||||
);
|
||||
}
|
||||
|
||||
var cancellationToken = console.RegisterCancellationHandler();
|
||||
var errors = new ConcurrentDictionary<Channel, string>();
|
||||
|
||||
// Export
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using DiscordChatExporter.Cli.Commands.Base;
|
||||
@@ -17,7 +18,10 @@ public class ExportDirectMessagesCommand : ExportCommandBase
|
||||
var cancellationToken = console.RegisterCancellationHandler();
|
||||
|
||||
await console.Output.WriteLineAsync("Fetching channels...");
|
||||
var channels = await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken);
|
||||
|
||||
var channels = (await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken))
|
||||
.Where(c => c.Kind != ChannelKind.GuildCategory)
|
||||
.ToArray();
|
||||
|
||||
await base.ExecuteAsync(console, channels);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using DiscordChatExporter.Cli.Commands.Base;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
@@ -25,7 +27,10 @@ public class ExportGuildCommand : ExportCommandBase
|
||||
var cancellationToken = console.RegisterCancellationHandler();
|
||||
|
||||
await console.Output.WriteLineAsync("Fetching channels...");
|
||||
var channels = await Discord.GetGuildChannelsAsync(GuildId, cancellationToken);
|
||||
|
||||
var channels = (await Discord.GetGuildChannelsAsync(GuildId, cancellationToken))
|
||||
.Where(c => c.Kind != ChannelKind.GuildCategory)
|
||||
.ToArray();
|
||||
|
||||
await base.ExecuteAsync(console, channels);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using DiscordChatExporter.Cli.Commands.Base;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
@@ -25,6 +26,7 @@ public class GetChannelsCommand : TokenCommandBase
|
||||
var cancellationToken = console.RegisterCancellationHandler();
|
||||
|
||||
var channels = (await Discord.GetGuildChannelsAsync(GuildId, cancellationToken))
|
||||
.Where(c => c.Kind != ChannelKind.GuildCategory)
|
||||
.OrderBy(c => c.Category.Position)
|
||||
.ThenBy(c => c.Name)
|
||||
.ToArray();
|
||||
|
||||
@@ -17,6 +17,7 @@ public class GetDirectMessageChannelsCommand : TokenCommandBase
|
||||
var cancellationToken = console.RegisterCancellationHandler();
|
||||
|
||||
var channels = (await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken))
|
||||
.Where(c => c.Kind != ChannelKind.GuildCategory)
|
||||
.OrderByDescending(c => c.LastMessageId)
|
||||
.ThenBy(c => c.Name)
|
||||
.ToArray();
|
||||
|
||||
@@ -17,13 +17,14 @@ public class GuideCommand : ICommand
|
||||
|
||||
console.Output.WriteLine(" * Automating user accounts is technically against TOS — USE AT YOUR OWN RISK!");
|
||||
console.Output.WriteLine(" 1. Open Discord in your web browser and login");
|
||||
console.Output.WriteLine(" 2. Press Ctrl+Shift+I to show developer tools");
|
||||
console.Output.WriteLine(" 3. Navigate to the Network tab");
|
||||
console.Output.WriteLine(" 4. Press Ctrl+R to reload");
|
||||
console.Output.WriteLine(" 5. Search for a request containing \"messages?limit=50\" or similar");
|
||||
console.Output.WriteLine(" 6. Select the Headers tab on the right");
|
||||
console.Output.WriteLine(" 7. Scroll down to the Request Headers section");
|
||||
console.Output.WriteLine(" 8. Copy the value of the \"authorization\" header");
|
||||
console.Output.WriteLine(" 2. Open any server or direct message channel");
|
||||
console.Output.WriteLine(" 3. Press Ctrl+Shift+I to show developer tools");
|
||||
console.Output.WriteLine(" 4. Navigate to the Network tab");
|
||||
console.Output.WriteLine(" 5. Press Ctrl+R to reload");
|
||||
console.Output.WriteLine(" 6. Search for a request containing \"messages?limit=50\" or similar");
|
||||
console.Output.WriteLine(" 7. Select the Headers tab on the right");
|
||||
console.Output.WriteLine(" 8. Scroll down to the Request Headers section");
|
||||
console.Output.WriteLine(" 9. Copy the value of the \"authorization\" header");
|
||||
console.Output.WriteLine();
|
||||
|
||||
// Bot token
|
||||
@@ -34,6 +35,7 @@ public class GuideCommand : ICommand
|
||||
console.Output.WriteLine(" 2. Open your application's settings");
|
||||
console.Output.WriteLine(" 3. Navigate to the Bot section on the left");
|
||||
console.Output.WriteLine(" 4. Under Token click Copy");
|
||||
console.Output.WriteLine(" * Your bot needs to have Message Content Intent enabled to read messages");
|
||||
console.Output.WriteLine();
|
||||
|
||||
// Guild or channel ID
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliFx" Version="2.2.5" />
|
||||
<PackageReference Include="CliFx" Version="2.3.0" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.44.0" />
|
||||
<PackageReference Include="Gress" Version="2.0.1" />
|
||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.3.1" PrivateAssets="all" />
|
||||
|
||||
@@ -11,12 +11,12 @@ namespace DiscordChatExporter.Core.Discord.Data;
|
||||
public partial record Emoji(
|
||||
// Only present on custom emoji
|
||||
Snowflake? Id,
|
||||
// Name of custom emoji (e.g. LUL) or actual representation of standard emoji (e.g. 🙂)
|
||||
// Name of a custom emoji (e.g. LUL) or actual representation of a standard emoji (e.g. 🙂)
|
||||
string Name,
|
||||
bool IsAnimated,
|
||||
string ImageUrl)
|
||||
{
|
||||
// Name of custom emoji (e.g. LUL) or name of standard emoji (e.g. slight_smile)
|
||||
// Name of a custom emoji (e.g. LUL) or name of a standard emoji (e.g. slight_smile)
|
||||
public string Code => Id is not null
|
||||
? Name
|
||||
: EmojiIndex.TryGetCode(Name) ?? Name;
|
||||
@@ -24,17 +24,19 @@ public partial record Emoji(
|
||||
|
||||
public partial record Emoji
|
||||
{
|
||||
private static string GetTwemojiName(string name)
|
||||
private static string GetTwemojiId(string name)
|
||||
{
|
||||
var runes = name.GetRunes();
|
||||
var runes = name.GetRunes().ToArray();
|
||||
|
||||
// Variant selector rune is skipped in Twemoji names, except when the emoji also contains a zero-width joiner.
|
||||
// VS = 0xfe0f; ZWJ = 0x200d.
|
||||
var filteredRunes = runes.Any(r => r.Value == 0x200d)
|
||||
? runes
|
||||
: runes.Where(r => r.Value != 0xfe0f);
|
||||
|
||||
return string.Join(
|
||||
"-",
|
||||
// Variant selector rune is skipped in Twemoji names, except when the emoji also contains a zero-width joiner.
|
||||
// VS = 0xfe0f. ZWJ = 0x200d.
|
||||
runes.Any(r => r.Value == 0x200d)
|
||||
? runes.Select(r => r.Value.ToString("x"))
|
||||
: runes.Where(r => r.Value != 0xfe0f).Select(r => r.Value.ToString("x"))
|
||||
filteredRunes.Select(r => r.Value.ToString("x"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +45,7 @@ public partial record Emoji
|
||||
: $"https://cdn.discordapp.com/emojis/{id}.png";
|
||||
|
||||
private static string GetImageUrl(string name) =>
|
||||
$"https://twemoji.maxcdn.com/2/svg/{GetTwemojiName(name)}.svg";
|
||||
$"https://twemoji.maxcdn.com/v/latest/svg/{GetTwemojiId(name)}.svg";
|
||||
|
||||
public static string GetImageUrl(Snowflake? id, string? name, bool isAnimated)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using DiscordChatExporter.Core.Discord.Data.Common;
|
||||
using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using JsonExtensions.Reading;
|
||||
@@ -17,8 +18,14 @@ public record Guild(Snowflake Id, string Name, string IconUrl) : IHasId
|
||||
private static string GetDefaultIconUrl() =>
|
||||
"https://cdn.discordapp.com/embed/avatars/0.png";
|
||||
|
||||
private static string GetIconUrl(Snowflake id, string iconHash) =>
|
||||
$"https://cdn.discordapp.com/icons/{id}/{iconHash}.png";
|
||||
private static string GetIconUrl(Snowflake id, string iconHash)
|
||||
{
|
||||
var extension = iconHash.StartsWith("a_", StringComparison.Ordinal)
|
||||
? "gif"
|
||||
: "png";
|
||||
|
||||
return $"https://cdn.discordapp.com/icons/{id}/{iconHash}.{extension}";
|
||||
}
|
||||
|
||||
public static Guild Parse(JsonElement json)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<PackageReference Include="MiniRazor.CodeGen" Version="2.2.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
<PackageReference Include="Superpower" Version="3.0.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.9.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.12.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,11 +10,19 @@ internal class ContainsMessageFilter : MessageFilter
|
||||
|
||||
public ContainsMessageFilter(string text) => _text = text;
|
||||
|
||||
// Match content within word boundaries, between spaces, or as the whole input.
|
||||
// For example, "max" shouldn't match on content "our maximum effort",
|
||||
// but should match on content "our max effort".
|
||||
// Also, "(max)" should match on content "our (max) effort", even though
|
||||
// parentheses are not considered word characters.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/909
|
||||
private bool IsMatch(string? content) =>
|
||||
!string.IsNullOrWhiteSpace(content) &&
|
||||
Regex.IsMatch(
|
||||
content,
|
||||
"\\b" + Regex.Escape(_text) + "\\b",
|
||||
@"(?:\b|\s|^)" +
|
||||
Regex.Escape(_text) +
|
||||
@"(?:\b|\s|$)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
|
||||
);
|
||||
|
||||
|
||||
@@ -15,95 +15,107 @@ internal static class FilterGrammar
|
||||
from close in Character.EqualTo(open)
|
||||
select value;
|
||||
|
||||
private static readonly TextParser<char> FreeCharacter =
|
||||
Character.Matching(c =>
|
||||
!char.IsWhiteSpace(c) &&
|
||||
// Avoid all special tokens used by the grammar
|
||||
c is not ('(' or ')' or '"' or '\'' or '-' or '|' or '&'),
|
||||
"any character except whitespace or `(`, `)`, `\"`, `'`, `-`, `|`, `&`"
|
||||
);
|
||||
|
||||
private static readonly TextParser<string> UnquotedString =
|
||||
Parse.OneOf(EscapedCharacter, FreeCharacter).AtLeastOnce().Text();
|
||||
Parse.OneOf(
|
||||
EscapedCharacter,
|
||||
// Avoid whitespace as it's treated as an implicit 'and' operator.
|
||||
// Also avoid all special tokens used by other parsers.
|
||||
Character.ExceptIn(' ', '(', ')', '"', '\'', '-', '~', '|', '&')
|
||||
).AtLeastOnce().Text();
|
||||
|
||||
private static readonly TextParser<string> String =
|
||||
Parse.OneOf(QuotedString, UnquotedString).Named("text string");
|
||||
|
||||
private static readonly TextParser<MessageFilter> ContainsFilter =
|
||||
String.Select(v => (MessageFilter) new ContainsMessageFilter(v));
|
||||
String.Select(v => (MessageFilter)new ContainsMessageFilter(v));
|
||||
|
||||
private static readonly TextParser<MessageFilter> FromFilter = Span
|
||||
.EqualToIgnoreCase("from:")
|
||||
.IgnoreThen(String)
|
||||
.Select(v => (MessageFilter) new FromMessageFilter(v))
|
||||
.Named("from:<value>");
|
||||
private static readonly TextParser<MessageFilter> FromFilter =
|
||||
Span
|
||||
.EqualToIgnoreCase("from:")
|
||||
.Try()
|
||||
.IgnoreThen(String)
|
||||
.Select(v => (MessageFilter)new FromMessageFilter(v))
|
||||
.Named("from:<value>");
|
||||
|
||||
private static readonly TextParser<MessageFilter> MentionsFilter = Span
|
||||
.EqualToIgnoreCase("mentions:")
|
||||
.IgnoreThen(String)
|
||||
.Select(v => (MessageFilter) new MentionsMessageFilter(v))
|
||||
.Named("mentions:<value>");
|
||||
private static readonly TextParser<MessageFilter> MentionsFilter =
|
||||
Span
|
||||
.EqualToIgnoreCase("mentions:")
|
||||
.Try()
|
||||
.IgnoreThen(String)
|
||||
.Select(v => (MessageFilter)new MentionsMessageFilter(v))
|
||||
.Named("mentions:<value>");
|
||||
|
||||
private static readonly TextParser<MessageFilter> ReactionFilter = Span
|
||||
.EqualToIgnoreCase("reaction:")
|
||||
.IgnoreThen(String)
|
||||
.Select(v => (MessageFilter) new ReactionMessageFilter(v))
|
||||
.Named("reaction:<value>");
|
||||
private static readonly TextParser<MessageFilter> ReactionFilter =
|
||||
Span
|
||||
.EqualToIgnoreCase("reaction:")
|
||||
.Try()
|
||||
.IgnoreThen(String)
|
||||
.Select(v => (MessageFilter)new ReactionMessageFilter(v))
|
||||
.Named("reaction:<value>");
|
||||
|
||||
private static readonly TextParser<MessageFilter> HasFilter = Span
|
||||
.EqualToIgnoreCase("has:")
|
||||
.IgnoreThen(Parse.OneOf(
|
||||
Span.EqualToIgnoreCase("link").IgnoreThen(Parse.Return(MessageContentMatchKind.Link)),
|
||||
Span.EqualToIgnoreCase("embed").IgnoreThen(Parse.Return(MessageContentMatchKind.Embed)),
|
||||
Span.EqualToIgnoreCase("file").IgnoreThen(Parse.Return(MessageContentMatchKind.File)),
|
||||
Span.EqualToIgnoreCase("video").IgnoreThen(Parse.Return(MessageContentMatchKind.Video)),
|
||||
Span.EqualToIgnoreCase("image").IgnoreThen(Parse.Return(MessageContentMatchKind.Image)),
|
||||
Span.EqualToIgnoreCase("sound").IgnoreThen(Parse.Return(MessageContentMatchKind.Sound)),
|
||||
Span.EqualToIgnoreCase("pin").IgnoreThen(Parse.Return(MessageContentMatchKind.Pin))
|
||||
))
|
||||
.Select(k => (MessageFilter) new HasMessageFilter(k))
|
||||
.Named("has:<value>");
|
||||
private static readonly TextParser<MessageFilter> HasFilter =
|
||||
Span
|
||||
.EqualToIgnoreCase("has:")
|
||||
.Try()
|
||||
.IgnoreThen(Parse.OneOf(
|
||||
Span.EqualToIgnoreCase("link").IgnoreThen(Parse.Return(MessageContentMatchKind.Link)),
|
||||
Span.EqualToIgnoreCase("embed").IgnoreThen(Parse.Return(MessageContentMatchKind.Embed)),
|
||||
Span.EqualToIgnoreCase("file").IgnoreThen(Parse.Return(MessageContentMatchKind.File)),
|
||||
Span.EqualToIgnoreCase("video").IgnoreThen(Parse.Return(MessageContentMatchKind.Video)),
|
||||
Span.EqualToIgnoreCase("image").IgnoreThen(Parse.Return(MessageContentMatchKind.Image)),
|
||||
Span.EqualToIgnoreCase("sound").IgnoreThen(Parse.Return(MessageContentMatchKind.Sound)),
|
||||
Span.EqualToIgnoreCase("pin").IgnoreThen(Parse.Return(MessageContentMatchKind.Pin))
|
||||
))
|
||||
.Select(k => (MessageFilter)new HasMessageFilter(k))
|
||||
.Named("has:<value>");
|
||||
|
||||
private static readonly TextParser<MessageFilter> NegatedFilter = Character
|
||||
.EqualTo('-')
|
||||
.IgnoreThen(Parse.Ref(() => StandaloneFilter!))
|
||||
.Select(f => (MessageFilter) new NegatedMessageFilter(f));
|
||||
// Make sure that property-based filters like 'has:link' don't prevent text like 'hello' from being parsed.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/909#issuecomment-1227575455
|
||||
private static readonly TextParser<MessageFilter> PrimitiveFilter =
|
||||
Parse.OneOf(
|
||||
FromFilter,
|
||||
MentionsFilter,
|
||||
ReactionFilter,
|
||||
HasFilter,
|
||||
ContainsFilter
|
||||
);
|
||||
|
||||
private static readonly TextParser<MessageFilter> GroupedFilter =
|
||||
from open in Character.EqualTo('(')
|
||||
from content in Parse.Ref(() => BinaryExpressionFilter!).Token()
|
||||
from content in Parse.Ref(() => ChainedFilter!).Token()
|
||||
from close in Character.EqualTo(')')
|
||||
select content;
|
||||
|
||||
private static readonly TextParser<MessageFilter> StandaloneFilter = Parse.OneOf(
|
||||
GroupedFilter,
|
||||
FromFilter,
|
||||
MentionsFilter,
|
||||
ReactionFilter,
|
||||
HasFilter,
|
||||
ContainsFilter
|
||||
);
|
||||
private static readonly TextParser<MessageFilter> NegatedFilter =
|
||||
Character
|
||||
// Dash is annoying to use from CLI due to conflicts with options, so we provide tilde as an alias
|
||||
.In('-', '~')
|
||||
.IgnoreThen(Parse.OneOf(GroupedFilter, PrimitiveFilter))
|
||||
.Select(f => (MessageFilter)new NegatedMessageFilter(f));
|
||||
|
||||
private static readonly TextParser<MessageFilter> UnaryExpressionFilter = Parse.OneOf(
|
||||
NegatedFilter,
|
||||
StandaloneFilter
|
||||
);
|
||||
|
||||
private static readonly TextParser<MessageFilter> BinaryExpressionFilter = Parse.Chain(
|
||||
Parse.OneOf(
|
||||
// Explicit operator
|
||||
Character.In('|', '&').Token().Try(),
|
||||
// Implicit operator (resolves to 'and')
|
||||
Character.WhiteSpace.AtLeastOnce().IgnoreThen(Parse.Return(' '))
|
||||
),
|
||||
UnaryExpressionFilter,
|
||||
(op, left, right) => op switch
|
||||
{
|
||||
'|' => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.Or),
|
||||
_ => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.And)
|
||||
}
|
||||
);
|
||||
private static readonly TextParser<MessageFilter> ChainedFilter =
|
||||
Parse.Chain(
|
||||
// Operator
|
||||
Parse.OneOf(
|
||||
// Explicit operator
|
||||
Character.In('|', '&').Token().Try(),
|
||||
// Implicit operator (resolves to 'and')
|
||||
Character.EqualTo(' ').AtLeastOnce().IgnoreThen(Parse.Return(' '))
|
||||
),
|
||||
// Operand
|
||||
Parse.OneOf(
|
||||
NegatedFilter,
|
||||
GroupedFilter,
|
||||
PrimitiveFilter
|
||||
),
|
||||
// Reducer
|
||||
(op, left, right) => op switch
|
||||
{
|
||||
'|' => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.Or),
|
||||
_ => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.And)
|
||||
}
|
||||
);
|
||||
|
||||
public static readonly TextParser<MessageFilter> Filter =
|
||||
BinaryExpressionFilter.Token().AtEnd();
|
||||
ChainedFilter.Token().AtEnd();
|
||||
}
|
||||
@@ -725,18 +725,17 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.chatlog__sticker--media[data-source]').forEach(e => {
|
||||
const imageDataUrl = e.getAttribute('data-source');
|
||||
|
||||
const anim = lottie.loadAnimation({
|
||||
container: e,
|
||||
renderer: 'svg',
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
path: imageDataUrl
|
||||
container: e,
|
||||
renderer: 'svg',
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
path: e.getAttribute('data-source')
|
||||
});
|
||||
|
||||
anim.addEventListener('data_failed', () =>
|
||||
e.innerHTML = '<strong>[Sticker cannot be rendered]</strong>'
|
||||
anim.addEventListener(
|
||||
'data_failed',
|
||||
() => e.innerHTML = '<strong>[Sticker cannot be rendered]</strong>'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -745,7 +744,7 @@
|
||||
@{/* Scripts */}
|
||||
<script>
|
||||
function scrollToMessage(event, id) {
|
||||
var element = document.getElementById('chatlog__message-container-' + id);
|
||||
const element = document.getElementById('chatlog__message-container-' + id);
|
||||
if (!element)
|
||||
return;
|
||||
|
||||
@@ -757,9 +756,10 @@
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
window.setTimeout(function() {
|
||||
element.classList.remove('chatlog__message-container--highlighted');
|
||||
}, 2000);
|
||||
window.setTimeout(
|
||||
() => element.classList.remove('chatlog__message-container--highlighted'),
|
||||
2000
|
||||
);
|
||||
}
|
||||
|
||||
function showSpoiler(event, element) {
|
||||
|
||||
@@ -6,13 +6,13 @@ namespace DiscordChatExporter.Core.Markdown;
|
||||
internal record EmojiNode(
|
||||
// Only present on custom emoji
|
||||
Snowflake? Id,
|
||||
// Name of custom emoji (e.g. LUL) or actual representation of standard emoji (e.g. 🙂)
|
||||
// Name of a custom emoji (e.g. LUL) or actual representation of a standard emoji (e.g. 🙂)
|
||||
string Name,
|
||||
bool IsAnimated) : MarkdownNode
|
||||
{
|
||||
public bool IsCustomEmoji => Id is not null;
|
||||
|
||||
// Name of custom emoji (e.g. LUL) or name of standard emoji (e.g. slight_smile)
|
||||
// Name of a custom emoji (e.g. LUL) or name of a standard emoji (e.g. slight_smile)
|
||||
public string Code => IsCustomEmoji
|
||||
? Name
|
||||
: EmojiIndex.TryGetCode(Name) ?? Name;
|
||||
|
||||
@@ -13,10 +13,11 @@ public static class Http
|
||||
{
|
||||
public static HttpClient Client { get; } = new();
|
||||
|
||||
private static bool IsRetryableStatusCode(HttpStatusCode statusCode) => statusCode is
|
||||
HttpStatusCode.TooManyRequests or
|
||||
HttpStatusCode.RequestTimeout or
|
||||
HttpStatusCode.InternalServerError;
|
||||
private static bool IsRetryableStatusCode(HttpStatusCode statusCode) =>
|
||||
statusCode is HttpStatusCode.TooManyRequests or HttpStatusCode.RequestTimeout ||
|
||||
// Treat all server-side errors as retryable.
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/908
|
||||
(int)statusCode >= 500;
|
||||
|
||||
private static bool IsRetryableException(Exception exception) =>
|
||||
exception.GetSelfAndChildren().Any(ex =>
|
||||
|
||||
@@ -17,4 +17,8 @@ public static class PathEx
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
public static bool IsDirectoryPath(string path) =>
|
||||
path.EndsWith(Path.DirectorySeparatorChar) ||
|
||||
path.EndsWith(Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
@@ -105,7 +105,11 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
|
||||
var guildChannelMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
||||
await foreach (var guild in discord.GetUserGuildsAsync())
|
||||
guildChannelMap[guild] = await discord.GetGuildChannelsAsync(guild.Id);
|
||||
{
|
||||
guildChannelMap[guild] = (await discord.GetGuildChannelsAsync(guild.Id))
|
||||
.Where(c => c.Kind != ChannelKind.GuildCategory)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
_discord = discord;
|
||||
GuildChannelMap = guildChannelMap;
|
||||
|
||||
@@ -135,22 +135,14 @@
|
||||
Grid.Row="2"
|
||||
Background="{DynamicResource MaterialDesignCardBackground}"
|
||||
IsEnabled="{Binding IsBusy, Converter={x:Static converters:InverseBoolConverter.Instance}}">
|
||||
<Grid.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="FontWeight" Value="Light" />
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
<!-- Placeholder / usage instructions -->
|
||||
<Grid Visibility="{Binding AvailableGuilds, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock
|
||||
Margin="32,16"
|
||||
FontSize="14"
|
||||
FontWeight="Light"
|
||||
LineHeight="20">
|
||||
<Run FontSize="18" Text="Please provide authentication token to continue" />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
|
||||
<!-- User token -->
|
||||
<InlineUIContainer>
|
||||
<materialDesign:PackIcon
|
||||
@@ -158,7 +150,7 @@
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Account" />
|
||||
</InlineUIContainer>
|
||||
<Run FontSize="16" Text="To get token for your personal account:" />
|
||||
<Run FontSize="16" FontWeight="SemiBold" Text="To get token for your personal account:" />
|
||||
<LineBreak />
|
||||
<Run Text="* Automating user accounts is technically against TOS —" />
|
||||
<Run FontWeight="SemiBold" Text="use at your own risk" /><Run Text="!" />
|
||||
@@ -169,31 +161,33 @@
|
||||
</Hyperlink>
|
||||
<Run Text="and login" />
|
||||
<LineBreak />
|
||||
<Run Text="2. Press" />
|
||||
<Run Text="2. Open any server or direct message channel" />
|
||||
<LineBreak />
|
||||
<Run Text="3. Press" />
|
||||
<Run FontWeight="SemiBold" Text="Ctrl+Shift+I" />
|
||||
<Run Text="to show developer tools" />
|
||||
<LineBreak />
|
||||
<Run Text="3. Navigate to the" />
|
||||
<Run Text="4. Navigate to the" />
|
||||
<Run FontWeight="SemiBold" Text="Network" />
|
||||
<Run Text="tab" />
|
||||
<LineBreak />
|
||||
<Run Text="4. Press" />
|
||||
<Run Text="5. Press" />
|
||||
<Run FontWeight="SemiBold" Text="Ctrl+R" />
|
||||
<Run Text="to reload" />
|
||||
<LineBreak />
|
||||
<Run Text="5. Search for a request containing" />
|
||||
<Run Text="6. Search for a request containing" />
|
||||
<Run FontWeight="SemiBold" Text="messages?limit=50" />
|
||||
<Run Text="or similar" />
|
||||
<LineBreak />
|
||||
<Run Text="6. Select the" />
|
||||
<Run Text="7. Select the" />
|
||||
<Run FontWeight="SemiBold" Text="Headers" />
|
||||
<Run Text=" tab on the right" />
|
||||
<LineBreak />
|
||||
<Run Text="7. Scroll down to the" />
|
||||
<Run Text="8. Scroll down to the" />
|
||||
<Run FontWeight="SemiBold" Text="Request Headers" />
|
||||
<Run Text="section" />
|
||||
<LineBreak />
|
||||
<Run Text="8. Copy the value of the" />
|
||||
<Run Text="9. Copy the value of the" />
|
||||
<Run FontWeight="SemiBold" Text="authorization" />
|
||||
<Run Text="header" />
|
||||
<LineBreak />
|
||||
@@ -206,7 +200,7 @@
|
||||
Foreground="{DynamicResource PrimaryHueMidBrush}"
|
||||
Kind="Robot" />
|
||||
</InlineUIContainer>
|
||||
<Run FontSize="16" Text="To get token for a bot account:" />
|
||||
<Run FontSize="16" FontWeight="SemiBold" Text="To get token for a bot account:" />
|
||||
<LineBreak />
|
||||
<Run Text="1. Open Discord" />
|
||||
<Hyperlink Command="{s:Action OpenDiscordDeveloperPortal}">
|
||||
@@ -224,10 +218,14 @@
|
||||
<Run Text="click" />
|
||||
<Run FontWeight="SemiBold" Text="Copy" />
|
||||
<LineBreak />
|
||||
<Run Text="* Your bot needs to have" />
|
||||
<Run FontWeight="SemiBold" Text="Message Content Intent" />
|
||||
<Run Text="enabled to read messages" />
|
||||
<LineBreak />
|
||||
<LineBreak />
|
||||
|
||||
<Run FontSize="16" Text="If you have questions or issues, please refer to the" />
|
||||
<Hyperlink Command="{s:Action ShowHelp}" FontSize="16">wiki</Hyperlink><Run FontSize="16" Text="." />
|
||||
<Run Text="If you have questions or issues, please refer to the" />
|
||||
<Hyperlink Command="{s:Action ShowHelp}">wiki</Hyperlink><Run Text="." />
|
||||
</TextBlock>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
[](https://tyrrrz.me/donate)
|
||||
[](https://twitter.com/tyrrrz/status/1495972128977571848)
|
||||
|
||||
> 🟢 **Project status**: active<sup>[[?]](https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md)</sup>
|
||||
> 🟡 **Project status**: maintenance mode<sup>[[?]](https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md)</sup>
|
||||
|
||||
**DiscordChatExporter** can be used to export message history from a [Discord](https://discord.com) channel to a file.
|
||||
It works with direct messages, group messages, and server channels, and supports Discord's dialect of markdown as well as all other rich media features.
|
||||
@@ -61,10 +61,10 @@ The following table lists all available download options:
|
||||
<td><b>CLI</b></td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>🟢 <b><a href="https://github.com/Tyrrrz/DiscordChatExporter/releases/latest">Stable release</a></b> (<code>DiscordChatExporter.CLI.zip</code>) ⚙️</li>
|
||||
<li>🟠 <a href="https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/main.yml">CI build</a> (<code>DiscordChatExporter.CLI.zip</code>) ⚙️</li>
|
||||
<li>🟢 <b><a href="https://github.com/Tyrrrz/DiscordChatExporter/releases/latest">Stable release</a></b> (<code>DiscordChatExporter.CLI.zip</code></li>
|
||||
<li>🟠 <a href="https://github.com/Tyrrrz/DiscordChatExporter/actions/workflows/main.yml">CI build</a> (<code>DiscordChatExporter.CLI.zip</code>)</li>
|
||||
<li>🐋 <a href="https://hub.docker.com/r/tyrrrz/discordchatexporter">Docker</a> (<code>tyrrrz/discordchatexporter</code>)</li>
|
||||
<li>📦 <a href="https://aur.archlinux.org/packages/discord-chat-exporter-cli">AUR</a> (<code>discord-chat-exporter-cli</code>) 🦄</li>
|
||||
<li>📦 <a href="https://aur.archlinux.org/packages/discord-chat-exporter-cli">AUR</a> (<code>discord-chat-exporter-cli</code>)</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
@@ -78,12 +78,16 @@ The following table lists all available download options:
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
- ⚙️ - Requires .NET runtime to be installed manually:
|
||||
- [.NET v6.0 Runtime for **Windows x64**](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-desktop-6.0.0-windows-x64-installer)
|
||||
- [.NET v6.0 Runtime for **Windows x86**](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-desktop-6.0.0-windows-x86-installer)
|
||||
- [.NET v6.0 Runtime for **macOS x64**](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-6.0.0-macos-x64-installer)
|
||||
- [.NET v6.0 Runtime for **Linux**](https://docs.microsoft.com/en-us/dotnet/core/install/linux) (find your distro)
|
||||
- 🦄 - Community-maintained resource
|
||||
> **Note**:
|
||||
> **DiscordChatExporter**'s AUR package is maintained by the community.
|
||||
|
||||
> **Warning**:
|
||||
> To run **DiscordChatExporter** on macOS or Linux, you will need to additionally install **.NET Runtime v6**:
|
||||
> - [.NET Runtime v6 for **macOS x64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-6.0.6-macos-x64-installer)
|
||||
> - [.NET Runtime v6 for **macOS Arm64**](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-6.0.6-macos-arm64-installer)
|
||||
> - [.NET Runtime v6 for **Linux**](https://docs.microsoft.com/en-us/dotnet/core/install/linux) (find the correct download for your distro)
|
||||
>
|
||||
> This is not required if you installed **DiscordChatExporter** using a package manager, or if you plan to run **DiscordChatExporter** via Docker.
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
Reference in New Issue
Block a user