diff --git a/DiscordChatExporter.Cli.Tests/Specs/JsonContentSpecs.cs b/DiscordChatExporter.Cli.Tests/Specs/JsonContentSpecs.cs index 0f9701cf..ec1e7f1f 100644 --- a/DiscordChatExporter.Cli.Tests/Specs/JsonContentSpecs.cs +++ b/DiscordChatExporter.Cli.Tests/Specs/JsonContentSpecs.cs @@ -88,4 +88,4 @@ public class JsonContentSpecs "866674314627121232" ); } -} \ No newline at end of file +} diff --git a/DiscordChatExporter.Core/Discord/Data/Components/ActionRowComponent.cs b/DiscordChatExporter.Core/Discord/Data/Components/ActionRowComponent.cs deleted file mode 100644 index 4c012f3d..00000000 --- a/DiscordChatExporter.Core/Discord/Data/Components/ActionRowComponent.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using JsonExtensions.Reading; - -namespace DiscordChatExporter.Core.Discord.Data.Components; - -// https://discord.com/developers/docs/components/reference#action-row -public partial record ActionRowComponent(IReadOnlyList Components) -{ - public bool HasButtons => Components.Any(); -} - -public partial record ActionRowComponent -{ - public static ActionRowComponent? Parse(JsonElement json) - { - var type = json.GetPropertyOrNull("type")?.GetInt32OrNull(); - if (type != 1) - return null; - - var components = - json.GetPropertyOrNull("components") - ?.EnumerateArrayOrNull() - ?.Where(c => c.GetPropertyOrNull("type")?.GetInt32OrNull() == 2) // TODO: support other component types (selects) - ?.Select(ButtonComponent.Parse) - .ToArray() - ?? []; - - return new ActionRowComponent(components); - } -} diff --git a/DiscordChatExporter.Core/Discord/Data/Components/MessageComponent.cs b/DiscordChatExporter.Core/Discord/Data/Components/MessageComponent.cs new file mode 100644 index 00000000..82622aa9 --- /dev/null +++ b/DiscordChatExporter.Core/Discord/Data/Components/MessageComponent.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using DiscordChatExporter.Core.Utils.Extensions; +using JsonExtensions.Reading; + +namespace DiscordChatExporter.Core.Discord.Data.Components; + +// https://docs.discord.com/developers/components/reference#what-is-a-component +public partial record MessageComponent( + MessageComponentKind Kind, + IReadOnlyList Components, + ButtonComponent? Button +) +{ + public bool HasButtons => Button is not null || Components.Any(c => c.HasButtons); + + public IReadOnlyList Buttons => + Components.Select(c => c.Button).WhereNotNull().ToArray(); +} + +public partial record MessageComponent +{ + public static MessageComponent? Parse(JsonElement json) + { + var rawType = json.GetPropertyOrNull("type")?.GetInt32OrNull(); + if (rawType is null) + return null; + + var type = rawType.Value; + if (!Enum.IsDefined(typeof(MessageComponentKind), type)) + return null; + + var kind = (MessageComponentKind)type; + + var components = + json.GetPropertyOrNull("components") + ?.EnumerateArrayOrNull() + ?.Select(Parse) + .WhereNotNull() + .ToArray() + ?? []; + + var button = kind == MessageComponentKind.Button ? ButtonComponent.Parse(json) : null; + + return new MessageComponent(kind, components, button); + } +} diff --git a/DiscordChatExporter.Core/Discord/Data/Components/MessageComponentKind.cs b/DiscordChatExporter.Core/Discord/Data/Components/MessageComponentKind.cs new file mode 100644 index 00000000..a7e1c122 --- /dev/null +++ b/DiscordChatExporter.Core/Discord/Data/Components/MessageComponentKind.cs @@ -0,0 +1,22 @@ +namespace DiscordChatExporter.Core.Discord.Data.Components; + +// https://discord.com/developers/docs/components/reference#component-object-component-types +public enum MessageComponentKind +{ + ActionRow = 1, + Button = 2, + StringSelect = 3, + TextInput = 4, + UserSelect = 5, + RoleSelect = 6, + MentionableSelect = 7, + ChannelSelect = 8, + Section = 9, + TextDisplay = 10, + Thumbnail = 11, + MediaGallery = 12, + File = 13, + Separator = 14, + Container = 17, + Label = 18, +} diff --git a/DiscordChatExporter.Core/Discord/Data/Message.cs b/DiscordChatExporter.Core/Discord/Data/Message.cs index 60932828..7f4b1a16 100644 --- a/DiscordChatExporter.Core/Discord/Data/Message.cs +++ b/DiscordChatExporter.Core/Discord/Data/Message.cs @@ -22,9 +22,9 @@ public partial record Message( bool IsPinned, string Content, IReadOnlyList Attachments, - IReadOnlyList Components, IReadOnlyList Embeds, IReadOnlyList Stickers, + IReadOnlyList Components, IReadOnlyList Reactions, IReadOnlyList MentionedUsers, MessageReference? Reference, @@ -152,14 +152,6 @@ public partial record Message .ToArray() ?? []; - var components = - json.GetPropertyOrNull("components") - ?.EnumerateArrayOrNull() - ?.Select(ActionRowComponent.Parse) - .WhereNotNull() - .ToArray() - ?? []; - var embeds = NormalizeEmbeds( json.GetPropertyOrNull("embeds")?.EnumerateArrayOrNull()?.Select(Embed.Parse).ToArray() ?? [] @@ -172,6 +164,14 @@ public partial record Message .ToArray() ?? []; + var components = + json.GetPropertyOrNull("components") + ?.EnumerateArrayOrNull() + ?.Select(MessageComponent.Parse) + .WhereNotNull() + .ToArray() + ?? []; + var reactions = json.GetPropertyOrNull("reactions") ?.EnumerateArrayOrNull() @@ -209,9 +209,9 @@ public partial record Message isPinned, content, attachments, - components, embeds, stickers, + components, reactions, mentionedUsers, messageReference, @@ -220,4 +220,4 @@ public partial record Message interaction ); } -} \ No newline at end of file +} diff --git a/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs b/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs index c3c38739..988b4464 100644 --- a/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/JsonMessageWriter.cs @@ -353,7 +353,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context) _writer.WriteEndObject(); await _writer.FlushAsync(cancellationToken); } - + private async ValueTask WriteStickerAsync( Sticker sticker, CancellationToken cancellationToken = default @@ -373,45 +373,43 @@ internal class JsonMessageWriter(Stream stream, ExportContext context) await _writer.FlushAsync(cancellationToken); } - private async ValueTask WriteButtonComponentAsync( - ButtonComponent button, - { - _writer.WriteStartObject(); - - _writer.WriteString("type", "Button"); - _writer.WriteString("style", button.Style.ToString()); - _writer.WriteString("label", button.Label); - _writer.WriteString("url", button.Url); - _writer.WriteString("customId", button.CustomId); - _writer.WriteString("skuId", button.SkuId?.ToString()); - _writer.WriteBoolean("isDisabled", button.IsDisabled); - - if (button.Emoji is not null) - { - _writer.WritePropertyName("emoji"); - await WriteEmojiAsync(button.Emoji, cancellationToken); - } - - _writer.WriteEndObject(); - await _writer.FlushAsync(cancellationToken); - } - - private async ValueTask WriteActionRowComponentAsync( - ActionRowComponent actionRow, + private async ValueTask WriteComponentAsync( + MessageComponent component, CancellationToken cancellationToken = default ) { _writer.WriteStartObject(); - _writer.WriteString("type", "ActionRow"); + _writer.WriteString("type", component.Kind.ToString()); - _writer.WriteStartArray("components"); - foreach (var button in actionRow.Components) - await WriteButtonComponentAsync(button, cancellationToken); - _writer.WriteEndArray(); + if (component.Button is not null) + { + _writer.WriteString("style", component.Button.Style.ToString()); + _writer.WriteString("label", component.Button.Label); + _writer.WriteString("url", component.Button.Url); + _writer.WriteString("customId", component.Button.CustomId); + _writer.WriteString("skuId", component.Button.SkuId?.ToString()); + _writer.WriteBoolean("isDisabled", component.Button.IsDisabled); + + if (component.Button.Emoji is not null) + { + _writer.WritePropertyName("emoji"); + await WriteEmojiAsync(component.Button.Emoji, cancellationToken); + } + } + + if (component.Components.Any()) + { + _writer.WriteStartArray("components"); + + foreach (var child in component.Components) + await WriteComponentAsync(child, cancellationToken); + + _writer.WriteEndArray(); + } _writer.WriteEndObject(); - await _writer.FlushAsync(cancellationToken); + await _writer.FlushAsync(cancellationToken); } public override async ValueTask WritePreambleAsync( @@ -524,7 +522,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context) _writer.WriteStartArray("components"); foreach (var component in message.Components) - await WriteActionRowComponentAsync(component, cancellationToken); + await WriteComponentAsync(component, cancellationToken); _writer.WriteEndArray(); @@ -696,4 +694,4 @@ internal class JsonMessageWriter(Stream stream, ExportContext context) await _writer.DisposeAsync(); await base.DisposeAsync(); } -} \ No newline at end of file +} diff --git a/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml b/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml index b14e4122..7777619f 100644 --- a/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml @@ -733,13 +733,13 @@ } @* Components *@ - @if (message.Components.Any(c => c.HasButtons)) + @if (message.Components.Any(c => c.Kind == MessageComponentKind.ActionRow && c.HasButtons)) {
- @foreach (var actionRow in message.Components.Where(c => c.HasButtons)) + @foreach (var actionRow in message.Components.Where(c => c.Kind == MessageComponentKind.ActionRow && c.HasButtons)) {
- @foreach (var button in actionRow.Components) + @foreach (var button in actionRow.Buttons) { var styleClass = button.Style switch { ButtonStyle.Primary => "chatlog__component-button--primary", diff --git a/PKGBUILD b/PKGBUILD deleted file mode 100644 index 84942f94..00000000 --- a/PKGBUILD +++ /dev/null @@ -1,152 +0,0 @@ -# Maintainer: Vitalii Kuzhdin - -_sdk=10.0 -_Name="DiscordChatExporter" -pkgbase="discord-chat-exporter" -pkgname=( - "${pkgbase}-core" - "${pkgbase}-cli" - "${pkgbase}-gui" -) -pkgver=2.46.1 -pkgrel=1 -pkgdesc="Exports Discord chat logs to a file" -arch=( - 'aarch64' - 'armv7h' - 'x86_64' -) -url="https://github.com/Tyrrrz/${_Name}" -license=( - 'MIT' -) -depends=( - "dotnet-runtime-${_sdk}" -) -makedepends=( - "dotnet-sdk-${_sdk}" - 'gendesk' -) -options=( - '!strip' - '!debug' -) -_pkgsrc="${_Name}-${pkgver}" -source=( - "${pkgbase}_xdg_settings.patch" -) -b2sums=('ec3740a7c60b0f5fc2773e991e6cde9b4116d77d50094b237e118f456d9273c18a8e3bc2da2ff8a86eb35fa7df4f81c94759467b415f53e4794fb7a4e0929a91') - -if [ "${CARCH}" = 'aarch64' ]; then _msarch=arm64; -elif [ "${CARCH}" = 'armv7h' ]; then _msarch=arm; -elif [ "${CARCH}" = 'x86_64' ]; then _msarch=x64; fi - -_source() { - export NUGET_PACKAGES="${srcdir}/.nuget" - export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true - export DOTNET_NOLOGO=true - export DOTNET_CLI_TELEMETRY_OPTOUT=true -} - -prepare() { - _source - local dotnet_restore_options=( - --runtime "linux-${_msarch}" - --locked-mode - ) - - mkdir -p "${srcdir}/${_pkgsrc}" - rsync -a --delete \ - --exclude='/.git' \ - --exclude='/src' \ - --exclude='/pkg' \ - --exclude='/.nuget' \ - "${startdir}/" "${srcdir}/${_pkgsrc}/" - - cd "${srcdir}/${_pkgsrc}" - patch -Np1 -i "${srcdir}/${pkgbase}_xdg_settings.patch" - - for dir in Core Cli Gui; do - dotnet restore "${dotnet_restore_options[@]}" "${_Name}.${dir}" - done -} - -build() { - _source - local dotnet_publish_options=( - --configuration Release - --framework "net${_sdk}" - --no-restore - # --output build - --no-self-contained - --runtime "linux-${_msarch}" - -p:DebugType=None - -p:DebugSymbols=false - -p:Version="${pkgver%%.[A-Za-z]*}" - -p:PublishTrimmed=false - -p:PublishMacOSBundle=false - ) - - cd "${srcdir}" - gendesk -f -n \ - --pkgname "${pkgbase}-gui" \ - --pkgdesc "${pkgdesc}" \ - --name "Discord Chat Exporter (GUI)" \ - --exec "${pkgbase}-gui" \ - --icon "${pkgbase}" \ - --categories "Utility" - - cd "${_pkgsrc}" - dotnet publish "${dotnet_publish_options[@]}" --output build-core "${_Name}.Core" - - mkdir -p build-{cli,gui} - cp -aT build-core build-cli - cp -aT build-core build-gui - - dotnet publish "${dotnet_publish_options[@]}" --output build-cli "${_Name}.Cli" - dotnet publish "${dotnet_publish_options[@]}" --output build-gui "${_Name}.Gui" - - find build-core -type f | while read -r f; do - rel="${f#build-core/}" - rm -f "build-cli/$rel" "build-gui/$rel" - done -} - -package_discord-chat-exporter-core() { - pkgdesc+=" - Core" - - cd "${srcdir}/${_pkgsrc}" - install -vd "${pkgdir}/usr/lib/${pkgbase}" - cp -vaT --no-preserve=ownership "build-core" "${pkgdir}/usr/lib/${pkgbase}" - - install -vDm644 "Readme.md" "${pkgdir}/usr/share/doc/${pkgbase}/README.md" - install -vDm644 "License.txt" "${pkgdir}/usr/share/licenses/${pkgbase}/LICENSE" - install -vDm644 "favicon.png" "${pkgdir}/usr/share/pixmaps/${pkgbase}.png" -} - -package_discord-chat-exporter-cli() { - pkgdesc+=" - CLI" - depends+=( - "${pkgbase}-core>=${pkgver}-${pkgrel}" - ) - - cd "${srcdir}/${_pkgsrc}" - install -vd "${pkgdir}/usr/bin" "${pkgdir}/usr/lib/${pkgbase}" - cp -vaT --no-preserve=ownership "build-cli" "${pkgdir}/usr/lib/${pkgbase}" - ln -vsf "/usr/lib/${pkgbase}/${_Name}.Cli" "${pkgdir}/usr/bin/${pkgname}" -} - -package_discord-chat-exporter-gui() { - pkgdesc+=" - GUI" - depends+=( - "${pkgbase}-core>=${pkgver}-${pkgrel}" - ) - - cd "${srcdir}" - install -vDm644 "${pkgname}.desktop" "${pkgdir}/usr/share/applications/${pkgname}.desktop" - - cd "${_pkgsrc}" - install -vd "${pkgdir}/usr/bin" "${pkgdir}/usr/lib/${pkgbase}" - cp -vaT --no-preserve=ownership "build-gui" "${pkgdir}/usr/lib/${pkgbase}/" - ln -vsf "/usr/lib/${pkgbase}/${_Name}" "${pkgdir}/usr/bin/${pkgname}" -}