Compare commits

...

11 Commits

16 changed files with 182 additions and 37 deletions
+11
View File
@@ -61,6 +61,17 @@ For more information, please refer to the [Dockerfile](https://github.com/Tyrrrz
To get your Token and Channel IDs, please refer to [this page](Token-and-IDs.md). To get your Token and Channel IDs, please refer to [this page](Token-and-IDs.md).
## Unix permissions issues
This image was designed with a user running as uid:gid of 1000:1000.
If your current user has different IDs, and you want to generate files directly editable for your user, you might want to run the container like this:
```console
mkdir data # or chown -R $(id -u):$(id -g) data
docker run -it --rm -v $PWD/data:/out --user $(id -u):$(id -g) tyrrrz/discordchatexporter:stable export -t TOKEN -g CHANNELID
```
## Environment variables ## Environment variables
DiscordChatExpoter CLI accepts the `DISCORD_TOKEN` environment variable as a fallback for the `--token` option. You can set this variable either with the `--env` Docker option or with a combination of the `--env-file` Docker option and a `.env` file. DiscordChatExpoter CLI accepts the `DISCORD_TOKEN` environment variable as a fallback for the `--token` option. You can set this variable either with the `--env` Docker option or with a combination of the `--env-file` Docker option and a `.env` file.
+13 -7
View File
@@ -8,19 +8,25 @@ Please follow the [instructions provided here](Dotnet.md).
1. Download [DiscordChatExporter.CLI.zip](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest) and extract it to a folder. 1. Download [DiscordChatExporter.CLI.zip](https://github.com/Tyrrrz/DiscordChatExporter/releases/latest) and extract it to a folder.
2. Open Terminal. 2. Open Terminal.
3. `cd` into the extracted folder. You can do this in Terminal by typing `cd`, then press the SPACE key, drag and drop the extracted folder into the Terminal window, and press the ENTER key. 3. Change the working directory into the extracted folder. You can do this in Terminal by typing `cd`, then press the SPACE key, drag and drop the extracted folder into the Terminal window, and press the ENTER key.
4. Replace `TOKEN` and `CHANNEL`, then execute this command to export: 4. Replace `TOKEN` and `CHANNEL`, then execute this command to export:
```console
./DiscordChatExporter.Cli.sh export -t TOKEN -c CHANNEL
```
If the above command throws a "Permission denied" error, use `chmod` to fix the permissions:
```console
chmod +x DiscordChatExporter.Cli.sh
```
Alternatively, if the script doesn't work, you can run the following command to run the application directly:
```console ```console
dotnet DiscordChatExporter.Cli.dll export -t TOKEN -c CHANNEL dotnet DiscordChatExporter.Cli.dll export -t TOKEN -c CHANNEL
``` ```
If the above command throws a "Permission denied" error, execute this command to fix the permissions:
```console
chmod 644 *.dll DiscordChatExporter.*
```
> [How to get Token and Channel IDs](Token-and-IDs.md). > [How to get Token and Channel IDs](Token-and-IDs.md).
There's much more you can do with DCE.CLI! Read the [CLI explained](Using-the-CLI.md) page to get started. There's much more you can do with DCE.CLI! Read the [CLI explained](Using-the-CLI.md) page to get started.
+12
View File
@@ -13,6 +13,18 @@ Please follow the [instructions provided here](Dotnet.md).
3. In the Terminal window, type `cd` , press the SPACE key, then drag and drop the extracted folder into the window, then press the RETURN key. 3. In the Terminal window, type `cd` , press the SPACE key, then drag and drop the extracted folder into the window, then press the RETURN key.
4. Execute the following command to export, replacing `TOKEN` and `CHANNEL` with your own values: 4. Execute the following command to export, replacing `TOKEN` and `CHANNEL` with your own values:
```console
./DiscordChatExporter.Cli.sh export -t TOKEN -c CHANNEL
```
If the above command throws a "Permission denied" error, use `chmod` to fix the permissions:
```console
chmod +x DiscordChatExporter.Cli.sh
```
Alternatively, if the script doesn't work, you can run the following command to run the application directly:
```console ```console
dotnet DiscordChatExporter.Cli.dll export -t TOKEN -c CHANNEL dotnet DiscordChatExporter.Cli.dll export -t TOKEN -c CHANNEL
``` ```
+13 -1
View File
@@ -28,7 +28,19 @@ You can also drag and drop the folder on **every platform**.
Now we're ready to run the commands. The examples on this page follow the Windows file path format, change the file Now we're ready to run the commands. The examples on this page follow the Windows file path format, change the file
paths according to your system. paths according to your system.
Type the following in Command Prompt (Terminal), then press ENTER to run it. This will list DCE's options. On **Windows**, type the following command in your terminal of choice, then press ENTER to run it. This will list all available subcommands and options.
```console
DiscordChatExporter.Cli.exe
```
On **macOS** and **Linux**, run the following instead:
```console
./DiscordChatExporter.Cli.sh
```
If either of the above approaches don't work for you, you can also run **DiscordChatExporter** directly like this, regardless of the platform:
```console ```console
dotnet DiscordChatExporter.Cli.dll dotnet DiscordChatExporter.Cli.dll
+5 -5
View File
@@ -24,10 +24,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # 4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install Docker Buildx - name: Install Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # 3.0.0 uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Build image - name: Build image
run: > run: >
@@ -38,7 +38,7 @@ jobs:
--output type=tar,dest=DiscordChatExporter.Cli.Docker.tar --output type=tar,dest=DiscordChatExporter.Cli.Docker.tar
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # 3.1.3 uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
with: with:
name: DiscordChatExporter.Cli.Docker name: DiscordChatExporter.Cli.Docker
path: DiscordChatExporter.Cli.Docker.tar path: DiscordChatExporter.Cli.Docker.tar
@@ -56,10 +56,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # 4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install Docker Buildx - name: Install Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # 3.0.0 uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Login to DockerHub - name: Login to DockerHub
run: > run: >
+15 -10
View File
@@ -26,10 +26,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # 4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install .NET - name: Install .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # 3.2.0 uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.x
@@ -51,10 +51,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # 4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install .NET - name: Install .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # 3.2.0 uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.x
@@ -72,7 +72,7 @@ jobs:
DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # 3.1.4 uses: codecov/codecov-action@f30e4959ba63075080d4f7f90cacc18d9f3fafd7 # v4.0.0
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@@ -92,10 +92,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # 4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install .NET - name: Install .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # 3.2.0 uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.x
@@ -108,7 +108,7 @@ jobs:
--configuration Release --configuration Release
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # 3.1.3 uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
with: with:
name: ${{ matrix.app }} name: ${{ matrix.app }}
path: ${{ matrix.app }}/bin/publish/ path: ${{ matrix.app }}/bin/publish/
@@ -163,15 +163,20 @@ jobs:
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # 3.0.2 uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
with: with:
name: ${{ matrix.app }} name: ${{ matrix.app }}
path: ${{ matrix.app }}/ path: ${{ matrix.app }}/
- name: Configure permissions
# Remove this when the GUI also becomes cross-platform
if: ${{ matrix.app == 'DiscordChatExporter.Cli' }}
run: chmod +x ${{ matrix.app }}/${{ matrix.app }}.sh
- name: Create package - name: Create package
# Change into the artifacts directory to avoid including the directory itself in the zip archive # Change into the artifacts directory to avoid including the directory itself in the zip archive
working-directory: ${{ matrix.app }}/ working-directory: ${{ matrix.app }}/
run: zip -r ../${{ matrix.asset }}.zip . run: zip -rX ../${{ matrix.asset }}.zip .
- name: Upload release asset - name: Upload release asset
env: env:
@@ -150,6 +150,26 @@ public class HtmlEmbedSpecs
iframeUrl.Should().StartWith("https://open.spotify.com/embed/track/1LHZMWefF9502NPfArRfvP"); iframeUrl.Should().StartWith("https://open.spotify.com/embed/track/1LHZMWefF9502NPfArRfvP");
} }
[Fact(Skip = "Twitch does not allow embeds from inside local HTML files")]
public async Task I_can_export_a_channel_that_contains_a_message_with_a_Twitch_clip_embed()
{
// https://github.com/Tyrrrz/DiscordChatExporter/issues/1196
// Act
var message = await ExportWrapper.GetMessageAsHtmlAsync(
ChannelIds.EmbedTestCases,
Snowflake.Parse("1207002986128216074")
);
// Assert
var iframeUrl = message.QuerySelector("iframe")?.GetAttribute("src");
iframeUrl
.Should()
.StartWith(
"https://clips.twitch.tv/embed?clip=SpicyMildCiderThisIsSparta--PQhbllrvej_Ee7v"
);
}
[Fact] [Fact]
public async Task I_can_export_a_channel_that_contains_a_message_with_a_YouTube_video_embed() public async Task I_can_export_a_channel_that_contains_a_message_with_a_YouTube_video_embed()
{ {
+6 -5
View File
@@ -40,20 +40,21 @@ LABEL org.opencontainers.image.licenses="MIT"
# Alpine image doesn't come with the ICU libraries pre-installed, so we need to install them manually. # Alpine image doesn't come with the ICU libraries pre-installed, so we need to install them manually.
# We need the full ICU data because we allow the user to specify any locale for formatting purposes. # We need the full ICU data because we allow the user to specify any locale for formatting purposes.
RUN apk add --no-cache icu-libs RUN apk add --no-cache icu-libs icu-data-full
RUN apk add --no-cache icu-data-full
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
ENV LC_ALL=en_US.UTF-8 ENV LC_ALL=en_US.UTF-8
ENV LANG=en_US.UTF-8 ENV LANG=en_US.UTF-8
# Use a non-root user to ensure that the files shared with the host are accessible by the host user # Use a non-root user to ensure that the files shared with the host are accessible by the host user
# https://github.com/Tyrrrz/DiscordChatExporter/issues/851 # https://github.com/Tyrrrz/DiscordChatExporter/issues/851
RUN adduser --disabled-password --no-create-home dce # https://github.com/Tyrrrz/DiscordChatExporter/issues/1174
USER dce RUN apk add --no-cache su-exec
RUN addgroup -S -g 1000 dce && adduser -S -H -G dce -u 1000 dce
# This directory is exposed to the user for mounting purposes, so it's important that it always # This directory is exposed to the user for mounting purposes, so it's important that it always
# stays the same for backwards compatibility. # stays the same for backwards compatibility.
WORKDIR /out WORKDIR /out
COPY --from=build /tmp/app/DiscordChatExporter.Cli/bin/publish /opt/app COPY --from=build /tmp/app/DiscordChatExporter.Cli/bin/publish /opt/app
ENTRYPOINT ["/opt/app/DiscordChatExporter.Cli"] COPY docker-entrypoint.sh /opt/app
ENTRYPOINT ["/opt/app/docker-entrypoint.sh"]
@@ -5,6 +5,10 @@
<ApplicationIcon>..\favicon.ico</ApplicationIcon> <ApplicationIcon>..\favicon.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Include="DiscordChatExporter.Cli.sh" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CliFx" Version="2.3.5" /> <PackageReference Include="CliFx" Version="2.3.5" />
<PackageReference Include="CSharpier.MsBuild" Version="0.26.7" PrivateAssets="all" /> <PackageReference Include="CSharpier.MsBuild" Version="0.26.7" PrivateAssets="all" />
@@ -0,0 +1 @@
dotnet "$(dirname "$0")/DiscordChatExporter.Cli.dll" "$@"
@@ -31,6 +31,9 @@ public partial record Embed(
public SpotifyTrackEmbedProjection? TryGetSpotifyTrack() => public SpotifyTrackEmbedProjection? TryGetSpotifyTrack() =>
SpotifyTrackEmbedProjection.TryResolve(this); SpotifyTrackEmbedProjection.TryResolve(this);
public TwitchClipEmbedProjection? TryGetTwitchClip() =>
TwitchClipEmbedProjection.TryResolve(this);
public YouTubeVideoEmbedProjection? TryGetYouTubeVideo() => public YouTubeVideoEmbedProjection? TryGetYouTubeVideo() =>
YouTubeVideoEmbedProjection.TryResolve(this); YouTubeVideoEmbedProjection.TryResolve(this);
} }
@@ -0,0 +1,53 @@
using System.Text.RegularExpressions;
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
public partial record TwitchClipEmbedProjection(string ClipId)
{
public string Url => $"https://clips.twitch.tv/embed?clip={ClipId}&parent=localhost";
}
public partial record TwitchClipEmbedProjection
{
private static string? TryParseClipId(string embedUrl)
{
// https://clips.twitch.tv/SpookyTenuousPidgeonPanicVis
{
var clipId = Regex
.Match(embedUrl, @"clips\.twitch\.tv/(.*?)(?:\?|&|/|$)")
.Groups[1]
.Value;
if (!string.IsNullOrWhiteSpace(clipId))
return clipId;
}
// https://twitch.tv/clip/SpookyTenuousPidgeonPanicVis
{
var clipId = Regex
.Match(embedUrl, @"twitch\.tv/clip/(.*?)(?:\?|&|/|$)")
.Groups[1]
.Value;
if (!string.IsNullOrWhiteSpace(clipId))
return clipId;
}
return null;
}
public static TwitchClipEmbedProjection? TryResolve(Embed embed)
{
if (embed.Kind != EmbedKind.Video)
return null;
if (string.IsNullOrWhiteSpace(embed.Url))
return null;
var clipId = TryParseClipId(embed.Url);
if (string.IsNullOrWhiteSpace(clipId))
return null;
return new TwitchClipEmbedProjection(clipId);
}
}
@@ -20,8 +20,8 @@ public class ChannelExporter(DiscordClient discord)
if (request.Channel.Kind == ChannelKind.GuildForum) if (request.Channel.Kind == ChannelKind.GuildForum)
{ {
throw new DiscordChatExporterException( throw new DiscordChatExporterException(
$"Channel '{request.Channel.Name}' (#{request.Channel.Id}) " $"Channel '{request.Channel.Name}' "
+ $"of guild '{request.Guild.Name}' (#{request.Guild.Id}) " + $"of guild '{request.Guild.Name}' "
+ $"is a forum and cannot be exported directly. " + $"is a forum and cannot be exported directly. "
+ "You need to pull its threads and export them individually." + "You need to pull its threads and export them individually."
); );
@@ -31,8 +31,8 @@ public class ChannelExporter(DiscordClient discord)
if (request.Channel.IsEmpty) if (request.Channel.IsEmpty)
{ {
throw new DiscordChatExporterException( throw new DiscordChatExporterException(
$"Channel '{request.Channel.Name}' (#{request.Channel.Id}) " $"Channel '{request.Channel.Name}' "
+ $"of guild '{request.Guild.Name}' (#{request.Guild.Id}) " + $"of guild '{request.Guild.Name}' "
+ $"does not contain any messages." + $"does not contain any messages."
); );
} }
@@ -41,8 +41,8 @@ public class ChannelExporter(DiscordClient discord)
if (request.After is not null && !request.Channel.MayHaveMessagesAfter(request.After.Value)) if (request.After is not null && !request.Channel.MayHaveMessagesAfter(request.After.Value))
{ {
throw new DiscordChatExporterException( throw new DiscordChatExporterException(
$"Channel '{request.Channel.Name}' (#{request.Channel.Id}) " $"Channel '{request.Channel.Name}' "
+ $"of guild '{request.Guild.Name}' (#{request.Guild.Id}) " + $"of guild '{request.Guild.Name}' "
+ $"does not contain any messages within the specified period." + $"does not contain any messages within the specified period."
); );
} }
@@ -54,8 +54,8 @@ public class ChannelExporter(DiscordClient discord)
) )
{ {
throw new DiscordChatExporterException( throw new DiscordChatExporterException(
$"Channel '{request.Channel.Name}' (#{request.Channel.Id}) " $"Channel '{request.Channel.Name}' "
+ $"of guild '{request.Guild.Name}' (#{request.Guild.Id}) " + $"of guild '{request.Guild.Name}' "
+ $"does not contain any messages within the specified period." + $"does not contain any messages within the specified period."
); );
} }
@@ -448,7 +448,10 @@
</div> </div>
} }
// Generic video embed // Generic video embed
else if (embed.Kind == EmbedKind.Video && !string.IsNullOrWhiteSpace(embed.Url)) else if (embed.Kind == EmbedKind.Video
&& !string.IsNullOrWhiteSpace(embed.Url)
// Twitch clips cannot be embedded in local HTML files
&& embed.TryGetTwitchClip() is null)
{ {
var embedVideoUrl = var embedVideoUrl =
embed.Video?.ProxyUrl ?? embed.Video?.Url ?? embed.Video?.ProxyUrl ?? embed.Video?.Url ??
@@ -702,6 +702,10 @@
.chatlog__embed-spotify { .chatlog__embed-spotify {
border: 0; border: 0;
} }
.chatlog__embed-twitch {
border: 0;
}
.chatlog__embed-youtube-container { .chatlog__embed-youtube-container {
margin-top: 0.6rem; margin-top: 0.6rem;
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env sh
# If we are root, ensure the files in /out are writable
# by the dce user and restart the process as the dce user
if [ "$(id -u)" = '0' ]; then
chown -R dce:dce /out
exec su-exec dce "$0" "$@"
fi
exec /opt/app/DiscordChatExporter.Cli "$@"