refactor: pt 1 of redesign for generic components structure

This commit is contained in:
Solareon 2026-03-04 11:59:29 +01:00
parent 1a671b31e6
commit 0414da2fb7
8 changed files with 118 additions and 233 deletions

View file

@ -88,4 +88,4 @@ public class JsonContentSpecs
"866674314627121232" "866674314627121232"
); );
} }
} }

View file

@ -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<ButtonComponent> 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);
}
}

View file

@ -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<MessageComponent> Components,
ButtonComponent? Button
)
{
public bool HasButtons => Button is not null || Components.Any(c => c.HasButtons);
public IReadOnlyList<ButtonComponent> 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);
}
}

View file

@ -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,
}

View file

@ -22,9 +22,9 @@ public partial record Message(
bool IsPinned, bool IsPinned,
string Content, string Content,
IReadOnlyList<Attachment> Attachments, IReadOnlyList<Attachment> Attachments,
IReadOnlyList<ActionRowComponent> Components,
IReadOnlyList<Embed> Embeds, IReadOnlyList<Embed> Embeds,
IReadOnlyList<Sticker> Stickers, IReadOnlyList<Sticker> Stickers,
IReadOnlyList<MessageComponent> Components,
IReadOnlyList<Reaction> Reactions, IReadOnlyList<Reaction> Reactions,
IReadOnlyList<User> MentionedUsers, IReadOnlyList<User> MentionedUsers,
MessageReference? Reference, MessageReference? Reference,
@ -152,14 +152,6 @@ public partial record Message
.ToArray() .ToArray()
?? []; ?? [];
var components =
json.GetPropertyOrNull("components")
?.EnumerateArrayOrNull()
?.Select(ActionRowComponent.Parse)
.WhereNotNull()
.ToArray()
?? [];
var embeds = NormalizeEmbeds( var embeds = NormalizeEmbeds(
json.GetPropertyOrNull("embeds")?.EnumerateArrayOrNull()?.Select(Embed.Parse).ToArray() json.GetPropertyOrNull("embeds")?.EnumerateArrayOrNull()?.Select(Embed.Parse).ToArray()
?? [] ?? []
@ -172,6 +164,14 @@ public partial record Message
.ToArray() .ToArray()
?? []; ?? [];
var components =
json.GetPropertyOrNull("components")
?.EnumerateArrayOrNull()
?.Select(MessageComponent.Parse)
.WhereNotNull()
.ToArray()
?? [];
var reactions = var reactions =
json.GetPropertyOrNull("reactions") json.GetPropertyOrNull("reactions")
?.EnumerateArrayOrNull() ?.EnumerateArrayOrNull()
@ -209,9 +209,9 @@ public partial record Message
isPinned, isPinned,
content, content,
attachments, attachments,
components,
embeds, embeds,
stickers, stickers,
components,
reactions, reactions,
mentionedUsers, mentionedUsers,
messageReference, messageReference,
@ -220,4 +220,4 @@ public partial record Message
interaction interaction
); );
} }
} }

View file

@ -353,7 +353,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
_writer.WriteEndObject(); _writer.WriteEndObject();
await _writer.FlushAsync(cancellationToken); await _writer.FlushAsync(cancellationToken);
} }
private async ValueTask WriteStickerAsync( private async ValueTask WriteStickerAsync(
Sticker sticker, Sticker sticker,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
@ -373,45 +373,43 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
await _writer.FlushAsync(cancellationToken); await _writer.FlushAsync(cancellationToken);
} }
private async ValueTask WriteButtonComponentAsync( private async ValueTask WriteComponentAsync(
ButtonComponent button, MessageComponent component,
{
_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,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
_writer.WriteStartObject(); _writer.WriteStartObject();
_writer.WriteString("type", "ActionRow"); _writer.WriteString("type", component.Kind.ToString());
_writer.WriteStartArray("components"); if (component.Button is not null)
foreach (var button in actionRow.Components) {
await WriteButtonComponentAsync(button, cancellationToken); _writer.WriteString("style", component.Button.Style.ToString());
_writer.WriteEndArray(); _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(); _writer.WriteEndObject();
await _writer.FlushAsync(cancellationToken); await _writer.FlushAsync(cancellationToken);
} }
public override async ValueTask WritePreambleAsync( public override async ValueTask WritePreambleAsync(
@ -524,7 +522,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
_writer.WriteStartArray("components"); _writer.WriteStartArray("components");
foreach (var component in message.Components) foreach (var component in message.Components)
await WriteActionRowComponentAsync(component, cancellationToken); await WriteComponentAsync(component, cancellationToken);
_writer.WriteEndArray(); _writer.WriteEndArray();
@ -696,4 +694,4 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
await _writer.DisposeAsync(); await _writer.DisposeAsync();
await base.DisposeAsync(); await base.DisposeAsync();
} }
} }

View file

@ -733,13 +733,13 @@
} }
@* Components *@ @* Components *@
@if (message.Components.Any(c => c.HasButtons)) @if (message.Components.Any(c => c.Kind == MessageComponentKind.ActionRow && c.HasButtons))
{ {
<div class="chatlog__components"> <div class="chatlog__components">
@foreach (var actionRow in message.Components.Where(c => c.HasButtons)) @foreach (var actionRow in message.Components.Where(c => c.Kind == MessageComponentKind.ActionRow && c.HasButtons))
{ {
<div class="chatlog__action-row"> <div class="chatlog__action-row">
@foreach (var button in actionRow.Components) @foreach (var button in actionRow.Buttons)
{ {
var styleClass = button.Style switch { var styleClass = button.Style switch {
ButtonStyle.Primary => "chatlog__component-button--primary", ButtonStyle.Primary => "chatlog__component-button--primary",

152
PKGBUILD
View file

@ -1,152 +0,0 @@
# Maintainer: Vitalii Kuzhdin <vitaliikuzhdin@gmail.com>
_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}"
}