mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-16 02:31:33 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 966627f289 | |||
| 4ec00d0ec8 | |||
| 8bc1bcaf16 | |||
| 8f40acdda7 | |||
| bbe18368fa |
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.39.1 (10-Mar-2023)
|
||||||
|
|
||||||
|
- Fixed an issue where messages containing mentions of users that left the server (but haven't deleted their account) were incorrectly rendered as `@Unknown`.
|
||||||
|
- Fixed an issue where messages containing video links were not rendered correctly in the HTML export. Now, they are rendered using a video player and the referenced assets are pulled if the "download assets" option is enabled.
|
||||||
|
|
||||||
## v2.39 (25-Feb-2023)
|
## v2.39 (25-Feb-2023)
|
||||||
|
|
||||||
- Added an option to specify the directory path for downloaded assets. This can be used, in combination with the "reuse assets" option, to have a shared asset directory for multiple exports. In GUI, this option can be found in the advanced setion of the export dialog. In CLI, it's available as `--media-dir <path>`. (Thanks [@96-LB](https://github.com/96-LB))
|
- Added an option to specify the directory path for downloaded assets. This can be used, in combination with the "reuse assets" option, to have a shared asset directory for multiple exports. In GUI, this option can be found in the advanced setion of the export dialog. In CLI, it's available as `--media-dir <path>`. (Thanks [@96-LB](https://github.com/96-LB))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<Version>2.39</Version>
|
<Version>2.39.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>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
|||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -46,7 +47,8 @@ public class HtmlEmbedSpecs
|
|||||||
message
|
message
|
||||||
.QuerySelectorAll("img")
|
.QuerySelectorAll("img")
|
||||||
.Select(e => e.GetAttribute("src"))
|
.Select(e => e.GetAttribute("src"))
|
||||||
.Where(s => s?.EndsWith("i.redd.it/f8w05ja8s4e61.png") ?? false)
|
.WhereNotNull()
|
||||||
|
.Where(s => s.EndsWith("f8w05ja8s4e61.png"))
|
||||||
.Should()
|
.Should()
|
||||||
.ContainSingle();
|
.ContainSingle();
|
||||||
}
|
}
|
||||||
@@ -67,6 +69,25 @@ public class HtmlEmbedSpecs
|
|||||||
content.Should().BeNullOrEmpty();
|
content.Should().BeNullOrEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Message_with_a_video_link_is_rendered_with_a_video_embed()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.EmbedTestCases,
|
||||||
|
Snowflake.Parse("1083751036596002856")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
message
|
||||||
|
.QuerySelectorAll("source")
|
||||||
|
.Select(e => e.GetAttribute("src"))
|
||||||
|
.WhereNotNull()
|
||||||
|
.Where(s => s.EndsWith("i_am_currently_feeling_slight_displeasure_of_what_you_have_just_sent_lqrem.mp4"))
|
||||||
|
.Should()
|
||||||
|
.ContainSingle();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Message_with_a_GIFV_link_is_rendered_with_a_video_embed()
|
public async Task Message_with_a_GIFV_link_is_rendered_with_a_video_embed()
|
||||||
{
|
{
|
||||||
@@ -80,7 +101,8 @@ public class HtmlEmbedSpecs
|
|||||||
message
|
message
|
||||||
.QuerySelectorAll("source")
|
.QuerySelectorAll("source")
|
||||||
.Select(e => e.GetAttribute("src"))
|
.Select(e => e.GetAttribute("src"))
|
||||||
.Where(s => s?.EndsWith("media.tenor.com/DDAJeW6BQKkAAAPo/tooncasm-test-copy.mp4") ?? false)
|
.WhereNotNull()
|
||||||
|
.Where(s => s.EndsWith("tooncasm-test-copy.mp4"))
|
||||||
.Should()
|
.Should()
|
||||||
.ContainSingle();
|
.ContainSingle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,19 +23,6 @@ public partial record Channel(
|
|||||||
|
|
||||||
public partial record Channel
|
public partial record Channel
|
||||||
{
|
{
|
||||||
private static ChannelCategory GetFallbackCategory(ChannelKind channelKind) => new(
|
|
||||||
Snowflake.Zero,
|
|
||||||
channelKind switch
|
|
||||||
{
|
|
||||||
ChannelKind.GuildTextChat => "Text",
|
|
||||||
ChannelKind.DirectTextChat => "Private",
|
|
||||||
ChannelKind.DirectGroupTextChat => "Group",
|
|
||||||
ChannelKind.GuildNews => "News",
|
|
||||||
_ => "Default"
|
|
||||||
},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
public static Channel Parse(JsonElement json, ChannelCategory? categoryHint = null, int? positionHint = null)
|
public static Channel Parse(JsonElement json, ChannelCategory? categoryHint = null, int? positionHint = null)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||||
@@ -45,7 +32,7 @@ public partial record Channel
|
|||||||
json.GetPropertyOrNull("guild_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse) ??
|
json.GetPropertyOrNull("guild_id")?.GetNonWhiteSpaceStringOrNull()?.Pipe(Snowflake.Parse) ??
|
||||||
Guild.DirectMessages.Id;
|
Guild.DirectMessages.Id;
|
||||||
|
|
||||||
var category = categoryHint ?? GetFallbackCategory(kind);
|
var category = categoryHint ?? ChannelCategory.CreateDefault(kind);
|
||||||
|
|
||||||
var name =
|
var name =
|
||||||
// Guild channel
|
// Guild channel
|
||||||
|
|||||||
@@ -7,6 +7,19 @@ namespace DiscordChatExporter.Core.Discord.Data;
|
|||||||
|
|
||||||
public record ChannelCategory(Snowflake Id, string Name, int? Position) : IHasId
|
public record ChannelCategory(Snowflake Id, string Name, int? Position) : IHasId
|
||||||
{
|
{
|
||||||
|
public static ChannelCategory CreateDefault(ChannelKind channelKind) => new(
|
||||||
|
Snowflake.Zero,
|
||||||
|
channelKind switch
|
||||||
|
{
|
||||||
|
ChannelKind.GuildTextChat => "Text",
|
||||||
|
ChannelKind.DirectTextChat => "Private",
|
||||||
|
ChannelKind.DirectGroupTextChat => "Group",
|
||||||
|
ChannelKind.GuildNews => "News",
|
||||||
|
_ => "Default"
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
public static ChannelCategory Parse(JsonElement json, int? positionHint = null)
|
public static ChannelCategory Parse(JsonElement json, int? positionHint = null)
|
||||||
{
|
{
|
||||||
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ public partial record Member(
|
|||||||
|
|
||||||
public partial record Member
|
public partial record Member
|
||||||
{
|
{
|
||||||
|
public static Member CreateDefault(User user) => new(user, null, null, Array.Empty<Snowflake>());
|
||||||
|
|
||||||
public static Member Parse(JsonElement json, Snowflake? guildId = null)
|
public static Member Parse(JsonElement json, Snowflake? guildId = null)
|
||||||
{
|
{
|
||||||
var user = json.GetProperty("user").Pipe(User.Parse);
|
var user = json.GetProperty("user").Pipe(User.Parse);
|
||||||
|
|||||||
@@ -150,6 +150,14 @@ public class DiscordClient
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask<User?> TryGetUserAsync(
|
||||||
|
Snowflake userId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var response = await TryGetJsonResponseAsync($"users/{userId}", cancellationToken);
|
||||||
|
return response?.Pipe(User.Parse);
|
||||||
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<Guild> GetUserGuildsAsync(
|
public async IAsyncEnumerable<Guild> GetUserGuildsAsync(
|
||||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncKeyedLock" Version="6.1.1" />
|
<PackageReference Include="AsyncKeyedLock" Version="6.2.0" />
|
||||||
<PackageReference Include="Gress" Version="2.0.1" />
|
<PackageReference Include="Gress" Version="2.0.1" />
|
||||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||||
<PackageReference Include="Polly" Version="7.2.3" />
|
<PackageReference Include="Polly" Version="7.2.3" />
|
||||||
@@ -14,4 +14,4 @@
|
|||||||
<PackageReference Include="WebMarkupMin.Core" Version="2.13.8" />
|
<PackageReference Include="WebMarkupMin.Core" Version="2.13.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class ChannelExporter
|
|||||||
{
|
{
|
||||||
// Resolve members for referenced users
|
// Resolve members for referenced users
|
||||||
foreach (var user in message.GetReferencedUsers())
|
foreach (var user in message.GetReferencedUsers())
|
||||||
await context.PopulateMemberAsync(user.Id, cancellationToken);
|
await context.PopulateMemberAsync(user, cancellationToken);
|
||||||
|
|
||||||
// Export the message
|
// Export the message
|
||||||
if (request.MessageFilter.IsMatch(message))
|
if (request.MessageFilter.IsMatch(message))
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ internal class ExportContext
|
|||||||
private readonly ExportAssetDownloader _assetDownloader;
|
private readonly ExportAssetDownloader _assetDownloader;
|
||||||
|
|
||||||
public DiscordClient Discord { get; }
|
public DiscordClient Discord { get; }
|
||||||
|
|
||||||
public ExportRequest Request { get; }
|
public ExportRequest Request { get; }
|
||||||
|
|
||||||
public ExportContext(DiscordClient discord,
|
public ExportContext(DiscordClient discord,
|
||||||
@@ -43,18 +44,35 @@ internal class ExportContext
|
|||||||
_roles[role.Id] = role;
|
_roles[role.Id] = role;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Because members are not pulled in bulk, we need to populate them on demand
|
// Because members cannot be pulled in bulk, we need to populate them on demand
|
||||||
public async ValueTask PopulateMemberAsync(Snowflake id, CancellationToken cancellationToken = default)
|
private async ValueTask PopulateMemberAsync(
|
||||||
|
Snowflake id,
|
||||||
|
User? fallbackUser,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_members.ContainsKey(id))
|
if (_members.ContainsKey(id))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var member = await Discord.TryGetGuildMemberAsync(Request.Guild.Id, id, cancellationToken);
|
var member = await Discord.TryGetGuildMemberAsync(Request.Guild.Id, id, cancellationToken);
|
||||||
|
|
||||||
|
// User may have left the guild since they were mentioned
|
||||||
|
if (member is null)
|
||||||
|
{
|
||||||
|
var user = fallbackUser ?? await Discord.TryGetUserAsync(id, cancellationToken);
|
||||||
|
if (user is not null)
|
||||||
|
member = Member.CreateDefault(user);
|
||||||
|
}
|
||||||
|
|
||||||
// Store the result even if it's null, to avoid re-fetching non-existing members
|
// Store the result even if it's null, to avoid re-fetching non-existing members
|
||||||
_members[id] = member;
|
_members[id] = member;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask PopulateMemberAsync(Snowflake id, CancellationToken cancellationToken = default) =>
|
||||||
|
await PopulateMemberAsync(id, null, cancellationToken);
|
||||||
|
|
||||||
|
public async ValueTask PopulateMemberAsync(User user, CancellationToken cancellationToken = default) =>
|
||||||
|
await PopulateMemberAsync(user.Id, user, cancellationToken);
|
||||||
|
|
||||||
public string FormatDate(DateTimeOffset instant) => Request.DateFormat switch
|
public string FormatDate(DateTimeOffset instant) => Request.DateFormat switch
|
||||||
{
|
{
|
||||||
"unix" => instant.ToUnixTimeSeconds().ToString(),
|
"unix" => instant.ToUnixTimeSeconds().ToString(),
|
||||||
|
|||||||
@@ -447,6 +447,19 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
// Generic video embed
|
||||||
|
else if (embed.Kind == EmbedKind.Video && !string.IsNullOrWhiteSpace(embed.Url))
|
||||||
|
{
|
||||||
|
var embedVideoUrl =
|
||||||
|
embed.Video?.ProxyUrl ?? embed.Video?.Url ??
|
||||||
|
embed.Url;
|
||||||
|
|
||||||
|
<div class="chatlog__embed">
|
||||||
|
<video class="chatlog__embed-generic-video" width="@embed.Video?.Width" height="@embed.Video?.Height" controls>
|
||||||
|
<source src="@await ResolveAssetUrlAsync(embedVideoUrl)" alt="Embedded video">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
// Generic gifv embed
|
// Generic gifv embed
|
||||||
else if (embed.Kind == EmbedKind.Gifv && !string.IsNullOrWhiteSpace(embed.Url))
|
else if (embed.Kind == EmbedKind.Gifv && !string.IsNullOrWhiteSpace(embed.Url))
|
||||||
{
|
{
|
||||||
@@ -455,8 +468,8 @@
|
|||||||
embed.Url;
|
embed.Url;
|
||||||
|
|
||||||
<div class="chatlog__embed">
|
<div class="chatlog__embed">
|
||||||
<video class="chatlog__embed-generic-gifv" loop width="@embed.Video?.Width" height="@embed.Video?.Height" onmouseover="this.play()" onmouseout="this.pause()">
|
<video class="chatlog__embed-generic-gifv" width="@embed.Video?.Width" height="@embed.Video?.Height" loop onmouseover="this.play()" onmouseout="this.pause()">
|
||||||
<source src="@await ResolveAssetUrlAsync(embedVideoUrl)" alt="Embedded video">
|
<source src="@await ResolveAssetUrlAsync(embedVideoUrl)" alt="Embedded gifv">
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -622,24 +622,6 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatlog__embed-generic-image {
|
|
||||||
object-fit: contain;
|
|
||||||
object-position: left;
|
|
||||||
max-width: 45vw;
|
|
||||||
max-height: 500px;
|
|
||||||
vertical-align: top;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-generic-gifv {
|
|
||||||
object-fit: contain;
|
|
||||||
object-position: left;
|
|
||||||
max-width: 45vw;
|
|
||||||
max-height: 500px;
|
|
||||||
vertical-align: top;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatlog__embed-invite-container {
|
.chatlog__embed-invite-container {
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
padding: 0.6rem 0.7rem;
|
padding: 0.6rem 0.7rem;
|
||||||
@@ -690,6 +672,33 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-generic-image {
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: left;
|
||||||
|
max-width: 45vw;
|
||||||
|
max-height: 500px;
|
||||||
|
vertical-align: top;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-generic-video {
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: left;
|
||||||
|
max-width: 45vw;
|
||||||
|
max-height: 500px;
|
||||||
|
vertical-align: top;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatlog__embed-generic-gifv {
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: left;
|
||||||
|
max-width: 45vw;
|
||||||
|
max-height: 500px;
|
||||||
|
vertical-align: top;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.chatlog__embed-spotify {
|
.chatlog__embed-spotify {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user