mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-10 07:59:35 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2024d3658b | |||
| 9453cac389 | |||
| 504c3c53cd | |||
| 4e358e8e6f | |||
| 5e852235ce | |||
| 612c6d37a4 | |||
| 92cf886eab | |||
| f48af53d72 | |||
| 99d721c63b | |||
| 11414b5937 | |||
| 6c039ebfa6 | |||
| 9cffbbc5ce | |||
| 6f87960513 | |||
| e8f8c47dbd | |||
| 9aba36fce5 | |||
| f29e1d2d9f | |||
| b000a72f81 |
@@ -34,7 +34,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
publish:
|
package:
|
||||||
needs: test
|
needs: test
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
|
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
|
||||||
needs: publish
|
needs: package
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -151,4 +151,4 @@ jobs:
|
|||||||
payload: |
|
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>"
|
"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,13 @@
|
|||||||
|
### 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)
|
### 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.
|
- 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 +27,7 @@
|
|||||||
- Added file name template token that resolves to the current date (`%d`). (Thanks [@Lucas LaBuff](https://github.com/96-LB))
|
- 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 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.
|
- 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)
|
### v2.34.1 (01-Jun-2022)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Version>2.35</Version>
|
<Version>2.35.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>
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.1" PrivateAssets="all" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.1" PrivateAssets="all" />
|
||||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
<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="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="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>
|
||||||
|
|||||||
@@ -14,18 +14,27 @@ using DiscordChatExporter.Core.Exceptions;
|
|||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
using DiscordChatExporter.Core.Exporting.Filtering;
|
using DiscordChatExporter.Core.Exporting.Filtering;
|
||||||
using DiscordChatExporter.Core.Exporting.Partitioning;
|
using DiscordChatExporter.Core.Exporting.Partitioning;
|
||||||
|
using DiscordChatExporter.Core.Utils;
|
||||||
using Gress;
|
using Gress;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands.Base;
|
namespace DiscordChatExporter.Cli.Commands.Base;
|
||||||
|
|
||||||
public abstract class ExportCommandBase : TokenCommandBase
|
public abstract class ExportCommandBase : TokenCommandBase
|
||||||
{
|
{
|
||||||
|
private readonly string _outputPath = Directory.GetCurrentDirectory();
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"output",
|
"output",
|
||||||
'o',
|
'o',
|
||||||
Description = "Output file or directory path."
|
Description = "Output file or directory path."
|
||||||
)]
|
)]
|
||||||
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(
|
[CommandOption(
|
||||||
"format",
|
"format",
|
||||||
@@ -90,9 +99,20 @@ public abstract class ExportCommandBase : TokenCommandBase
|
|||||||
{
|
{
|
||||||
var cancellationToken = console.RegisterCancellationHandler();
|
var cancellationToken = console.RegisterCancellationHandler();
|
||||||
|
|
||||||
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/425
|
||||||
if (ShouldReuseMedia && !ShouldDownloadMedia)
|
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."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/799
|
||||||
|
if (channels.Count > 1 && !PathEx.IsDirectoryPath(OutputPath) && !OutputPath.Contains('%'))
|
||||||
|
{
|
||||||
|
throw new CommandException(
|
||||||
|
"Attempted to export multiple channels, but the output path is neither a directory nor a template."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var errors = new ConcurrentDictionary<Channel, string>();
|
var errors = new ConcurrentDictionary<Channel, string>();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands.Base;
|
using DiscordChatExporter.Cli.Commands.Base;
|
||||||
@@ -17,7 +18,10 @@ public class ExportDirectMessagesCommand : ExportCommandBase
|
|||||||
var cancellationToken = console.RegisterCancellationHandler();
|
var cancellationToken = console.RegisterCancellationHandler();
|
||||||
|
|
||||||
await console.Output.WriteLineAsync("Fetching channels...");
|
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);
|
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.Attributes;
|
||||||
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.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands;
|
namespace DiscordChatExporter.Cli.Commands;
|
||||||
@@ -25,7 +27,10 @@ public class ExportGuildCommand : ExportCommandBase
|
|||||||
var cancellationToken = console.RegisterCancellationHandler();
|
var cancellationToken = console.RegisterCancellationHandler();
|
||||||
|
|
||||||
await console.Output.WriteLineAsync("Fetching channels...");
|
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);
|
await base.ExecuteAsync(console, channels);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using CliFx.Attributes;
|
|||||||
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.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Commands;
|
namespace DiscordChatExporter.Cli.Commands;
|
||||||
@@ -25,6 +26,7 @@ public class GetChannelsCommand : TokenCommandBase
|
|||||||
var cancellationToken = console.RegisterCancellationHandler();
|
var cancellationToken = console.RegisterCancellationHandler();
|
||||||
|
|
||||||
var channels = (await Discord.GetGuildChannelsAsync(GuildId, cancellationToken))
|
var channels = (await Discord.GetGuildChannelsAsync(GuildId, cancellationToken))
|
||||||
|
.Where(c => c.Kind != ChannelKind.GuildCategory)
|
||||||
.OrderBy(c => c.Category.Position)
|
.OrderBy(c => c.Category.Position)
|
||||||
.ThenBy(c => c.Name)
|
.ThenBy(c => c.Name)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public class GetDirectMessageChannelsCommand : TokenCommandBase
|
|||||||
var cancellationToken = console.RegisterCancellationHandler();
|
var cancellationToken = console.RegisterCancellationHandler();
|
||||||
|
|
||||||
var channels = (await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken))
|
var channels = (await Discord.GetGuildChannelsAsync(Guild.DirectMessages.Id, cancellationToken))
|
||||||
|
.Where(c => c.Kind != ChannelKind.GuildCategory)
|
||||||
.OrderByDescending(c => c.LastMessageId)
|
.OrderByDescending(c => c.LastMessageId)
|
||||||
.ThenBy(c => c.Name)
|
.ThenBy(c => c.Name)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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="Spectre.Console" Version="0.44.0" />
|
||||||
<PackageReference Include="Gress" Version="2.0.1" />
|
<PackageReference Include="Gress" Version="2.0.1" />
|
||||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.3.1" PrivateAssets="all" />
|
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.3.1" PrivateAssets="all" />
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ namespace DiscordChatExporter.Core.Discord.Data;
|
|||||||
public partial record Emoji(
|
public partial record Emoji(
|
||||||
// Only present on custom emoji
|
// Only present on custom emoji
|
||||||
Snowflake? Id,
|
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,
|
string Name,
|
||||||
bool IsAnimated,
|
bool IsAnimated,
|
||||||
string ImageUrl)
|
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
|
public string Code => Id is not null
|
||||||
? Name
|
? Name
|
||||||
: EmojiIndex.TryGetCode(Name) ?? Name;
|
: EmojiIndex.TryGetCode(Name) ?? Name;
|
||||||
@@ -24,17 +24,19 @@ public partial record Emoji(
|
|||||||
|
|
||||||
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(
|
return string.Join(
|
||||||
"-",
|
"-",
|
||||||
// Variant selector rune is skipped in Twemoji names, except when the emoji also contains a zero-width joiner.
|
filteredRunes.Select(r => r.Value.ToString("x"))
|
||||||
// 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"))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ public partial record Emoji
|
|||||||
: $"https://cdn.discordapp.com/emojis/{id}.png";
|
: $"https://cdn.discordapp.com/emojis/{id}.png";
|
||||||
|
|
||||||
private static string GetImageUrl(string name) =>
|
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)
|
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.Discord.Data.Common;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using JsonExtensions.Reading;
|
using JsonExtensions.Reading;
|
||||||
@@ -17,8 +18,14 @@ public record Guild(Snowflake Id, string Name, string IconUrl) : IHasId
|
|||||||
private static string GetDefaultIconUrl() =>
|
private static string GetDefaultIconUrl() =>
|
||||||
"https://cdn.discordapp.com/embed/avatars/0.png";
|
"https://cdn.discordapp.com/embed/avatars/0.png";
|
||||||
|
|
||||||
private static string GetIconUrl(Snowflake id, string iconHash) =>
|
private static string GetIconUrl(Snowflake id, string iconHash)
|
||||||
$"https://cdn.discordapp.com/icons/{id}/{iconHash}.png";
|
{
|
||||||
|
var extension = iconHash.StartsWith("a_", StringComparison.Ordinal)
|
||||||
|
? "gif"
|
||||||
|
: "png";
|
||||||
|
|
||||||
|
return $"https://cdn.discordapp.com/icons/{id}/{iconHash}.{extension}";
|
||||||
|
}
|
||||||
|
|
||||||
public static Guild Parse(JsonElement json)
|
public static Guild Parse(JsonElement json)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<PackageReference Include="MiniRazor.CodeGen" Version="2.2.1" />
|
<PackageReference Include="MiniRazor.CodeGen" Version="2.2.1" />
|
||||||
<PackageReference Include="Polly" Version="7.2.3" />
|
<PackageReference Include="Polly" Version="7.2.3" />
|
||||||
<PackageReference Include="Superpower" Version="3.0.0" />
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -10,11 +10,19 @@ internal class ContainsMessageFilter : MessageFilter
|
|||||||
|
|
||||||
public ContainsMessageFilter(string text) => _text = text;
|
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) =>
|
private bool IsMatch(string? content) =>
|
||||||
!string.IsNullOrWhiteSpace(content) &&
|
!string.IsNullOrWhiteSpace(content) &&
|
||||||
Regex.IsMatch(
|
Regex.IsMatch(
|
||||||
content,
|
content,
|
||||||
"\\b" + Regex.Escape(_text) + "\\b",
|
@"(?:\b|\s|^)" +
|
||||||
|
Regex.Escape(_text) +
|
||||||
|
@"(?:\b|\s|$)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -15,95 +15,107 @@ internal static class FilterGrammar
|
|||||||
from close in Character.EqualTo(open)
|
from close in Character.EqualTo(open)
|
||||||
select value;
|
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 =
|
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 =
|
private static readonly TextParser<string> String =
|
||||||
Parse.OneOf(QuotedString, UnquotedString).Named("text string");
|
Parse.OneOf(QuotedString, UnquotedString).Named("text string");
|
||||||
|
|
||||||
private static readonly TextParser<MessageFilter> ContainsFilter =
|
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
|
private static readonly TextParser<MessageFilter> FromFilter =
|
||||||
.EqualToIgnoreCase("from:")
|
Span
|
||||||
.IgnoreThen(String)
|
.EqualToIgnoreCase("from:")
|
||||||
.Select(v => (MessageFilter) new FromMessageFilter(v))
|
.Try()
|
||||||
.Named("from:<value>");
|
.IgnoreThen(String)
|
||||||
|
.Select(v => (MessageFilter)new FromMessageFilter(v))
|
||||||
|
.Named("from:<value>");
|
||||||
|
|
||||||
private static readonly TextParser<MessageFilter> MentionsFilter = Span
|
private static readonly TextParser<MessageFilter> MentionsFilter =
|
||||||
.EqualToIgnoreCase("mentions:")
|
Span
|
||||||
.IgnoreThen(String)
|
.EqualToIgnoreCase("mentions:")
|
||||||
.Select(v => (MessageFilter) new MentionsMessageFilter(v))
|
.Try()
|
||||||
.Named("mentions:<value>");
|
.IgnoreThen(String)
|
||||||
|
.Select(v => (MessageFilter)new MentionsMessageFilter(v))
|
||||||
|
.Named("mentions:<value>");
|
||||||
|
|
||||||
private static readonly TextParser<MessageFilter> ReactionFilter = Span
|
private static readonly TextParser<MessageFilter> ReactionFilter =
|
||||||
.EqualToIgnoreCase("reaction:")
|
Span
|
||||||
.IgnoreThen(String)
|
.EqualToIgnoreCase("reaction:")
|
||||||
.Select(v => (MessageFilter) new ReactionMessageFilter(v))
|
.Try()
|
||||||
.Named("reaction:<value>");
|
.IgnoreThen(String)
|
||||||
|
.Select(v => (MessageFilter)new ReactionMessageFilter(v))
|
||||||
|
.Named("reaction:<value>");
|
||||||
|
|
||||||
private static readonly TextParser<MessageFilter> HasFilter = Span
|
private static readonly TextParser<MessageFilter> HasFilter =
|
||||||
.EqualToIgnoreCase("has:")
|
Span
|
||||||
.IgnoreThen(Parse.OneOf(
|
.EqualToIgnoreCase("has:")
|
||||||
Span.EqualToIgnoreCase("link").IgnoreThen(Parse.Return(MessageContentMatchKind.Link)),
|
.Try()
|
||||||
Span.EqualToIgnoreCase("embed").IgnoreThen(Parse.Return(MessageContentMatchKind.Embed)),
|
.IgnoreThen(Parse.OneOf(
|
||||||
Span.EqualToIgnoreCase("file").IgnoreThen(Parse.Return(MessageContentMatchKind.File)),
|
Span.EqualToIgnoreCase("link").IgnoreThen(Parse.Return(MessageContentMatchKind.Link)),
|
||||||
Span.EqualToIgnoreCase("video").IgnoreThen(Parse.Return(MessageContentMatchKind.Video)),
|
Span.EqualToIgnoreCase("embed").IgnoreThen(Parse.Return(MessageContentMatchKind.Embed)),
|
||||||
Span.EqualToIgnoreCase("image").IgnoreThen(Parse.Return(MessageContentMatchKind.Image)),
|
Span.EqualToIgnoreCase("file").IgnoreThen(Parse.Return(MessageContentMatchKind.File)),
|
||||||
Span.EqualToIgnoreCase("sound").IgnoreThen(Parse.Return(MessageContentMatchKind.Sound)),
|
Span.EqualToIgnoreCase("video").IgnoreThen(Parse.Return(MessageContentMatchKind.Video)),
|
||||||
Span.EqualToIgnoreCase("pin").IgnoreThen(Parse.Return(MessageContentMatchKind.Pin))
|
Span.EqualToIgnoreCase("image").IgnoreThen(Parse.Return(MessageContentMatchKind.Image)),
|
||||||
))
|
Span.EqualToIgnoreCase("sound").IgnoreThen(Parse.Return(MessageContentMatchKind.Sound)),
|
||||||
.Select(k => (MessageFilter) new HasMessageFilter(k))
|
Span.EqualToIgnoreCase("pin").IgnoreThen(Parse.Return(MessageContentMatchKind.Pin))
|
||||||
.Named("has:<value>");
|
))
|
||||||
|
.Select(k => (MessageFilter)new HasMessageFilter(k))
|
||||||
|
.Named("has:<value>");
|
||||||
|
|
||||||
private static readonly TextParser<MessageFilter> NegatedFilter = Character
|
// Make sure that property-based filters like 'has:link' don't prevent text like 'hello' from being parsed.
|
||||||
.EqualTo('-')
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/909#issuecomment-1227575455
|
||||||
.IgnoreThen(Parse.Ref(() => StandaloneFilter!))
|
private static readonly TextParser<MessageFilter> PrimitiveFilter =
|
||||||
.Select(f => (MessageFilter) new NegatedMessageFilter(f));
|
Parse.OneOf(
|
||||||
|
FromFilter,
|
||||||
|
MentionsFilter,
|
||||||
|
ReactionFilter,
|
||||||
|
HasFilter,
|
||||||
|
ContainsFilter
|
||||||
|
);
|
||||||
|
|
||||||
private static readonly TextParser<MessageFilter> GroupedFilter =
|
private static readonly TextParser<MessageFilter> GroupedFilter =
|
||||||
from open in Character.EqualTo('(')
|
from open in Character.EqualTo('(')
|
||||||
from content in Parse.Ref(() => BinaryExpressionFilter!).Token()
|
from content in Parse.Ref(() => ChainedFilter!).Token()
|
||||||
from close in Character.EqualTo(')')
|
from close in Character.EqualTo(')')
|
||||||
select content;
|
select content;
|
||||||
|
|
||||||
private static readonly TextParser<MessageFilter> StandaloneFilter = Parse.OneOf(
|
private static readonly TextParser<MessageFilter> NegatedFilter =
|
||||||
GroupedFilter,
|
Character
|
||||||
FromFilter,
|
// Dash is annoying to use from CLI due to conflicts with options, so we provide tilde as an alias
|
||||||
MentionsFilter,
|
.In('-', '~')
|
||||||
ReactionFilter,
|
.IgnoreThen(Parse.OneOf(GroupedFilter, PrimitiveFilter))
|
||||||
HasFilter,
|
.Select(f => (MessageFilter)new NegatedMessageFilter(f));
|
||||||
ContainsFilter
|
|
||||||
);
|
|
||||||
|
|
||||||
private static readonly TextParser<MessageFilter> UnaryExpressionFilter = Parse.OneOf(
|
private static readonly TextParser<MessageFilter> ChainedFilter =
|
||||||
NegatedFilter,
|
Parse.Chain(
|
||||||
StandaloneFilter
|
// Operator
|
||||||
);
|
Parse.OneOf(
|
||||||
|
// Explicit operator
|
||||||
private static readonly TextParser<MessageFilter> BinaryExpressionFilter = Parse.Chain(
|
Character.In('|', '&').Token().Try(),
|
||||||
Parse.OneOf(
|
// Implicit operator (resolves to 'and')
|
||||||
// Explicit operator
|
Character.EqualTo(' ').AtLeastOnce().IgnoreThen(Parse.Return(' '))
|
||||||
Character.In('|', '&').Token().Try(),
|
),
|
||||||
// Implicit operator (resolves to 'and')
|
// Operand
|
||||||
Character.WhiteSpace.AtLeastOnce().IgnoreThen(Parse.Return(' '))
|
Parse.OneOf(
|
||||||
),
|
NegatedFilter,
|
||||||
UnaryExpressionFilter,
|
GroupedFilter,
|
||||||
(op, left, right) => op switch
|
PrimitiveFilter
|
||||||
{
|
),
|
||||||
'|' => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.Or),
|
// Reducer
|
||||||
_ => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.And)
|
(op, left, right) => op switch
|
||||||
}
|
{
|
||||||
);
|
'|' => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.Or),
|
||||||
|
_ => new BinaryExpressionMessageFilter(left, right, BinaryExpressionKind.And)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly TextParser<MessageFilter> Filter =
|
public static readonly TextParser<MessageFilter> Filter =
|
||||||
BinaryExpressionFilter.Token().AtEnd();
|
ChainedFilter.Token().AtEnd();
|
||||||
}
|
}
|
||||||
@@ -725,18 +725,17 @@
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.querySelectorAll('.chatlog__sticker--media[data-source]').forEach(e => {
|
document.querySelectorAll('.chatlog__sticker--media[data-source]').forEach(e => {
|
||||||
const imageDataUrl = e.getAttribute('data-source');
|
|
||||||
|
|
||||||
const anim = lottie.loadAnimation({
|
const anim = lottie.loadAnimation({
|
||||||
container: e,
|
container: e,
|
||||||
renderer: 'svg',
|
renderer: 'svg',
|
||||||
loop: true,
|
loop: true,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
path: imageDataUrl
|
path: e.getAttribute('data-source')
|
||||||
});
|
});
|
||||||
|
|
||||||
anim.addEventListener('data_failed', () =>
|
anim.addEventListener(
|
||||||
e.innerHTML = '<strong>[Sticker cannot be rendered]</strong>'
|
'data_failed',
|
||||||
|
() => e.innerHTML = '<strong>[Sticker cannot be rendered]</strong>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -745,7 +744,7 @@
|
|||||||
@{/* Scripts */}
|
@{/* Scripts */}
|
||||||
<script>
|
<script>
|
||||||
function scrollToMessage(event, id) {
|
function scrollToMessage(event, id) {
|
||||||
var element = document.getElementById('chatlog__message-container-' + id);
|
const element = document.getElementById('chatlog__message-container-' + id);
|
||||||
if (!element)
|
if (!element)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -757,9 +756,10 @@
|
|||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
});
|
});
|
||||||
|
|
||||||
window.setTimeout(function() {
|
window.setTimeout(
|
||||||
element.classList.remove('chatlog__message-container--highlighted');
|
() => element.classList.remove('chatlog__message-container--highlighted'),
|
||||||
}, 2000);
|
2000
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSpoiler(event, element) {
|
function showSpoiler(event, element) {
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ namespace DiscordChatExporter.Core.Markdown;
|
|||||||
internal record EmojiNode(
|
internal record EmojiNode(
|
||||||
// Only present on custom emoji
|
// Only present on custom emoji
|
||||||
Snowflake? Id,
|
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,
|
string Name,
|
||||||
bool IsAnimated) : MarkdownNode
|
bool IsAnimated) : MarkdownNode
|
||||||
{
|
{
|
||||||
public bool IsCustomEmoji => Id is not null;
|
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
|
public string Code => IsCustomEmoji
|
||||||
? Name
|
? Name
|
||||||
: EmojiIndex.TryGetCode(Name) ?? Name;
|
: EmojiIndex.TryGetCode(Name) ?? Name;
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ public static class Http
|
|||||||
{
|
{
|
||||||
public static HttpClient Client { get; } = new();
|
public static HttpClient Client { get; } = new();
|
||||||
|
|
||||||
private static bool IsRetryableStatusCode(HttpStatusCode statusCode) => statusCode is
|
private static bool IsRetryableStatusCode(HttpStatusCode statusCode) =>
|
||||||
HttpStatusCode.TooManyRequests or
|
statusCode is HttpStatusCode.TooManyRequests or HttpStatusCode.RequestTimeout ||
|
||||||
HttpStatusCode.RequestTimeout or
|
// Treat all server-side errors as retryable.
|
||||||
HttpStatusCode.InternalServerError;
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/908
|
||||||
|
(int)statusCode >= 500;
|
||||||
|
|
||||||
private static bool IsRetryableException(Exception exception) =>
|
private static bool IsRetryableException(Exception exception) =>
|
||||||
exception.GetSelfAndChildren().Any(ex =>
|
exception.GetSelfAndChildren().Any(ex =>
|
||||||
|
|||||||
@@ -17,4 +17,8 @@ public static class PathEx
|
|||||||
|
|
||||||
return buffer.ToString();
|
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>>();
|
var guildChannelMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
||||||
await foreach (var guild in discord.GetUserGuildsAsync())
|
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;
|
_discord = discord;
|
||||||
GuildChannelMap = guildChannelMap;
|
GuildChannelMap = guildChannelMap;
|
||||||
|
|||||||
@@ -61,10 +61,10 @@ The following table lists all available download options:
|
|||||||
<td><b>CLI</b></td>
|
<td><b>CLI</b></td>
|
||||||
<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/main.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>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -78,12 +78,16 @@ The following table lists all available download options:
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
- ⚙️ - Requires .NET runtime to be installed manually:
|
> **Note**:
|
||||||
- [.NET v6.0 Runtime for **Windows x64**](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-desktop-6.0.0-windows-x64-installer)
|
> **DiscordChatExporter**'s AUR package is maintained by the community.
|
||||||
- [.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)
|
> **Warning**:
|
||||||
- [.NET v6.0 Runtime for **Linux**](https://docs.microsoft.com/en-us/dotnet/core/install/linux) (find your distro)
|
> To run **DiscordChatExporter** on macOS or Linux, you will need to additionally install **.NET Runtime v6**:
|
||||||
- 🦄 - Community-maintained resource
|
> - [.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
|
## Features
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user